• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gameap / gameap / 25960901307

16 May 2026 11:29AM UTC coverage: 76.887% (+0.2%) from 76.64%
25960901307

push

github

et-nik
audit logs tests

45395 of 59041 relevant lines covered (76.89%)

33895.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.92
/internal/api/filemanager/chmod/handler.go
1
package chmod
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "log/slog"
7
        "net/http"
8
        "path/filepath"
9
        "strconv"
10
        "strings"
11

12
        "github.com/gameap/gameap/internal/api/base"
13
        serversbase "github.com/gameap/gameap/internal/api/servers/base"
14
        "github.com/gameap/gameap/internal/audit"
15
        "github.com/gameap/gameap/internal/domain"
16
        "github.com/gameap/gameap/internal/filters"
17
        "github.com/gameap/gameap/internal/repositories"
18
        "github.com/gameap/gameap/pkg/api"
19
        "github.com/gameap/gameap/pkg/auth"
20
        "github.com/pkg/errors"
21
)
22

23
const maxMode = 0o777
24

25
type Handler struct {
26
        serverFinder   *serversbase.ServerFinder
27
        abilityChecker *serversbase.AbilityChecker
28
        nodeRepo       repositories.NodeRepository
29
        daemonFiles    fileService
30
        responder      base.Responder
31
        audit          audit.Logger
32
}
33

34
func NewHandler(
35
        serverRepo repositories.ServerRepository,
36
        nodeRepo repositories.NodeRepository,
37
        rbac base.RBAC,
38
        daemonFiles fileService,
39
        responder base.Responder,
40
        auditLogger audit.Logger,
41
) *Handler {
21✔
42
        if auditLogger == nil {
40✔
43
                auditLogger = audit.NopLogger{}
19✔
44
        }
19✔
45

46
        return &Handler{
21✔
47
                serverFinder:   serversbase.NewServerFinder(serverRepo, rbac),
21✔
48
                abilityChecker: serversbase.NewAbilityChecker(rbac),
21✔
49
                nodeRepo:       nodeRepo,
21✔
50
                daemonFiles:    daemonFiles,
21✔
51
                responder:      responder,
21✔
52
                audit:          auditLogger,
21✔
53
        }
21✔
54
}
55

56
func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
21✔
57
        ctx := r.Context()
21✔
58

21✔
59
        session := auth.SessionFromContext(ctx)
21✔
60
        if !session.IsAuthenticated() {
22✔
61
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
62
                        errors.New("user not authenticated"),
1✔
63
                        http.StatusUnauthorized,
1✔
64
                ))
1✔
65

1✔
66
                return
1✔
67
        }
1✔
68

69
        input := api.NewInputReader(r)
20✔
70

20✔
71
        serverID, err := input.ReadUint("server")
20✔
72
        if err != nil {
21✔
73
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
74
                        errors.WithMessage(err, "invalid server id"),
1✔
75
                        http.StatusBadRequest,
1✔
76
                ))
1✔
77

1✔
78
                return
1✔
79
        }
1✔
80

81
        server, err := h.serverFinder.FindUserServer(ctx, session.User, serverID)
19✔
82
        if err != nil {
21✔
83
                h.responder.WriteError(ctx, rw, err)
2✔
84

2✔
85
                return
2✔
86
        }
2✔
87

88
        err = h.abilityChecker.CheckOrError(
17✔
89
                ctx,
17✔
90
                session.User.ID,
17✔
91
                server.ID,
17✔
92
                []domain.AbilityName{domain.AbilityNameGameServerFiles},
17✔
93
        )
17✔
94
        if err != nil {
18✔
95
                h.responder.WriteError(ctx, rw, err)
1✔
96

1✔
97
                return
1✔
98
        }
1✔
99

100
        var req chmodRequest
16✔
101
        err = json.NewDecoder(r.Body).Decode(&req)
16✔
102
        if err != nil {
17✔
103
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
104
                        errors.WithMessage(err, "invalid request body"),
1✔
105
                        http.StatusBadRequest,
1✔
106
                ))
1✔
107

1✔
108
                return
1✔
109
        }
1✔
110

111
        if err = h.validateRequest(&req); err != nil {
19✔
112
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(err, http.StatusBadRequest))
4✔
113

4✔
114
                return
4✔
115
        }
4✔
116

117
        node, err := h.getNode(ctx, server.DSID)
11✔
118
        if err != nil {
12✔
119
                h.responder.WriteError(ctx, rw, err)
1✔
120

1✔
121
                return
1✔
122
        }
1✔
123

124
        if err = h.processItems(ctx, node, server.Dir, &req); err != nil {
13✔
125
                h.responder.WriteError(ctx, rw, err)
3✔
126

3✔
127
                return
3✔
128
        }
3✔
129

130
        audit.SensitiveOp(ctx, h.audit, audit.EventFileChmod, audit.CategoryFileOp,
7✔
131
                "server", strconv.FormatUint(uint64(serverID), 10), "chmod",
7✔
132
                slog.Int("items", len(req.Items)), slog.Int("mode", req.Mode))
7✔
133

7✔
134
        h.responder.Write(ctx, rw, newChmodResponse())
7✔
135
}
136

137
func (h *Handler) validateRequest(req *chmodRequest) error {
15✔
138
        if req.Disk != "server" {
16✔
139
                return errors.Errorf("unsupported disk: %s, only 'server' disk is supported", req.Disk)
1✔
140
        }
1✔
141

142
        if len(req.Items) == 0 {
15✔
143
                return errors.New("items array is empty")
1✔
144
        }
1✔
145

146
        if req.Mode < 0 || req.Mode > maxMode {
15✔
147
                return errors.Errorf("invalid mode: %d, must be between 0 and 0o777", req.Mode)
2✔
148
        }
2✔
149

150
        return nil
11✔
151
}
152

153
func (h *Handler) getNode(ctx context.Context, nodeID uint) (*domain.Node, error) {
11✔
154
        nodes, err := h.nodeRepo.Find(ctx, &filters.FindNode{
11✔
155
                IDs: []uint{nodeID},
11✔
156
        }, nil, &filters.Pagination{
11✔
157
                Limit: 1,
11✔
158
        })
11✔
159
        if err != nil {
11✔
160
                return nil, errors.WithMessage(err, "failed to find node")
×
161
        }
×
162

163
        if len(nodes) == 0 {
12✔
164
                return nil, api.NewNotFoundError("node not found")
1✔
165
        }
1✔
166

167
        return &nodes[0], nil
10✔
168
}
169

170
func (h *Handler) processItems(
171
        ctx context.Context,
172
        node *domain.Node,
173
        serverDir string,
174
        req *chmodRequest,
175
) error {
10✔
176
        perm := uint32(req.Mode) //nolint:gosec // validateRequest guarantees 0..0o777 range
10✔
177

10✔
178
        for _, item := range req.Items {
23✔
179
                if err := validatePath(item.Path); err != nil {
14✔
180
                        return api.WrapHTTPError(err, http.StatusBadRequest)
1✔
181
                }
1✔
182

183
                fullPath := filepath.Join(node.WorkPath, serverDir, item.Path)
12✔
184

12✔
185
                err := h.daemonFiles.Chmod(ctx, node, fullPath, perm)
12✔
186
                if err != nil {
14✔
187
                        return errors.WithMessage(err, "failed to change file permissions")
2✔
188
                }
2✔
189
        }
190

191
        return nil
7✔
192
}
193

194
func validatePath(path string) error {
23✔
195
        if strings.Contains(path, "..") {
28✔
196
                return errors.New("path contains invalid directory traversal")
5✔
197
        }
5✔
198

199
        cleanPath := filepath.Clean(path)
18✔
200
        if strings.HasPrefix(cleanPath, "..") {
18✔
201
                return errors.New("path attempts to escape base directory")
×
202
        }
×
203

204
        return nil
18✔
205
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc