This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git

commit 283d1eac377dfdab64934641eea62590d3cbfb95
Author: Yang Luo <[email protected]>
AuthorDate: Sat Aug 15 14:01:19 2026 +0800

    fix(agentpatch): resolve the owner home strictly and verify patch targets
    
    homeOf fell back to os.UserHomeDir() whenever user.Lookup failed. Lookup
    routinely fails for DOMAIN\user on Windows, so patching an installation 
owned
    by another account silently wrote the hook into the home of whichever 
account
    runs Gateway: the operator saw Patched while nothing was collected and an
    unrelated settings.json had been rewritten. An unresolvable owner is now an
    error, with a fallback only when the owner really is the current account.
    
    PatchAgent and UnpatchAgent also took the agentId/path/owner triple straight
    from the request body. Combined with the home lookup that is a write 
primitive
    into any account's home, so the target is now matched against the discovered
    installations before it is used.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
---
 agentpatch/home.go   | 67 ++++++++++++++++++++++++++++++++++++++++++++++++----
 controllers/agent.go | 25 +++++++++++++++++++-
 2 files changed, 87 insertions(+), 5 deletions(-)

diff --git a/agentpatch/home.go b/agentpatch/home.go
index 7ae7263..24659ca 100644
--- a/agentpatch/home.go
+++ b/agentpatch/home.go
@@ -15,15 +15,74 @@
 package agentpatch
 
 import (
+       "fmt"
        "os"
        "os/user"
+       "path/filepath"
+       "strings"
 )
 
+// homeOf resolves the home directory of the installation owner. Patching 
writes
+// into that home, so guessing here would silently modify the wrong account's
+// configuration: an unresolvable owner is an error rather than a fallback to
+// whichever account happens to run Gateway.
 func homeOf(target Target) (string, error) {
-       if target.Owner != "" {
-               if account, err := user.Lookup(target.Owner); err == nil && 
account.HomeDir != "" {
-                       return account.HomeDir, nil
+       owner := strings.TrimSpace(target.Owner)
+       if owner == "" {
+               home, err := os.UserHomeDir()
+               if err != nil {
+                       return "", fmt.Errorf("resolve home directory: %w", err)
                }
+               return home, nil
        }
-       return os.UserHomeDir()
+
+       if account, err := lookupAccount(owner); err == nil {
+               if account.HomeDir == "" {
+                       return "", fmt.Errorf("account %q has no home 
directory", owner)
+               }
+               return account.HomeDir, nil
+       }
+
+       // user.Lookup is unreliable for domain-qualified names on Windows and 
for
+       // directory-backed accounts on Linux, so fall back to the process home 
only
+       // when the owner really is the account Gateway runs as.
+       if current, err := user.Current(); err == nil && sameAccount(owner, 
current.Username) {
+               home, err := os.UserHomeDir()
+               if err != nil {
+                       return "", fmt.Errorf("resolve home directory of %q: 
%w", owner, err)
+               }
+               return home, nil
+       }
+       return "", fmt.Errorf("cannot resolve the home directory of %q; run 
Gateway as that user to patch this installation", owner)
+}
+
+// lookupAccount resolves an owner name, retrying with the bare account name so
+// that DOMAIN\user and user@domain forms also resolve.
+func lookupAccount(owner string) (*user.User, error) {
+       account, err := user.Lookup(owner)
+       if err == nil {
+               return account, nil
+       }
+       if bare := accountName(owner); bare != owner && bare != "" {
+               if account, bareErr := user.Lookup(bare); bareErr == nil {
+                       return account, nil
+               }
+       }
+       return nil, err
+}
+
+func sameAccount(left, right string) bool {
+       return strings.EqualFold(accountName(left), accountName(right))
+}
+
+// accountName strips any domain qualifier from DOMAIN\user or user@domain.
+func accountName(value string) string {
+       value = strings.TrimSpace(value)
+       if index := strings.LastIndexAny(value, `\/`); index >= 0 {
+               value = value[index+1:]
+       }
+       if index := strings.Index(value, "@"); index > 0 {
+               value = value[:index]
+       }
+       return filepath.Base(value)
 }
diff --git a/controllers/agent.go b/controllers/agent.go
index 1c41af5..cd036ff 100644
--- a/controllers/agent.go
+++ b/controllers/agent.go
@@ -17,7 +17,9 @@ package controllers
 import (
        "encoding/json"
        "net"
+       "path/filepath"
        "strconv"
+       "strings"
 
        "github.com/apache/casbin-gateway/agent"
        "github.com/apache/casbin-gateway/agentmonitor"
@@ -147,13 +149,34 @@ func (c *ApiController) AddAgentRecord() {
        c.ResponseOk()
 }
 
+// readAgentPatchTarget resolves the request body against the installations 
that
+// were actually discovered. Patching writes into the owner's home directory, 
so
+// an unverified body would let a caller name any account on the host.
 func (c *ApiController) readAgentPatchTarget() (agentpatch.Target, bool) {
        var target agentpatch.Target
        if err := json.Unmarshal(c.Ctx.Input.RequestBody, &target); err != nil {
                c.ResponseError(err.Error())
                return target, false
        }
-       return target, true
+
+       installations, err := agent.Scan(false)
+       if err != nil {
+               c.ResponseError(err.Error())
+               return target, false
+       }
+       for _, installation := range installations {
+               if candidate := targetOf(installation); 
matchesTarget(candidate, target) {
+                       return candidate, true
+               }
+       }
+       c.ResponseError("no discovered agent installation matches this target")
+       return target, false
+}
+
+func matchesTarget(discovered, requested agentpatch.Target) bool {
+       return discovered.AgentId == requested.AgentId &&
+               strings.EqualFold(filepath.Clean(discovered.Path), 
filepath.Clean(requested.Path)) &&
+               strings.EqualFold(discovered.Owner, requested.Owner)
 }
 
 func targetOf(installation agent.Installation) agentpatch.Target {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to