Skip to content

Commit 293758c

Browse files
Implemented getting file from mounted directories (with security considerations) and fixed some error messages in auth
1 parent 59117cc commit 293758c

File tree

4 files changed

+145
-8
lines changed

4 files changed

+145
-8
lines changed

internal/auth/handler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
5555
response := map[string]string{"token": token}
5656
w.Header().Set("Content-Type", "application/json")
5757
if err := json.NewEncoder(w).Encode(response); err != nil {
58-
zap.L().Error("Failed to encode response",
58+
zap.L().Error("Failed to encode response for login request",
5959
zap.Error(err),
6060
)
61-
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
61+
http.Error(w, "Failed to encode response for login request", http.StatusInternalServerError)
6262
return
6363
}
6464
}

internal/traversal/handler.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,55 @@
11
package traversal
22

3+
import (
4+
"encoding/json"
5+
"net/http"
36

7+
"go.uber.org/zap"
8+
"github.com/PythonHacker24/linux-acl-management-backend/internal/auth"
9+
)
10+
11+
/*
12+
user considers / to be the root of the file path
13+
the backend transalates / to basepath/ securely
14+
this translation needs to be done wherever necessary
15+
*/
16+
17+
/* POST handler for listing files in given directory */
18+
func ListFilesInDirectory(w http.ResponseWriter, r *http.Request) {
19+
20+
/* extracting userID from request */
21+
userID, err := auth.ExtractUsernameFromRequest(r)
22+
if err != nil {
23+
zap.L().Error("Error during getting username in HandleListFiles handler",
24+
zap.Error(err),
25+
)
26+
return
27+
}
28+
29+
/* check if the request body is valid */
30+
var listRequest ListRequest
31+
err = json.NewDecoder(r.Body).Decode(&listRequest)
32+
if err != nil {
33+
http.Error(w, "Invalid request body", http.StatusBadRequest)
34+
return
35+
}
36+
37+
/* list all the files in given filepath */
38+
entries, err := ListFiles(listRequest.FilePath, userID)
39+
if err != nil {
40+
zap.L().Warn("File listing error",
41+
zap.Error(err),
42+
)
43+
http.Error(w, "Failed to list files", http.StatusInternalServerError)
44+
}
45+
46+
/* send the response back */
47+
w.Header().Set("Content-Type", "application/json")
48+
if err := json.NewEncoder(w).Encode(entries); err != nil {
49+
zap.L().Error("Failed to encode response for listing request",
50+
zap.Error(err),
51+
)
52+
http.Error(w, "Failed to encode response for listing request", http.StatusInternalServerError)
53+
return
54+
}
55+
}

internal/traversal/model.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ package traversal
77
type FileEntry struct {
88
Name string `json:"name"`
99
Path string `json:"path"`
10-
IsDir bool `json:"isDir"`
10+
IsDir bool `json:"is_dir"`
1111
Size int64 `json:"size"`
12-
ModTime int64 `json:"modTime"`
12+
ModTime int64 `json:"mod_time"`
13+
}
14+
15+
/* request for listing files in a given directory path */
16+
type ListRequest struct {
17+
FilePath string `json:"file_path"`
1318
}

internal/traversal/traversal.go

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,33 @@
11
package traversal
22

33
import (
4+
"fmt"
45
"os"
6+
"os/exec"
57
"path/filepath"
8+
"strings"
9+
10+
"github.com/PythonHacker24/linux-acl-management-backend/config"
11+
"go.uber.org/zap"
612
)
713

14+
/* list files in a given directory with some basic information */
815
func ListFiles(path string, userID string) ([]FileEntry, error) {
916
var entries []FileEntry
1017

11-
/* list all the files in the given directory */
12-
files, err := os.ReadDir(path)
18+
/* combine basePath with the requested path */
19+
fullPath := filepath.Join(config.BackendConfig.AppInfo.BasePath, path)
20+
21+
/* clean the path to prevent directory traversal */
22+
fullPath = filepath.Clean(fullPath)
23+
24+
/* ensure the resulting path is still within the basePath (prevent directory traversal) */
25+
if !strings.HasPrefix(fullPath, filepath.Clean(config.BackendConfig.AppInfo.BasePath)) {
26+
return nil, fmt.Errorf("Path traversal attempt detected: %s", path)
27+
}
28+
29+
/* list all the files in the given directory */
30+
files, err := os.ReadDir(fullPath)
1331
if err != nil {
1432
return nil, err
1533
}
@@ -19,8 +37,12 @@ func ListFiles(path string, userID string) ([]FileEntry, error) {
1937
fullPath := filepath.Join(path, f.Name())
2038

2139
/* check ACL access first */
22-
hasAccess := checkACLAccess(fullPath, userID)
23-
if !hasAccess {
40+
isOwner, err := isOwner(fullPath, userID)
41+
if err != nil {
42+
return nil, fmt.Errorf("error during listing files: %w", err)
43+
}
44+
45+
if !isOwner {
2446
/* if the user doesn't have right ACL permissions for the file, skip it */
2547
continue
2648
}
@@ -43,3 +65,61 @@ func ListFiles(path string, userID string) ([]FileEntry, error) {
4365

4466
return entries, nil
4567
}
68+
69+
/*
70+
checks if the user is the owner of the file
71+
username is the LDAP CN for the user
72+
uses getfacl to fetch the permissions (usually from filesystems mounted from remote servers)
73+
*/
74+
func isOwner(filePath string, userCN string) (bool, error) {
75+
76+
cleanPath := filepath.Clean(filePath)
77+
78+
/* additional validation to ensure that the path doesn't contain dangerous characters */
79+
if strings.Contains(cleanPath, ";") || strings.Contains(cleanPath, "|") ||
80+
strings.Contains(cleanPath, "&") || strings.Contains(cleanPath, "`") ||
81+
strings.Contains(cleanPath, "$") || strings.Contains(cleanPath, "(") ||
82+
strings.Contains(cleanPath, ")") {
83+
zap.L().Warn("Illegal method attempted while getting file path by injecting dangerous character in the file path!")
84+
return false, fmt.Errorf("invalid characters in file path: %s", cleanPath)
85+
}
86+
87+
/* get the file's ACL using getfacl with properly escaped arguments */
88+
cmd := exec.Command("getfacl", "--", cleanPath)
89+
output, err := cmd.Output()
90+
if err != nil {
91+
return false, fmt.Errorf("failed to execute getfacl on %s: %v", cleanPath, err)
92+
}
93+
94+
/* parse the getfacl output to check ownership */
95+
lines := strings.Split(string(output), "\n")
96+
for _, line := range lines {
97+
line = strings.TrimSpace(line)
98+
99+
/* check for owner line (format: "# owner: username") */
100+
if strings.HasPrefix(line, "# owner:") {
101+
owner := strings.TrimSpace(strings.TrimPrefix(line, "# owner:"))
102+
103+
/* compare with the provided CN (case-insensitive) */
104+
if strings.EqualFold(owner, userCN) {
105+
return true, nil
106+
}
107+
}
108+
109+
/* also check user ACL entries (format: "user:username:permissions") */
110+
if strings.HasPrefix(line, "user:") && !strings.HasPrefix(line, "user::") {
111+
parts := strings.Split(line, ":")
112+
if len(parts) >= 3 {
113+
aclUser := parts[1]
114+
permissions := parts[2]
115+
116+
/* check if this user has write permissions (indicating ownership-like access) */
117+
if strings.EqualFold(aclUser, userCN) && strings.Contains(permissions, "w") {
118+
return true, nil
119+
}
120+
}
121+
}
122+
}
123+
124+
return false, nil
125+
}

0 commit comments

Comments
 (0)