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

umputun / stash / 20634302296

01 Jan 2026 06:59AM UTC coverage: 83.804% (+0.07%) from 83.735%
20634302296

Pull #53

github

umputun
fix(e2e): add positive confirmation after key creation in FilterToggle test

wait for key to appear in table after modal closes to prevent flaky test
Pull Request #53: feat(audit): add audit log web UI

229 of 261 new or added lines in 7 files covered. (87.74%)

3 existing lines in 2 files now uncovered.

3943 of 4705 relevant lines covered (83.8%)

79.83 hits per line

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

81.11
/app/server/web/handler.go
1
// Package web provides HTTP handlers for the web UI.
2
package web
3

4
import (
5
        "context"
6
        "embed"
7
        "encoding/base64"
8
        "fmt"
9
        "html/template"
10
        "io/fs"
11
        "maps"
12
        "net/http"
13
        "net/url"
14
        "sort"
15
        "strconv"
16
        "strings"
17
        "time"
18
        "unicode/utf8"
19

20
        log "github.com/go-pkgz/lgr"
21
        "github.com/go-pkgz/rest/realip"
22
        "github.com/go-pkgz/routegroup"
23

24
        "github.com/umputun/stash/app/enum"
25
        "github.com/umputun/stash/app/git"
26
        "github.com/umputun/stash/app/server/internal/cookie"
27
        "github.com/umputun/stash/app/store"
28
)
29

30
//go:generate moq -out mocks/kvstore.go -pkg mocks -skip-ensure -fmt goimports . KVStore
31
//go:generate moq -out mocks/validator.go -pkg mocks -skip-ensure -fmt goimports . Validator
32
//go:generate moq -out mocks/authprovider.go -pkg mocks -skip-ensure -fmt goimports . AuthProvider
33
//go:generate moq -out mocks/gitservice.go -pkg mocks -skip-ensure -fmt goimports . GitService
34
//go:generate moq -out mocks/auditlogger.go -pkg mocks -skip-ensure -fmt goimports . AuditLogger
35

36
//go:embed static
37
var staticFS embed.FS
38

39
//go:embed templates
40
var templatesFS embed.FS
41

42
// StaticFS returns the embedded static filesystem for external use.
43
func StaticFS() (fs.FS, error) {
×
44
        sub, err := fs.Sub(staticFS, "static")
×
45
        if err != nil {
×
46
                return nil, fmt.Errorf("failed to get static sub-filesystem: %w", err)
×
47
        }
×
48
        return sub, nil
×
49
}
50

51
// KVStore defines the interface for key-value storage operations.
52
type KVStore interface {
53
        GetWithFormat(ctx context.Context, key string) ([]byte, string, error)
54
        GetInfo(ctx context.Context, key string) (store.KeyInfo, error)
55
        Set(ctx context.Context, key string, value []byte, format string) (created bool, err error)
56
        SetWithVersion(ctx context.Context, key string, value []byte, format string, expectedVersion time.Time) error
57
        Delete(ctx context.Context, key string) error
58
        List(ctx context.Context, filter enum.SecretsFilter) ([]store.KeyInfo, error)
59
        SecretsEnabled() bool
60
}
61

62
// Validator defines the interface for format validation.
63
type Validator interface {
64
        Validate(format string, value []byte) error
65
        IsValidFormat(format string) bool
66
        SupportedFormats() []string
67
}
68

69
// AuthProvider defines the interface for authentication operations.
70
type AuthProvider interface {
71
        Enabled() bool
72
        GetSessionUser(ctx context.Context, token string) (string, bool)
73
        FilterUserKeys(username string, keys []string) []string
74
        CheckUserPermission(username, key string, write bool) bool
75
        UserCanWrite(username string) bool
76
        IsAdmin(username string) bool
77

78
        IsValidUser(username, password string) bool
79
        CreateSession(ctx context.Context, username string) (string, error)
80
        InvalidateSession(ctx context.Context, token string)
81
        LoginTTL() time.Duration
82
}
83

84
// GitService defines the interface for git operations.
85
type GitService interface {
86
        Commit(req git.CommitRequest) error
87
        Delete(key string, author git.Author) error
88
        History(key string, limit int) ([]git.HistoryEntry, error)
89
        GetRevision(key string, rev string) ([]byte, string, error)
90
}
91

92
// AuditLogger defines the interface for audit log storage.
93
type AuditLogger interface {
94
        LogAudit(ctx context.Context, entry store.AuditEntry) error
95
}
96

97
// Config holds web handler configuration.
98
type Config struct {
99
        BaseURL      string
100
        PageSize     int
101
        AuditEnabled bool
102
}
103

104
// Handler handles web UI requests.
105
type Handler struct {
106
        store        KVStore
107
        validator    Validator
108
        auth         AuthProvider
109
        highlighter  *Highlighter
110
        tmpl         *template.Template
111
        baseURL      string
112
        pageSize     int
113
        auditEnabled bool
114
        git          GitService
115
        audit        AuditLogger
116
}
117

118
// New creates a new web handler.
119
// auditLogger is optional, pass nil to disable audit logging in web handlers.
120
func New(st KVStore, auth AuthProvider, val Validator, gs GitService, auditLogger AuditLogger, cfg Config) (*Handler, error) {
91✔
121
        tmpl, err := parseTemplates()
91✔
122
        if err != nil {
91✔
123
                return nil, fmt.Errorf("failed to parse templates: %w", err)
×
124
        }
×
125

126
        return &Handler{
91✔
127
                store:        st,
91✔
128
                validator:    val,
91✔
129
                auth:         auth,
91✔
130
                highlighter:  NewHighlighter(),
91✔
131
                tmpl:         tmpl,
91✔
132
                baseURL:      cfg.BaseURL,
91✔
133
                pageSize:     cfg.PageSize,
91✔
134
                auditEnabled: cfg.AuditEnabled,
91✔
135
                git:          gs,
91✔
136
                audit:        auditLogger,
91✔
137
        }, nil
91✔
138
}
139

140
// Register registers web UI routes on the given router.
141
func (h *Handler) Register(r *routegroup.Bundle) {
×
142
        r.HandleFunc("GET /{$}", h.handleIndex)
×
143
        r.HandleFunc("GET /web/keys", h.handleKeyList)
×
144
        r.HandleFunc("GET /web/keys/new", h.handleKeyNew)
×
145
        r.HandleFunc("GET /web/keys/view/{key...}", h.handleKeyView)
×
146
        r.HandleFunc("GET /web/keys/edit/{key...}", h.handleKeyEdit)
×
147
        r.HandleFunc("GET /web/keys/history/{key...}", h.handleKeyHistory)
×
148
        r.HandleFunc("GET /web/keys/revision/{key...}", h.handleKeyRevision)
×
149
        r.HandleFunc("POST /web/keys/restore/{key...}", h.handleKeyRestore)
×
150
        r.HandleFunc("POST /web/keys", h.handleKeyCreate)
×
151
        r.HandleFunc("PUT /web/keys/{key...}", h.handleKeyUpdate)
×
152
        r.HandleFunc("DELETE /web/keys/{key...}", h.handleKeyDelete)
×
153
        r.HandleFunc("POST /web/theme", h.handleThemeToggle)
×
154
        r.HandleFunc("POST /web/view-mode", h.handleViewModeToggle)
×
155
        r.HandleFunc("POST /web/sort", h.handleSortToggle)
×
156
        r.HandleFunc("POST /web/secrets-filter", h.handleSecretsFilterToggle)
×
157
}
×
158

159
// RegisterAuth registers auth routes (login/logout) on the given router.
160
func (h *Handler) RegisterAuth(r *routegroup.Bundle) {
×
161
        r.HandleFunc("GET /login", h.handleLoginForm)
×
162
        r.HandleFunc("POST /logout", h.handleLogout)
×
163
}
×
164

165
// RegisterLogin registers the login POST handler with custom middleware.
166
func (h *Handler) RegisterLogin(r *routegroup.Bundle, middleware func(http.Handler) http.Handler) {
×
167
        r.Handle("POST /login", middleware(http.HandlerFunc(h.handleLogin)))
×
168
}
×
169

170
// templateFuncs returns custom template functions.
171
func templateFuncs() template.FuncMap {
91✔
172
        // sortModeLabel returns a human-readable label for the sort mode.
91✔
173
        sortModeLabel := func(mode enum.SortMode) string {
118✔
174
                s := mode.String()
27✔
175
                return strings.ToUpper(s[:1]) + s[1:]
27✔
176
        }
27✔
177

178
        funcs := template.FuncMap{
91✔
179
                "formatTime": func(t time.Time) string {
114✔
180
                        return t.Format("2006-01-02 15:04")
23✔
181
                },
23✔
182
                "formatSize": func(size int) string {
10✔
183
                        if size < 1024 {
20✔
184
                                return strconv.Itoa(size) + " B"
10✔
185
                        }
10✔
186
                        if size < 1024*1024 {
×
187
                                return strconv.FormatFloat(float64(size)/1024, 'f', 1, 64) + " KB"
×
188
                        }
×
189
                        return strconv.FormatFloat(float64(size)/(1024*1024), 'f', 1, 64) + " MB"
×
190
                },
191
                "urlEncode":     url.PathEscape,
192
                "sortModeLabel": sortModeLabel,
193
                "add":           func(a, b int) int { return a + b },
4✔
194
                "sub":           func(a, b int) int { return a - b },
2✔
195
        }
196

197
        // add audit template functions
198
        maps.Copy(funcs, AuditTemplateFuncs())
91✔
199

91✔
200
        return funcs
91✔
201
}
202

203
// parseTemplates parses all templates from embedded filesystem.
204
func parseTemplates() (*template.Template, error) {
91✔
205
        tmpl := template.New("").Funcs(templateFuncs())
91✔
206

91✔
207
        // parse base template
91✔
208
        baseContent, err := templatesFS.ReadFile("templates/base.html")
91✔
209
        if err != nil {
91✔
210
                return nil, fmt.Errorf("read base.html: %w", err)
×
211
        }
×
212
        tmpl, err = tmpl.Parse(string(baseContent))
91✔
213
        if err != nil {
91✔
214
                return nil, fmt.Errorf("parse base.html: %w", err)
×
215
        }
×
216

217
        // parse login template
218
        loginContent, err := templatesFS.ReadFile("templates/login.html")
91✔
219
        if err != nil {
91✔
220
                return nil, fmt.Errorf("read login.html: %w", err)
×
221
        }
×
222
        _, err = tmpl.New("login.html").Parse(string(loginContent))
91✔
223
        if err != nil {
91✔
224
                return nil, fmt.Errorf("parse login.html: %w", err)
×
225
        }
×
226

227
        // parse index template
228
        indexContent, err := templatesFS.ReadFile("templates/index.html")
91✔
229
        if err != nil {
91✔
230
                return nil, fmt.Errorf("read index.html: %w", err)
×
231
        }
×
232
        _, err = tmpl.New("index.html").Parse(string(indexContent))
91✔
233
        if err != nil {
91✔
234
                return nil, fmt.Errorf("parse index.html: %w", err)
×
235
        }
×
236

237
        // parse audit template
238
        auditContent, err := templatesFS.ReadFile("templates/audit.html")
91✔
239
        if err != nil {
91✔
NEW
240
                return nil, fmt.Errorf("read audit.html: %w", err)
×
NEW
241
        }
×
242
        _, err = tmpl.New("audit.html").Parse(string(auditContent))
91✔
243
        if err != nil {
91✔
NEW
244
                return nil, fmt.Errorf("parse audit.html: %w", err)
×
NEW
245
        }
×
246

247
        // parse partials
248
        partials := []string{"keys-table", "form", "view", "history", "revision", "error", "audit-table"}
91✔
249
        for _, name := range partials {
728✔
250
                content, readErr := templatesFS.ReadFile("templates/partials/" + name + ".html")
637✔
251
                if readErr != nil {
637✔
252
                        return nil, fmt.Errorf("read partial %s: %w", name, readErr)
×
253
                }
×
254
                _, parseErr := tmpl.New(name).Parse(string(content))
637✔
255
                if parseErr != nil {
637✔
256
                        return nil, fmt.Errorf("parse partial %s: %w", name, parseErr)
×
257
                }
×
258
        }
259

260
        return tmpl, nil
91✔
261
}
262

263
// keyWithPermission wraps KeyInfo with per-key write permission.
264
type keyWithPermission struct {
265
        store.KeyInfo
266
        CanWrite bool // user has write permission for this specific key
267
}
268

269
// conflictData holds conflict detection fields for optimistic locking.
270
type conflictData struct {
271
        UpdatedAt       int64  // unix timestamp when key was loaded (for optimistic locking)
272
        Conflict        bool   // true when a conflict was detected on save
273
        ServerValue     string // current server value (shown during conflict)
274
        ServerFormat    string // current server format (shown during conflict)
275
        ServerUpdatedAt int64  // server's updated_at timestamp (for retry after conflict)
276
}
277

278
// paginationData holds pagination state.
279
type paginationData struct {
280
        Page       int  // current page (1-based)
281
        TotalPages int  // total number of pages
282
        TotalKeys  int  // total keys after filtering (before pagination)
283
        HasPrev    bool // has previous page
284
        HasNext    bool // has next page
285
}
286

287
// secretsData holds secrets filter state.
288
type secretsData struct {
289
        SecretsFilter  enum.SecretsFilter // current filter mode (all/secrets/keys)
290
        SecretsEnabled bool               // secrets feature enabled
291
}
292

293
// historyData holds git history state.
294
type historyData struct {
295
        GitEnabled bool               // git integration enabled
296
        History    []git.HistoryEntry // commit history entries
297
        RevHash    string             // specific revision hash being viewed
298
}
299

300
// templateData holds data passed to templates.
301
type templateData struct {
302
        // key display fields
303
        Keys           []keyWithPermission
304
        Key            string
305
        Value          string
306
        HighlightedVal template.HTML // syntax-highlighted value for view modal
307
        Format         string        // format type (text, json, yaml, etc.)
308
        Formats        []string      // available format options
309
        IsBinary       bool
310
        IsNew          bool
311
        ZKEncrypted    bool // true if value is ZK-encrypted (client-side encryption)
312

313
        // display settings
314
        Theme    enum.Theme
315
        ViewMode enum.ViewMode
316
        SortMode enum.SortMode
317

318
        // form state
319
        Search   string
320
        Error    string
321
        CanForce bool // allow force submit despite error (for validation errors, not conflicts)
322

323
        // auth and permissions
324
        AuthEnabled  bool
325
        AuditEnabled bool // audit feature enabled (for showing audit link)
326
        BaseURL      string
327
        CanWrite     bool   // user has write permission (for showing edit controls)
328
        Username     string // current logged-in username
329
        IsAdmin      bool   // user has admin privileges
330

331
        // modal sizing
332
        ModalWidth     int
333
        TextareaHeight int
334

335
        // embedded groups
336
        conflictData
337
        paginationData
338
        secretsData
339
        historyData
340
}
341

342
// getTheme returns the current theme from cookie, defaulting to system.
343
func (h *Handler) getTheme(r *http.Request) enum.Theme {
47✔
344
        if c, err := r.Cookie("theme"); err == nil {
52✔
345
                if theme, err := enum.ParseTheme(c.Value); err == nil {
9✔
346
                        return theme
4✔
347
                }
4✔
348
        }
349
        return enum.ThemeSystem
43✔
350
}
351

352
// getViewMode returns the current view mode from cookie, defaulting to grid.
353
func (h *Handler) getViewMode(r *http.Request) enum.ViewMode {
31✔
354
        if c, err := r.Cookie("view_mode"); err == nil {
38✔
355
                if mode, err := enum.ParseViewMode(c.Value); err == nil {
13✔
356
                        return mode
6✔
357
                }
6✔
358
        }
359
        return enum.ViewModeGrid
25✔
360
}
361

362
// getSortMode returns the current sort mode from cookie, defaulting to updated.
363
func (h *Handler) getSortMode(r *http.Request) enum.SortMode {
34✔
364
        if c, err := r.Cookie("sort_mode"); err == nil {
47✔
365
                if mode, err := enum.ParseSortMode(c.Value); err == nil {
25✔
366
                        return mode
12✔
367
                }
12✔
368
        }
369
        return enum.SortModeUpdated
22✔
370
}
371

372
// getSecretsFilter returns the current secrets filter from cookie, defaulting to all.
373
func (h *Handler) getSecretsFilter(r *http.Request) enum.SecretsFilter {
29✔
374
        if c, err := r.Cookie("secrets_filter"); err == nil {
35✔
375
                if filter, err := enum.ParseSecretsFilter(c.Value); err == nil {
12✔
376
                        return filter
6✔
377
                }
6✔
378
        }
379
        return enum.SecretsFilterAll
23✔
380
}
381

382
// listParams holds view state parameters extracted from cookies and Set-Cookie headers.
383
type listParams struct {
384
        viewMode      enum.ViewMode
385
        sortMode      enum.SortMode
386
        secretsFilter enum.SecretsFilter
387
}
388

389
// getListParams extracts view/sort/filter state, checking Set-Cookie header for just-set values.
390
// this is needed when toggle handlers set cookie and then call handleKeyList in same request.
391
func (h *Handler) getListParams(w http.ResponseWriter, r *http.Request) listParams {
21✔
392
        p := listParams{
21✔
393
                viewMode:      h.getViewMode(r),
21✔
394
                sortMode:      h.getSortMode(r),
21✔
395
                secretsFilter: h.getSecretsFilter(r),
21✔
396
        }
21✔
397
        for _, c := range w.Header()["Set-Cookie"] {
32✔
398
                switch {
11✔
399
                case strings.Contains(c, "view_mode=cards"):
2✔
400
                        p.viewMode = enum.ViewModeCards
2✔
401
                case strings.Contains(c, "view_mode=grid"):
1✔
402
                        p.viewMode = enum.ViewModeGrid
1✔
403
                case strings.Contains(c, "sort_mode=key"):
1✔
404
                        p.sortMode = enum.SortModeKey
1✔
405
                case strings.Contains(c, "sort_mode=size"):
1✔
406
                        p.sortMode = enum.SortModeSize
1✔
407
                case strings.Contains(c, "sort_mode=created"):
1✔
408
                        p.sortMode = enum.SortModeCreated
1✔
409
                case strings.Contains(c, "sort_mode=updated"):
1✔
410
                        p.sortMode = enum.SortModeUpdated
1✔
411
                case strings.Contains(c, "secrets_filter=all"):
1✔
412
                        p.secretsFilter = enum.SecretsFilterAll
1✔
413
                case strings.Contains(c, "secrets_filter=secretsonly"):
2✔
414
                        p.secretsFilter = enum.SecretsFilterSecretsOnly
2✔
415
                case strings.Contains(c, "secrets_filter=keysonly"):
1✔
416
                        p.secretsFilter = enum.SecretsFilterKeysOnly
1✔
417
                }
418
        }
419
        return p
21✔
420
}
421

422
// url returns a URL path with the base URL prefix.
423
func (h *Handler) url(path string) string {
7✔
424
        return h.baseURL + path
7✔
425
}
7✔
426

427
// cookiePath returns the path for cookies (base URL with trailing slash or "/").
428
func (h *Handler) cookiePath() string {
20✔
429
        if h.baseURL == "" {
38✔
430
                return "/"
18✔
431
        }
18✔
432
        return h.baseURL + "/"
2✔
433
}
434

435
// getCurrentUser returns the username from the session cookie, or empty string if not logged in.
436
func (h *Handler) getCurrentUser(r *http.Request) string {
95✔
437
        for _, cookieName := range cookie.SessionCookieNames {
285✔
438
                if c, err := r.Cookie(cookieName); err == nil {
217✔
439
                        if username, ok := h.auth.GetSessionUser(r.Context(), c.Value); ok {
52✔
440
                                return username
25✔
441
                        }
25✔
442
                }
443
        }
444
        return ""
70✔
445
}
446

447
// getIdentityForLog returns identity string for audit logging.
448
// returns "user:xxx" for authenticated users, "anonymous" otherwise.
449
func (h *Handler) getIdentityForLog(r *http.Request) string {
11✔
450
        if username := h.getCurrentUser(r); username != "" {
16✔
451
                return "user:" + username
5✔
452
        }
5✔
453
        return "anonymous"
6✔
454
}
455

456
// getAuthor returns git author for the given username.
457
func (h *Handler) getAuthor(username string) git.Author {
3✔
458
        if username == "" {
4✔
459
                return git.DefaultAuthor()
1✔
460
        }
1✔
461
        return git.Author{Name: username, Email: username + "@stash"}
2✔
462
}
463

464
// sortByMode sorts a slice by the given mode using a key accessor.
465
func (h *Handler) sortByMode(keys []keyWithPermission, mode enum.SortMode) {
28✔
466
        switch mode {
28✔
467
        case enum.SortModeKey:
2✔
468
                sort.Slice(keys, func(i, j int) bool {
5✔
469
                        return strings.ToLower(keys[i].Key) < strings.ToLower(keys[j].Key)
3✔
470
                })
3✔
471
        case enum.SortModeSize:
2✔
472
                sort.Slice(keys, func(i, j int) bool {
5✔
473
                        return keys[i].Size > keys[j].Size // largest first
3✔
474
                })
3✔
475
        case enum.SortModeCreated:
2✔
476
                sort.Slice(keys, func(i, j int) bool {
5✔
477
                        return keys[i].CreatedAt.After(keys[j].CreatedAt) // newest first
3✔
478
                })
3✔
479
        default: // updated
22✔
480
                sort.Slice(keys, func(i, j int) bool {
44✔
481
                        return keys[i].UpdatedAt.After(keys[j].UpdatedAt) // newest first
22✔
482
                })
22✔
483
        }
484
}
485

486
// valueForDisplay converts a byte slice to a display string, detecting binary content.
487
func (h *Handler) valueForDisplay(value []byte) (string, bool) {
8✔
488
        if !utf8.Valid(value) {
9✔
489
                return base64.StdEncoding.EncodeToString(value), true
1✔
490
        }
1✔
491
        return string(value), false
7✔
492
}
493

494
// valueFromForm converts form input back to bytes, handling binary encoding.
495
func (h *Handler) valueFromForm(value string, isBinary bool) ([]byte, error) {
18✔
496
        if isBinary {
22✔
497
                decoded, err := base64.StdEncoding.DecodeString(value)
4✔
498
                if err != nil {
7✔
499
                        return nil, fmt.Errorf("decode base64: %w", err)
3✔
500
                }
3✔
501
                return decoded, nil
1✔
502
        }
503
        return []byte(value), nil
14✔
504
}
505

506
// filterBySearch filters keys by search term.
507
func (h *Handler) filterBySearch(keys []keyWithPermission, search string) []keyWithPermission {
25✔
508
        if search == "" {
46✔
509
                return keys
21✔
510
        }
21✔
511
        search = strings.ToLower(search)
4✔
512
        var filtered []keyWithPermission
4✔
513
        for _, k := range keys {
15✔
514
                if strings.Contains(strings.ToLower(k.Key), search) {
16✔
515
                        filtered = append(filtered, k)
5✔
516
                }
5✔
517
        }
518
        return filtered
4✔
519
}
520

521
// paginateResult holds the result of pagination.
522
type paginateResult struct {
523
        keys       []keyWithPermission
524
        page       int
525
        totalPages int
526
        hasPrev    bool
527
        hasNext    bool
528
}
529

530
// paginate applies pagination to a slice of keys and returns pagination info.
531
// page is 1-based, pageSize is the max keys per page.
532
func (h *Handler) paginate(keys []keyWithPermission, page, pageSize int) paginateResult {
34✔
533
        total := len(keys)
34✔
534
        if pageSize <= 0 {
57✔
535
                return paginateResult{keys: keys, page: 1, totalPages: 1}
23✔
536
        }
23✔
537

538
        totalPages := (total + pageSize - 1) / pageSize
11✔
539
        if totalPages == 0 {
12✔
540
                totalPages = 1
1✔
541
        }
1✔
542
        if page < 1 {
13✔
543
                page = 1
2✔
544
        }
2✔
545
        if page > totalPages {
12✔
546
                page = totalPages
1✔
547
        }
1✔
548

549
        start := (page - 1) * pageSize
11✔
550
        end := start + pageSize
11✔
551
        if start >= total {
12✔
552
                return paginateResult{page: page, totalPages: totalPages, hasPrev: page > 1}
1✔
553
        }
1✔
554
        if end > total {
13✔
555
                end = total
3✔
556
        }
3✔
557
        return paginateResult{keys: keys[start:end], page: page, totalPages: totalPages, hasPrev: page > 1, hasNext: page < totalPages}
10✔
558
}
559

560
// filterKeysByPermission filters keys based on user permissions and wraps with write permission info.
561
func (h *Handler) filterKeysByPermission(username string, keys []store.KeyInfo) []keyWithPermission {
27✔
562
        keyNames := make([]string, len(keys))
27✔
563
        for i, k := range keys {
61✔
564
                keyNames[i] = k.Key
34✔
565
        }
34✔
566
        allowedKeys := h.auth.FilterUserKeys(username, keyNames)
27✔
567
        allowedSet := make(map[string]bool, len(allowedKeys))
27✔
568
        for _, k := range allowedKeys {
57✔
569
                allowedSet[k] = true
30✔
570
        }
30✔
571
        var filtered []keyWithPermission
27✔
572
        for _, k := range keys {
61✔
573
                if allowedSet[k.Key] {
64✔
574
                        filtered = append(filtered, keyWithPermission{
30✔
575
                                KeyInfo:  k,
30✔
576
                                CanWrite: h.auth.CheckUserPermission(username, k.Key, true),
30✔
577
                        })
30✔
578
                }
30✔
579
        }
580
        return filtered
27✔
581
}
582

583
// logAudit logs an audit entry if audit logging is enabled.
584
func (h *Handler) logAudit(r *http.Request, key string, action enum.AuditAction, result enum.AuditResult, valueSize *int) {
10✔
585
        if h.audit == nil {
17✔
586
                return
7✔
587
        }
7✔
588

589
        username := h.getCurrentUser(r)
3✔
590
        actorType := enum.ActorTypePublic
3✔
591
        actor := "anonymous"
3✔
592
        if username != "" {
6✔
593
                actor = username
3✔
594
                actorType = enum.ActorTypeUser
3✔
595
        }
3✔
596

597
        ip, _ := realip.Get(r)
3✔
598

3✔
599
        entry := store.AuditEntry{
3✔
600
                Timestamp: time.Now(),
3✔
601
                Action:    action,
3✔
602
                Key:       key,
3✔
603
                Actor:     actor,
3✔
604
                ActorType: actorType,
3✔
605
                Result:    result,
3✔
606
                IP:        ip,
3✔
607
                UserAgent: r.UserAgent(),
3✔
608
                RequestID: r.Header.Get("X-Request-ID"),
3✔
609
                ValueSize: valueSize,
3✔
610
        }
3✔
611

3✔
612
        if err := h.audit.LogAudit(r.Context(), entry); err != nil {
3✔
NEW
613
                log.Printf("[WARN] failed to log audit entry for web operation: %v", err)
×
NEW
614
        }
×
615
}
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