Skip to content

Conversation

@techplayz32
Copy link
Member

@techplayz32 techplayz32 commented Oct 23, 2025

This update introduces a comprehensive user management system with permission checks, enhancing user authentication through a user store. It also includes Docker configuration for user directory mounting and updates dependencies. Implements #8

Summary by CodeRabbit

  • New Features

    • Full user-management CLI (create/list/delete/show users, set permissions/admin, reset passwords, regenerate tokens).
    • Built-in persisted user store with password login, API tokens, per-repo permissions and global admin.
    • Server enforces repository permission checks using the persisted user store.
  • Chores

    • Container now persists user data to a local users directory.
  • Documentation

    • Tightened README quick-start spacing.

@techplayz32 techplayz32 linked an issue Oct 23, 2025 that may be closed by this pull request
@techplayz32 techplayz32 linked an issue Oct 23, 2025 that may be closed by this pull request
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 23, 2025

Walkthrough

Adds a persisted, thread-safe UserStore and integrates it into the Authenticator and HTTP server for repo permission checks; adds a Cobra-based CLI for user management; updates docker-compose to persist users; updates go.mod and tightens README spacing.

Changes

Cohort / File(s) Summary
Auth core & user store
internal/auth/users.go, internal/auth/auth.go
New persisted, thread-safe UserStore with bcrypt password hashes, tokens, per-repo permissions and global admin. Authenticator now holds userStore; User gains Username. NewAuthenticator(cfg, storagePath) signature added plus GetUserStore(), CheckPermission(...), RequirePermission(...) and internal token/auth adjustments.
CLI user management
cmd/workgit/user.go
New Cobra user command with subcommands (create, list, delete, set-permission, show, set-admin, reset-password, regenerate-token) implementing password prompts, JSON/human output, and UserStore interactions.
Server wiring & handler
internal/server/handler.go, internal/server/server.go
Server now initializes authenticator with cfg.Storage.Root, propagates initialization errors, enforces repo permissions via RequirePermission, NewGitHandler now returns (*GitHandler, error), and registerRoutes returns an error. path usages replaced by path/filepath.
Docker persistence
docker-compose.yml
Added host volume ./users:/var/lib/workgit/users to persist user data from the container.
Go module updates
go.mod
Adjusted Go version and updated indirect dependencies (added/updated golang.org/x/term, golang.org/x/crypto, golang.org/x/sys).
Docs formatting
README.md
Removed two stray blank lines around the Docker Quick Start section to tighten spacing.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as workgit CLI
    participant Auth as Authenticator
    participant US as UserStore
    participant FS as users.json

    Note over CLI,Auth: user management commands
    CLI->>Auth: user command (create/list/delete/...)
    Auth->>US: CreateUser / ListUsers / SetPermission / RegenerateToken
    US->>FS: load / atomic save
    US-->>Auth: UserInfo / result
    Auth-->>CLI: formatted output (JSON or human)
Loading
sequenceDiagram
    participant GitClient
    participant Server
    participant Auth as Authenticator
    participant US as UserStore

    GitClient->>Server: Git request (mTLS / Bearer / Basic)
    Server->>Auth: Authenticate(ctx, creds)
    alt mTLS
        Auth->>Auth: extract CN → Username
    else Bearer
        Auth->>US: GetUserByToken(token)
    else Basic
        Auth->>US: AuthenticateUser(username, password or token)
    end
    US-->>Auth: UserInfo (if found)
    Auth->>Server: attach Username to ctx
    Server->>Auth: RequirePermission(ctx, repo, service)
    Auth->>US: CheckAccess(username, repo, required)
    US-->>Auth: allowed / denied
    alt allowed
        Server-->>GitClient: proceed
    else denied
        Server-->>GitClient: 401/403
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibble hashes, tokens bright,
I hop through perms in moonlit night,
CLI carrots, JSON crumbs to store,
users snug upon the filesystem floor,
a tiny hop — new features take flight.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat(auth): implement user management and authentication enhancements" clearly and directly summarizes the main changes in this changeset. The PR introduces a comprehensive user management system (evidenced by the new cmd/workgit/user.go CLI module and internal/auth/users.go UserStore), enhances authentication with permission checking capabilities (internal/auth/auth.go modifications), and updates supporting infrastructure (docker-compose.yml, go.mod, and server handlers) to integrate these features. The title is specific and avoids generic language, using concrete terms like "user management" and "authentication enhancements" that accurately reflect the primary objectives of the changeset. A developer scanning the repository history would understand that this PR introduces user management and auth system improvements.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands and usage tips.

@techplayz32 techplayz32 requested a review from nixilum October 23, 2025 19:44
@techplayz32 techplayz32 self-assigned this Oct 23, 2025
@techplayz32 techplayz32 linked an issue Oct 23, 2025 that may be closed by this pull request
@techplayz32 techplayz32 added feature New feature or request priority: medium Should be fixed eventually status: needs-review Waiting for code review labels Oct 23, 2025
@techplayz32 techplayz32 linked an issue Oct 23, 2025 that may be closed by this pull request
@techplayz32 techplayz32 added this to the v0.1.0-alpha milestone Oct 23, 2025
@techplayz32 techplayz32 linked an issue Oct 23, 2025 that may be closed by this pull request
@techplayz32 techplayz32 linked an issue Oct 23, 2025 that may be closed by this pull request
@techplayz32 techplayz32 linked an issue Oct 23, 2025 that may be closed by this pull request
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/server/server.go (1)

78-93: Auth middleware is not applied to Git endpoints; the handler expects authenticated context but none is provided.

The GitHandler.ServeHTTP() method checks auth.UserFromContext(r.Context()) at line 52 of handler.go and returns 401 if the user is missing or anonymous. However, wrapHandler() in server.go applies only logging, metrics, rate-limiting, and request-ID middleware—no authentication middleware.

The authenticator is instantiated in NewGitHandler() but never used to wrap or protect the handlers. The context that ServeHTTP() depends on will never be populated, causing all authenticated git operations to fail with 401.

Apply auth.Middleware() in the middleware chain (e.g., add it to wrapHandler() before other middleware) to populate the user context before ServeHTTP() is invoked.

🧹 Nitpick comments (10)
internal/auth/auth.go (3)

88-90: Auth challenge header could include Bearer.

If you support tokens, advertise both: WWW-Authenticate: Basic realm="workgit", Bearer.


305-318: mTLS policy: RequestClientCert may be too lax.

If you intend to enforce client certs when a CA is configured, consider tls.RequireAndVerifyClientCert. Current setting only requests the cert.


249-256: CheckPermission returns false when store is nil, but RequirePermission skips checks.

This is fine, but document it to avoid confusion for callers using CheckPermission directly.

internal/auth/users.go (3)

293-304: Avoid returning secrets in ListUsers.

ListUsers exposes PasswordHash and Token. Provide a sanitized variant for API/CLI output and use that downstream.

Example:

+type PublicUser struct {
+  Username    string                `json:"username"`
+  Email       string                `json:"email,omitempty"`
+  FullName    string                `json:"full_name,omitempty"`
+  Active      bool                  `json:"active"`
+  CreatedAt   time.Time             `json:"created_at"`
+  UpdatedAt   time.Time             `json:"updated_at"`
+  Permissions map[string]Permission `json:"permissions"`
+  GlobalAdmin bool                  `json:"global_admin"`
+}
+
+func (s *UserStore) ListUsersPublic() []PublicUser {
+  s.mu.RLock()
+  defer s.mu.RUnlock()
+  out := make([]PublicUser, 0, len(s.users))
+  for _, u := range s.users {
+    out = append(out, PublicUser{
+      Username:    u.Username, Email: u.Email, FullName: u.FullName,
+      Active: u.Active, CreatedAt: u.CreatedAt, UpdatedAt: u.UpdatedAt,
+      Permissions: u.Permissions, GlobalAdmin: u.GlobalAdmin,
+    })
+  }
+  return out
+}

168-193: Consider token collision check on regenerate.

Very unlikely, but add a loop to avoid collisions with existing tokens in s.tokens.


333-357: Durability: optional fsync.

For stronger crash consistency, consider os.CreateTemp + file.Sync + Close + Rename. Current approach is usually fine.

cmd/workgit/user.go (4)

146-153: Stop hand‑rolling JSON; use encoding/json to avoid broken/unsafe output.

Manual fmt.Printf JSON will break on quotes and special chars; also inconsistent escaping.

Apply structured encoding. Example for Lines 146‑153:

-	if jsonOutput {
-		fmt.Printf(`{"status":"created","username":"%s","token":"%s"}%s`,
-			user.Username, user.Token, "\n")
-	} else {
+	if jsonOutput {
+		if err := printJSON(map[string]any{
+			"status":   "created",
+			"username": user.Username,
+			"token":    user.Token,
+		}); err != nil {
+			return err
+		}
+	} else {
 		fmt.Printf("User created: %s\n", user.Username)
 		fmt.Printf("API Token: %s\n", user.Token)
 		fmt.Println("\nKeep this token secure! It won't be shown again.")
 	}

Repeat similarly for Lines 171‑181, 231‑235, 264‑269, 292‑297, 382‑386, and 409‑414.

Outside selected ranges, add helper and import:

 import (
+	"encoding/json"
 	"fmt"
 	"os"
 	"strings"
-	"syscall"
 	"text/tabwriter"
@@
 )
+
+func printJSON(v any) error {
+	enc := json.NewEncoder(os.Stdout)
+	enc.SetEscapeHTML(true)
+	return enc.Encode(v)
+}

Also applies to: 171-181, 231-235, 264-269, 292-297, 382-386, 409-414


419-427: Prefer os.Stdin.Fd() over syscall.Stdin; consider wiping password bytes.

For better portability use os.Stdin.Fd(). Also, avoid keeping passwords as Go strings and zero the byte slice after use.

Apply:

-func promptPassword(prompt string) (string, error) {
+func promptPassword(prompt string) (string, error) {
 	fmt.Print(prompt)
-	password, err := term.ReadPassword(int(syscall.Stdin))
+	pwBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
 	fmt.Println()
 	if err != nil {
 		return "", err
 	}
-	return string(password), nil
+	// Convert then wipe
+	pw := string(pwBytes)
+	for i := range pwBytes {
+		pwBytes[i] = 0
+	}
+	return pw, nil
 }

Also remove the unused syscall import in the file’s imports.


212-220: Safer confirmation input parsing.

fmt.Scanln leaves trailing input and ignores lowercase “yes”. Trim and normalize.

Apply:

-	fmt.Printf("Delete user '%s'? [y/N]: ", username)
-	var confirm string
-	fmt.Scanln(&confirm)
-	if confirm != "y" && confirm != "Y" {
+	fmt.Printf("Delete user '%s'? [y/N]: ", username)
+	var confirm string
+	if _, err := fmt.Scanln(&confirm); err != nil {
+		fmt.Println("Cancelled")
+		return nil
+	}
+	if strings.ToLower(strings.TrimSpace(confirm)) != "y" {
 		fmt.Println("Cancelled")
 		return nil
 	}

130-134: Centralize user store via Authenticator instead of constructing stores ad‑hoc.

Instantiate auth once per command and use GetUserStore to keep behavior consistent with server and future auth policies.

-userStore, err := auth.NewUserStore(cfg.Storage.Root)
+authn, err := auth.NewAuthenticator(cfg.Auth, cfg.Storage.Root)
 if err != nil {
-	return fmt.Errorf("initialize user store: %w", err)
+	return fmt.Errorf("auth init: %w", err)
 }
+userStore := authn.GetUserStore()

Apply similarly at: 164-168, 222-226, 255-258, 282-286, 329-333, 373-376, 399-402

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e98ded5 and f9ddfdc.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (8)
  • README.md (0 hunks)
  • cmd/workgit/user.go (1 hunks)
  • docker-compose.yml (1 hunks)
  • go.mod (2 hunks)
  • internal/auth/auth.go (7 hunks)
  • internal/auth/users.go (1 hunks)
  • internal/server/handler.go (1 hunks)
  • internal/server/server.go (1 hunks)
💤 Files with no reviewable changes (1)
  • README.md
🧰 Additional context used
🧬 Code graph analysis (5)
internal/server/handler.go (1)
internal/auth/auth.go (1)
  • NewAuthenticator (45-79)
internal/server/server.go (1)
internal/auth/auth.go (1)
  • NewAuthenticator (45-79)
internal/auth/auth.go (2)
internal/config/config.go (1)
  • AuthConfig (53-57)
internal/auth/users.go (5)
  • UserStore (51-56)
  • NewUserStore (59-71)
  • Permission (27-27)
  • PermissionRead (31-31)
  • PermissionWrite (32-32)
cmd/workgit/user.go (1)
internal/auth/users.go (2)
  • NewUserStore (59-71)
  • Permission (27-27)
internal/auth/users.go (1)
internal/auth/auth.go (1)
  • ErrUnauthorized (21-21)
🔇 Additional comments (4)
go.mod (3)

8-8: No issues found - versions are valid and build successfully.

The script execution confirms that golang.org/x/term v0.36.0 and golang.org/x/sys v0.37.0 are legitimate versions that exist in the Go module ecosystem. Both go mod tidy and go list -m -json completed successfully without errors, demonstrating that the versions resolve correctly and the project builds without dependency issues.


3-3: ****

The go 1.25.1 directive is valid. The go directive accepts valid Go version strings including patch versions such as go 1.20.3 and go 1.23.0. No changes are needed; the current syntax is correct per Go module specifications.

Likely an incorrect or invalid review comment.


23-23: The review comment is incorrect.

go.yaml.in/yaml/v2 is a valid Go module import path and a maintained upstream replacement for gopkg.in/yaml.v2. The script output confirms successful module resolution with valid checksums in go.sum. No changes are needed.

Likely an incorrect or invalid review comment.

cmd/workgit/user.go (1)

250-254: No issues found. The "none" permission is correctly handled.

The implementation already treats "none" as a delete operation: when perm == PermissionNone, the code executes delete(user.Permissions, repo) (line 210), removing the repository entry entirely rather than storing it as a literal value. This correctly removes access as intended.

Copy link
Collaborator

@nixilum nixilum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@techplayz32 Could you please look at my and CodeRabbit's comments? There are some changes are required to be made.

@nixilum
Copy link
Collaborator

nixilum commented Oct 23, 2025

@coderabbitai help

@coderabbitai

This comment was marked as off-topic.

Copy link
Collaborator

@nixilum nixilum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
go.mod (1)

3-3: Fix go directive: patch versions are invalid in go.mod.

Use only major.minor, e.g., go 1.25 (or your actual supported Go version).

-go 1.25.1
+go 1.25
internal/server/handler.go (1)

56-75: Enforce repository permissions via the authenticator.

Apply per-request permission checks; Unauthorized -> 401, insufficient -> 403.

   // Check authentication
   requireAuth := auth.RequireAuth(r.Method, service, h.cfg.Auth.AnonymousRead)
   if requireAuth {
     user, ok := auth.UserFromContext(r.Context())
     if !ok || user.Anonymous {
       w.Header().Set("WWW-Authenticate", `Basic realm="workgit"`)
       http.Error(w, "Unauthorized", http.StatusUnauthorized)
       return
     }
     // Audit log for pushes
     if service == "git-receive-pack" {
       h.logger.WithContext(r.Context()).Info("push attempt",
         "user", user.ID,
         "repo", repoName,
       )
     }
   }
+
+  // Enforce per-repo permissions (allows anonymous read when configured)
+  if err := h.auth.RequirePermission(r.Context(), repoName, service); err != nil {
+    if errors.Is(err, auth.ErrUnauthorized) {
+      w.Header().Set("WWW-Authenticate", `Basic realm="workgit"`)
+      http.Error(w, "Unauthorized", http.StatusUnauthorized)
+      return
+    }
+    http.Error(w, "Forbidden", http.StatusForbidden)
+    return
+  }
♻️ Duplicate comments (1)
internal/server/server.go (1)

38-41: Fix handler construction, wire auth middleware, and align users storage path.

  • Handle NewGitHandler error (signature returns (*GitHandler, error)).
  • Apply authentication middleware; otherwise auth.UserFromContext is never set and all auth-required requests fail.
  • Avoid passing <root>/users when NewAuthenticator already appends "users" (current code yields <root>/users/users). Pass the storage root instead.
@@
-import (
+import (
   "context"
   "crypto/tls"
   "errors"
   "fmt"
   "net/http"
   "os"
   "os/signal"
-  "path/filepath"
   "syscall"
   "time"
@@
-func New(cfg *config.Config, store storage.Store, logger *log.Logger) (*Server, error) {
-  usersDir := filepath.Join(cfg.Storage.Root, "users")
-  authenticator, err := auth.NewAuthenticator(cfg.Auth, usersDir)
+func New(cfg *config.Config, store storage.Store, logger *log.Logger) (*Server, error) {
+  authenticator, err := auth.NewAuthenticator(cfg.Auth, cfg.Storage.Root)
   if err != nil {
     return nil, fmt.Errorf("initialize authenticator: %w", err)
   }
@@
-  mux := http.NewServeMux()
-  s.registerRoutes(mux)
+  mux := http.NewServeMux()
+  if err := s.registerRoutes(mux); err != nil {
+    return nil, fmt.Errorf("register routes: %w", err)
+  }
@@
-func (s *Server) registerRoutes(mux *http.ServeMux) {
+func (s *Server) registerRoutes(mux *http.ServeMux) error {
@@
-  handler := NewGitHandler(s.store, s.logger, s.cfg)
+  handler, err := NewGitHandler(s.store, s.logger, s.cfg)
+  if err != nil {
+    return fmt.Errorf("new git handler: %w", err)
+  }
+  // Apply auth middleware (optional mode; handler decides if auth is required per request)
+  secured := s.auth.Middleware(false)(handler)
@@
-  mux.Handle("/", s.wrapHandler(handler))
+  mux.Handle("/", s.wrapHandler(secured))
+  return nil
 }

Previous review asked to plumb the error and update registerRoutes; this completes the change in this file too.

Also applies to: 79-96, 51-53, 6-15

🧹 Nitpick comments (4)
internal/auth/users.go (3)

168-193: Ensure regenerated tokens are unique; avoid accidental collision.

Low probability, but easy to harden by checking s.tokens.

-    // Generate new token
-    newToken := generateToken()
-    user.Token = newToken
+    // Generate new token (ensure uniqueness)
+    var newToken string
+    for {
+        cand := generateToken()
+        if _, taken := s.tokens[cand]; !taken {
+            newToken = cand
+            break
+        }
+    }
+    user.Token = newToken

293-304: Avoid returning password hashes and tokens from ListUsers.

Exposing PasswordHash and Token risks leakage via logs/CLI/HTTP. Return a sanitized view or deep copies with sensitive fields redacted.

Example new API (add alongside existing until callers migrate):

type PublicUser struct {
  Username    string                `json:"username"`
  Email       string                `json:"email,omitempty"`
  FullName    string                `json:"full_name,omitempty"`
  Active      bool                  `json:"active"`
  CreatedAt   time.Time             `json:"created_at"`
  UpdatedAt   time.Time             `json:"updated_at"`
  Permissions map[string]Permission `json:"permissions"`
  GlobalAdmin bool                  `json:"global_admin"`
}

func (s *UserStore) ListUsersPublic() []PublicUser {
  s.mu.RLock(); defer s.mu.RUnlock()
  out := make([]PublicUser, 0, len(s.users))
  for _, u := range s.users {
    out = append(out, PublicUser{
      Username: u.Username, Email: u.Email, FullName: u.FullName,
      Active: u.Active, CreatedAt: u.CreatedAt, UpdatedAt: u.UpdatedAt,
      Permissions: map[string]Permission(u.Permissions), GlobalAdmin: u.GlobalAdmin,
    })
  }
  return out
}

105-116: Return copies to avoid external mutation of internal state.

GetUser/GetPermission expose internal pointers/state. Consider copying UserInfo and Permissions map before returning to prevent unsynchronized mutation.

Also applies to: 221-243

internal/server/handler.go (1)

203-209: Use filepath.Base for filesystem paths.

path.Base is for slash-separated paths; prefer filepath.Base for OS paths.

-    unlock, lockErr := h.store.Lock(ctx, path.Base(repoPath))
+    unlock, lockErr := h.store.Lock(ctx, filepath.Base(repoPath))
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9ddfdc and 9a3ab03.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • go.mod (2 hunks)
  • internal/auth/auth.go (8 hunks)
  • internal/auth/users.go (1 hunks)
  • internal/server/handler.go (2 hunks)
  • internal/server/server.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
internal/server/handler.go (2)
internal/config/config.go (1)
  • Config (14-21)
internal/auth/auth.go (1)
  • NewAuthenticator (49-86)
internal/server/server.go (1)
internal/auth/auth.go (1)
  • NewAuthenticator (49-86)
internal/auth/users.go (1)
internal/auth/auth.go (1)
  • ErrUnauthorized (22-22)
internal/auth/auth.go (2)
internal/config/config.go (1)
  • AuthConfig (53-57)
internal/auth/users.go (5)
  • UserStore (51-56)
  • NewUserStore (59-71)
  • Permission (27-27)
  • PermissionRead (31-31)
  • PermissionWrite (32-32)
🔇 Additional comments (1)
go.mod (1)

23-23: Incorrect review comment: go.yaml.in/yaml/v2 is actually the canonical, maintained module path.

The canonical path is go.yaml.in/yaml/v2 (or /v3 for v3); gopkg.in/yaml.v2/v3 are the older endpoints for historical compatibility. The entry in your go.mod is correct and should not be removed or changed.

Likely an incorrect or invalid review comment.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
internal/auth/users.go (3)

82-85: Critical: This code will not compile.

hashPassword at lines 368-371 returns only string, but this call expects (string, error). The function signature must be updated to return an error.

This relates to past review feedback that has been partially addressed. Apply this fix:

-func hashPassword(password string) string {
-	h, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
-	return string(h)
-}
+func hashPassword(password string) (string, error) {
+	h, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+	if err != nil {
+		return "", fmt.Errorf("hash password: %w", err)
+	}
+	return string(h), nil
+}

166-167: Handle hashing errors and use UTC timestamps consistently.

Once hashPassword is updated to return errors (see previous comment), this call site must handle them. Additionally, use .UTC() for consistency with user creation.

Apply this diff after fixing hashPassword:

-	user.PasswordHash = hashPassword(newPassword)
-	user.UpdatedAt = time.Now()
+	hashed, err := hashPassword(newPassword)
+	if err != nil {
+		return fmt.Errorf("update password: %w", err)
+	}
+	user.PasswordHash = hashed
+	user.UpdatedAt = time.Now().UTC()

368-371: Critical: bcrypt errors are still being ignored.

Despite past review comments marking this as "Fixed in the next push," the error from bcrypt.GenerateFromPassword is still being ignored. This will cause silent failures when hashing fails.

This is the same issue flagged in previous reviews. Apply this diff:

-func hashPassword(password string) string {
-	h, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
-	return string(h)
-}
+func hashPassword(password string) (string, error) {
+	h, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+	if err != nil {
+		return "", fmt.Errorf("hash password: %w", err)
+	}
+	return string(h), nil
+}

Then update all call sites (lines 82, 166) to handle the returned error.

🧹 Nitpick comments (1)
internal/auth/users.go (1)

377-385: Token generation is much improved, but consider the fallback.

The error handling for rand.Read and use of RawURLEncoding address previous feedback well. However, the time-based fallback is cryptographically weak and produces a much shorter token.

Consider panicking or returning an error instead of falling back to weak entropy:

 func generateToken() string {
 	b := make([]byte, 32)
 	if _, err := rand.Read(b); err != nil {
-		// extremely unlikely; fall back to time-based entropy
-		ts := time.Now().UnixNano()
-		return base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%d", ts)))
+		// This should never happen in practice; if it does, the system
+		// is critically compromised and we should not silently degrade.
+		panic(fmt.Sprintf("crypto/rand failed: %v", err))
 	}
 	return base64.RawURLEncoding.EncodeToString(b)
 }

Alternatively, change the signature to return (string, error) and propagate the failure.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a3ab03 and 715944a.

📒 Files selected for processing (1)
  • internal/auth/users.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/auth/users.go (1)
internal/auth/auth.go (1)
  • ErrUnauthorized (22-22)
🔇 Additional comments (3)
internal/auth/users.go (3)

135-154: Authentication logic is sound.

The function correctly checks if the user exists, is active, and verifies the password. The use of ErrUnauthorized from auth.go is appropriate.


336-364: Excellent atomic write implementation.

The use of a temporary file followed by atomic rename prevents corruption if the process crashes during save. File and directory permissions (0600/0700) appropriately restrict access.


387-405: Permission helpers are well-designed.

The validation and hierarchical permission checking are implemented correctly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
internal/auth/users.go (3)

308-318: Don’t return internal pointers or secrets from ListUsers.

Returning live pointers enables external mutation and leaks PasswordHash and Token. Provide a redacted copy.

Option A (non-breaking): add a redacted variant and use it in CLI/UI.

// ListUsersRedacted returns safe copies without secrets.
func (s *UserStore) ListUsersRedacted() []*UserInfo {
    s.mu.RLock()
    defer s.mu.RUnlock()
    out := make([]*UserInfo, 0, len(s.users))
    for _, u := range s.users {
        c := *u
        c.PasswordHash = ""
        c.Token = ""
        out = append(out, &c)
    }
    return out
}

Option B (breaking): change ListUsers to redact and deep-copy (preferred long term).


358-371: Durability: fsync temp file before atomic rename.

Current flow writes then renames without syncing; a crash can lose updates. Sync the temp file first.

-    tmpFile := s.filePath + ".tmp"
-    if err := os.WriteFile(tmpFile, data, 0600); err != nil {
-        return fmt.Errorf("write temp file: %w", err)
-    }
-
-    if err := os.Rename(tmpFile, s.filePath); err != nil {
-        return fmt.Errorf("rename file: %w", err)
-    }
+    dir := filepath.Dir(s.filePath)
+    f, err := os.CreateTemp(dir, "users-*.tmp")
+    if err != nil {
+        return fmt.Errorf("create temp file: %w", err)
+    }
+    tmpFile := f.Name()
+    if _, err := f.Write(data); err != nil {
+        f.Close()
+        _ = os.Remove(tmpFile)
+        return fmt.Errorf("write temp file: %w", err)
+    }
+    if err := f.Sync(); err != nil {
+        f.Close()
+        _ = os.Remove(tmpFile)
+        return fmt.Errorf("sync temp file: %w", err)
+    }
+    if err := f.Close(); err != nil {
+        _ = os.Remove(tmpFile)
+        return fmt.Errorf("close temp file: %w", err)
+    }
+    if err := os.Rename(tmpFile, s.filePath); err != nil {
+        _ = os.Remove(tmpFile)
+        return fmt.Errorf("rename file: %w", err)
+    }

409-417: Tiny perf nit: avoid re-allocating the levels map per call.

Make levels a package-level var or use a switch.

internal/server/handler.go (1)

33-45: Minor: pass storage root and let NewAuthenticator normalize users dir.

Reduce coupling by passing the root; NewAuthenticator already appends “users” when needed.

-func NewGitHandler(store storage.Store, logger *log.Logger, cfg *config.Config) (*GitHandler, error) {
-    usersDir := filepath.Join(cfg.Storage.Root, "users")
-    authenticator, err := auth.NewAuthenticator(cfg.Auth, usersDir)
+func NewGitHandler(store storage.Store, logger *log.Logger, cfg *config.Config) (*GitHandler, error) {
+    authenticator, err := auth.NewAuthenticator(cfg.Auth, cfg.Storage.Root)
     if err != nil {
         return nil, fmt.Errorf("auth init: %w", err)
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 715944a and 8d8a88f.

📒 Files selected for processing (5)
  • go.mod (2 hunks)
  • internal/auth/auth.go (8 hunks)
  • internal/auth/users.go (1 hunks)
  • internal/server/handler.go (4 hunks)
  • internal/server/server.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/server/server.go
🧰 Additional context used
🧬 Code graph analysis (3)
internal/server/handler.go (3)
internal/storage/filesystem.go (1)
  • Store (27-45)
internal/config/config.go (1)
  • Config (14-21)
internal/auth/auth.go (2)
  • NewAuthenticator (49-89)
  • ErrUnauthorized (22-22)
internal/auth/auth.go (2)
internal/config/config.go (1)
  • AuthConfig (53-57)
internal/auth/users.go (5)
  • UserStore (51-56)
  • NewUserStore (59-71)
  • Permission (27-27)
  • PermissionRead (31-31)
  • PermissionWrite (32-32)
internal/auth/users.go (1)
internal/auth/auth.go (1)
  • ErrUnauthorized (22-22)
🔇 Additional comments (3)
go.mod (2)

8-8: Dependencies verified — both actively used.

x/term is correctly imported in cmd/workgit/user.go for CLI operations, and bcrypt functions are actively used in internal/auth/users.go for password hashing. Dependency graph is clean.


3-3: go.mod file is correct and all changes are verified.

Go 1.25 was released on August 12, 2025, confirming the go 1.25 directive is valid. All declared dependencies (x/term, x/crypto, x/sys) are actively used in the codebase:

  • x/term: cmd/workgit/user.go
  • x/crypto: internal/auth/users.go (bcrypt)
  • x/sys: transitive dependency

Note: The repository contains no CI configuration files, so the CI verification request cannot be fulfilled. If CI tooling is managed externally, ensure it's configured separately.

internal/server/handler.go (1)

214-214: No action required.

The lock key is correct. Since repoPath = filepath.Join(s.root, repoName) and validateRepoName prevents path separators (/, \), filepath.Base(repoPath) always equals repoName. The Lock implementation stores locks by this name in a map, which is exactly the repo identifier. The code is consistent and correct.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/server/server.go (2)

36-49: Propagate Git handler init errors; don’t continue with a nil handler.

registerRoutes creates an error with fmt.Errorf but ignores it, and New() ignores route setup failures. Fail fast and bubble the error.

Apply:

 func New(cfg *config.Config, store storage.Store, logger *log.Logger) (*Server, error) {
-	authenticator, err := auth.NewAuthenticator(cfg.Auth, cfg.Storage.Root)
+	authenticator, err := auth.NewAuthenticator(cfg.Auth, cfg.Storage.Root)
 	if err != nil {
 		return nil, fmt.Errorf("initialize authenticator: %w", err)
 	}
@@
-	mux := http.NewServeMux()
-	s.registerRoutes(mux)
+	mux := http.NewServeMux()
+	if err := s.registerRoutes(mux); err != nil {
+		return nil, fmt.Errorf("register routes: %w", err)
+	}
@@
-func (s *Server) registerRoutes(mux *http.ServeMux) {
+func (s *Server) registerRoutes(mux *http.ServeMux) error {
@@
-	handler, err := NewGitHandler(s.store, s.logger, s.cfg)
-	if err != nil {
-		fmt.Errorf("failed to create Git handler: %w", err)
-		return
-	}
+	handler, err := NewGitHandler(s.store, s.logger, s.cfg)
+	if err != nil {
+		return fmt.Errorf("create Git handler: %w", err)
+	}
@@
-	mux.Handle("/", s.wrapHandler(handler))
+	mux.Handle("/", s.wrapHandler(handler))
+	return nil
 }

Also applies to: 77-97


99-107: Add authentication middleware to the chain.

Handlers rely on UserFromContext, but it’s never set. Without auth middleware, pushes always 401 and permission checks see anonymous users.

Apply:

 func (s *Server) wrapHandler(h http.Handler) http.Handler {
 	// Apply middleware in reverse order
-	h = s.loggingMiddleware(h)
-	h = s.metricsMiddleware(h)
-	h = s.rateLimitMiddleware(h)
-	h = s.requestIDMiddleware(h)
+	h = s.loggingMiddleware(h)
+	h = s.metricsMiddleware(h)
+	h = s.rateLimitMiddleware(h)
+	h = s.requestIDMiddleware(h)
+	h = s.auth.Middleware(false)(h)
 	return h
 }
♻️ Duplicate comments (1)
internal/auth/users.go (1)

140-151: Stop user enumeration in AuthenticateUser.

Distinct errors (“user not found”, “disabled”) leak account state. Return ErrUnauthorized uniformly.

Apply:

-	user, exists := s.users[username]
-	if !exists {
-		return nil, ErrUserNotFound
-	}
-
-	if !user.Active {
-		return nil, errors.New("user account is disabled")
-	}
-
-	if !verifyPassword(password, user.PasswordHash) {
-		return nil, ErrUnauthorized
-	}
+	user, exists := s.users[username]
+	if !exists || !user.Active || !verifyPassword(password, user.PasswordHash) {
+		// Optional: run a dummy bcrypt compare here to flatten timing.
+		return nil, ErrUnauthorized
+	}
🧹 Nitpick comments (3)
internal/auth/users.go (1)

50-56: Consider storing API tokens non-reversibly (hashed).

Storing raw tokens in users.json means a file read leaks all tokens. Prefer a lookup-safe scheme (e.g., keyed HMAC of token as map key plus bcrypt/argon2id for verification) to avoid plaintext token exposure. Can be a follow-up.

Also applies to: 122-133, 311-321

internal/server/handler.go (1)

213-219: Lock by repo name, not derived path (verify store expectation).

Store.Lock typically expects the repository name key. Passing filepath.Base(repoPath) likely works, but using repoName directly avoids coupling to filesystem layout. Consider changing handleService signature to accept repoName and pass that to Lock.

cmd/workgit/user.go (1)

158-202: Optional: prefer RFC3339 timestamps for JSON output.

Human-readable is fine; if you add a “--json” flag, consider RFC3339 for machine parsing consistency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d8a88f and c842fb9.

📒 Files selected for processing (5)
  • cmd/workgit/user.go (1 hunks)
  • internal/auth/auth.go (8 hunks)
  • internal/auth/users.go (1 hunks)
  • internal/server/handler.go (4 hunks)
  • internal/server/server.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
cmd/workgit/user.go (2)
internal/auth/users.go (2)
  • NewUserStore (59-71)
  • Permission (27-27)
internal/auth/auth.go (1)
  • NormalizeRepo (263-268)
internal/server/handler.go (3)
internal/storage/filesystem.go (1)
  • Store (27-45)
internal/config/config.go (1)
  • Config (14-21)
internal/auth/auth.go (2)
  • NewAuthenticator (49-89)
  • ErrUnauthorized (22-22)
internal/auth/auth.go (2)
internal/config/config.go (1)
  • AuthConfig (53-57)
internal/auth/users.go (5)
  • UserStore (51-56)
  • NewUserStore (59-71)
  • Permission (27-27)
  • PermissionRead (31-31)
  • PermissionWrite (32-32)
internal/server/server.go (2)
internal/auth/auth.go (1)
  • NewAuthenticator (49-89)
internal/server/handler.go (1)
  • NewGitHandler (33-44)
internal/auth/users.go (1)
internal/auth/auth.go (2)
  • ErrUnauthorized (22-22)
  • NormalizeRepo (263-268)
🔇 Additional comments (3)
internal/auth/auth.go (1)

174-179: Legacy token identity fix looks correct.

You now avoid trusting the Basic username for legacy tokens and use a safe shortTokenPrefix in both Basic and Bearer paths. Good hardening.

Also applies to: 204-207

internal/server/handler.go (1)

32-44: Constructor error handling LGTM.

NewGitHandler now returns (handler, error) and plumbs auth init errors correctly.

cmd/workgit/user.go (1)

240-274: Good: repo key normalization before storing permissions.

Using auth.NormalizeRepo prevents “repo” vs “repo.git” mismatches between CLI and HTTP checks.

- Add `path/filepath` import to handle path manipulation
- Modify user store initialization to use a dedicated "users" subdirectory
  for storage, ensuring consistent path structure
- Update token generation to guarantee uniqueness by:
  - Implementing retry logic (max 10 attempts)
  - Verifying token doesn't already exist in the store
  - Returning error if unique token cannot be generated
- Apply changes consistently across all user management functions:
  `Create`, `List`, `Delete`, `SetPermission`, `Show`, `SetAdmin`,
  `ResetPassword`, and `RegenerateToken`
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
internal/auth/users.go (1)

199-229: Incomplete rollback on save failure leaves user locked out.

If save() fails, the rollback sets user.Token = "" (line 227) but doesn't restore the original token value. The old token was deleted at line 200 and overwritten at line 220, leaving the user with no valid authentication token.

Apply this diff to preserve and restore the old token:

 	user, exists := s.users[username]
 	if !exists {
 		return "", ErrUserNotFound
 	}

-	// Remove old token
+	// Preserve old token for rollback
+	oldToken := user.Token
 	delete(s.tokens, user.Token)

 	// Generate new token (ensure uniqueness)
 	var newToken string
 	const maxRetries = 10
 	for i := 0; i < maxRetries; i++ {
 		cand, err := generateToken()
 		if err != nil {
 			return "", fmt.Errorf("regenerate token: %w", err)
 		}
 		if _, taken := s.tokens[cand]; !taken {
 			newToken = cand
 			break
 		}
 	}
 	if newToken == "" {
 		// Re-add old token if new one couldn't be generated
-		s.tokens[user.Token] = username
+		s.tokens[oldToken] = username
 		return "", errors.New("failed to generate unique token after retries")
 	}
 	user.Token = newToken
 	user.UpdatedAt = time.Now().UTC()
 	s.tokens[newToken] = username

 	if err := s.save(); err != nil {
-		// Rollback on save failure
+		// Rollback to old token on save failure
 		delete(s.tokens, newToken)
-		user.Token = "" // Will be invalid, but consistent
+		user.Token = oldToken
+		s.tokens[oldToken] = username
 		return "", fmt.Errorf("regenerate token: %w", err)
 	}

 	return newToken, nil
🧹 Nitpick comments (2)
internal/auth/users.go (2)

179-186: Consider rollback on save failure for consistency.

If save() fails, the in-memory password hash is updated but the disk state retains the old hash. After a restart, the old password would work, creating temporary inconsistency.

Apply this diff to rollback on save failure:

 	hashed, err := hashPassword(newPassword)
 	if err != nil {
 		return fmt.Errorf("update password: %w", err)
 	}
+	oldHash := user.PasswordHash
 	user.PasswordHash = hashed
 	user.UpdatedAt = time.Now().UTC()

-	return s.save()
+	if err := s.save(); err != nil {
+		user.PasswordHash = oldHash
+		return err
+	}
+	return nil

336-347: Consider returning copies to prevent external mutation.

ListUsers returns pointers to internal UserInfo structs, allowing callers to mutate user data outside the lock. If the API should prevent this, return copies instead.

If mutation prevention is desired:

 func (s *UserStore) ListUsers() []*UserInfo {
 	s.mu.RLock()
 	defer s.mu.RUnlock()

 	users := make([]*UserInfo, 0, len(s.users))
 	for _, user := range s.users {
-		users = append(users, user)
+		userCopy := *user
+		users = append(users, &userCopy)
 	}

 	return users
 }

Alternatively, document that callers must not modify returned structs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c842fb9 and 407fccc.

📒 Files selected for processing (3)
  • cmd/workgit/user.go (1 hunks)
  • internal/auth/users.go (1 hunks)
  • internal/server/server.go (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/workgit/user.go
🧰 Additional context used
🧬 Code graph analysis (2)
internal/server/server.go (2)
internal/auth/auth.go (1)
  • NewAuthenticator (49-89)
internal/server/handler.go (1)
  • NewGitHandler (33-44)
internal/auth/users.go (1)
internal/auth/auth.go (2)
  • ErrUnauthorized (22-22)
  • NormalizeRepo (263-268)
🔇 Additional comments (12)
internal/server/server.go (3)

50-52: LGTM: Proper error handling for route registration.

The error handling correctly propagates registration failures during server initialization.


80-98: LGTM: Error handling integrated into route registration.

The updated signature properly propagates Git handler initialization errors.


108-108: LGTM: Authentication middleware integrated.

The middleware is properly positioned in the chain.

internal/auth/users.go (9)

20-48: LGTM: Well-structured type definitions.

The Permission enum and UserInfo struct follow Go best practices with appropriate field tags and documentation.


58-73: LGTM: Path normalization and initialization.

The constructor correctly normalizes paths and gracefully handles the first-run case when users.json doesn't exist yet.


129-153: LGTM: Read operations are thread-safe and correct.

Both GetUser and GetUserByToken properly use read locks and handle missing entries.


234-286: LGTM: Permission management is well-implemented.

Repository name normalization, permission validation, and global admin checks are correctly handled.


288-318: LGTM: Admin operations use UTC timestamps correctly.

Both SetGlobalAdmin and SetActive follow the consistent pattern with proper timestamp handling.


349-353: LGTM: Access check correctly delegates to permission helpers.


355-403: LGTM: Persistence uses atomic writes and appropriate permissions.

The atomic write pattern (temp file + rename) prevents partial writes, and file permissions (0700/0600) properly restrict access.


412-430: LGTM: Security helpers correctly implement bcrypt and crypto/rand.

Password hashing and token generation now properly handle errors and use cryptographically secure implementations. All past security issues are resolved.


432-450: LGTM: Permission validation and comparison logic is correct.

The hierarchical permission model correctly validates and compares permission levels.

- Add rollback mechanism in CreateUser and DeleteUser when save fails
  to prevent data inconsistency after failed operations

- Implement constant-time password comparison in AuthenticateUser
  to prevent timing attacks by:
  * Always performing bcrypt comparison regardless of user existence
  * Using generated hash for non-existent/inactive users
  * Maintaining consistent execution time for all authentication attempts

- Improve error handling with proper fallback in bcrypt operations
  to ensure minimum security guarantees even in edge cases
@techplayz32 techplayz32 added the status: needs-review Waiting for code review label Oct 24, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
internal/auth/users.go (1)

157-190: Critical: Timing attack mitigation is flawed—generates hash on-the-fly for non-existent users.

The current approach generates a bcrypt hash on-the-fly for non-existent users (line 172), which takes ~100-200ms. Combined with the comparison, non-existent users take ~200-300ms total, while existing users only take ~100ms for comparison. This creates a timing side-channel that allows username enumeration—the exact vulnerability this code attempts to prevent.

Apply this diff to use a pre-computed dummy hash:

+// Dummy hash for timing attack mitigation (computed once at init)
+var dummyHash = "$2a$10$AAAAAAAAAAAAAAAAAAAAAO4Z8K6cF3YYPPVYJJJJJJJJJJJJJJJJJa"
+
 func (s *UserStore) AuthenticateUser(username, password string) (*UserInfo, error) {
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 
 	user, exists := s.users[username]
-
-	// To prevent timing attacks, always perform a bcrypt comparison
-	// Use the actual user's hash if they exist, otherwise generate one on-the-fly
-	var hashToCompare string
-	if exists && user.Active {
-		hashToCompare = user.PasswordHash
-	} else {
-		// Generate a bcrypt hash on-the-fly for non-existent/inactive users
-		// This ensures we can't be distinguished even with code inspection
-		hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
-		if err != nil {
-			// Fallback: at minimum still do some CPU work
-			_ = bcrypt.CompareHashAndPassword([]byte("$2a$10$AAAAAAAAAAAAAAAAAAAAAO4Z8K6cF3YYPPVYJJJJJJJJJJJJJJJJJa"), []byte(password))
-			return nil, ErrUnauthorized
-		}
-		hashToCompare = string(hash)
+	
+	// Use actual hash if user exists and is active, otherwise use dummy
+	hashToCompare := dummyHash
+	if exists && user.Active {
+		hashToCompare = user.PasswordHash
 	}
 
 	// Always perform the comparison
 	valid := verifyPassword(password, hashToCompare)
 
 	// Only return success if user exists, is active, and password matches
 	if exists && user.Active && valid {
 		return user, nil
 	}
 
 	return nil, ErrUnauthorized
 }

The pre-computed dummy hash ensures consistent ~100ms timing for all authentication attempts, regardless of username validity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 407fccc and b3bae48.

📒 Files selected for processing (1)
  • internal/auth/users.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/auth/users.go (1)
internal/auth/auth.go (2)
  • ErrUnauthorized (22-22)
  • NormalizeRepo (263-268)

The token regeneration process now properly preserves the old token
value throughout the operation, ensuring complete rollback capability
in case of failures. Previously, the rollback could leave the system
in an inconsistent state by clearing the user's token instead of
restoring the original one.

Key changes:
- Store old token value before modification
- Use stored old token for all rollback operations
- Ensure token map consistency during rollback
- Maintain proper token association during failure scenarios
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
internal/auth/users.go (1)

157-190: Simplify timing attack mitigation with a constant dummy hash.

The current approach generates a new bcrypt hash on-the-fly for non-existent/inactive users (line 172). While this achieves constant-time behavior, generating a hash on every failed authentication is expensive and more complex than necessary.

The standard pattern uses a pre-computed package-level dummy hash:

+// Dummy hash for timing attack mitigation (computed once)
+const dummyHash = "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
+
 func (s *UserStore) AuthenticateUser(username, password string) (*UserInfo, error) {
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 
 	user, exists := s.users[username]
-
-	// To prevent timing attacks, always perform a bcrypt comparison
-	// Use the actual user's hash if they exist, otherwise generate one on-the-fly
-	var hashToCompare string
-	if exists && user.Active {
-		hashToCompare = user.PasswordHash
-	} else {
-		// Generate a bcrypt hash on-the-fly for non-existent/inactive users
-		// This ensures we can't be distinguished even with code inspection
-		hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
-		if err != nil {
-			// Fallback: at minimum still do some CPU work
-			_ = bcrypt.CompareHashAndPassword([]byte("$2a$10$AAAAAAAAAAAAAAAAAAAAAO4Z8K6cF3YYPPVYJJJJJJJJJJJJJJJJJa"), []byte(password))
-			return nil, ErrUnauthorized
-		}
-		hashToCompare = string(hash)
-	}
-
-	// Always perform the comparison
-	valid := verifyPassword(password, hashToCompare)
-
-	// Only return success if user exists, is active, and password matches
-	if exists && user.Active && valid {
+	
+	// Use actual hash if user exists and is active, otherwise dummy hash
+	hashToCompare := dummyHash
+	if exists && user.Active {
+		hashToCompare = user.PasswordHash
+	}
+	
+	// Always perform bcrypt comparison for constant timing
+	valid := verifyPassword(password, hashToCompare)
+	
+	// Return success only if user exists, is active, and password valid
+	if exists && user.Active && valid {
 		return user, nil
 	}
-
+	
 	return nil, ErrUnauthorized
 }

This change:

  • Eliminates expensive hash generation on every failed auth
  • Simplifies the code significantly
  • Follows the industry-standard pattern recommended in OWASP guidance
  • Maintains constant-time behavior
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b3bae48 and 542ed22.

📒 Files selected for processing (1)
  • internal/auth/users.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/auth/users.go (1)
internal/auth/auth.go (2)
  • ErrUnauthorized (22-22)
  • NormalizeRepo (263-268)
🔇 Additional comments (4)
internal/auth/users.go (4)

58-129: Excellent implementation of user creation with proper safeguards.

The code correctly addresses all previously identified concerns:

  • Path normalization prevents inconsistent file locations
  • Token generation with uniqueness enforcement and bounded retries
  • Error handling for hash generation
  • UTC timestamps for portability
  • Proper rollback on save failure to maintain consistency

192-367: Well-implemented CRUD operations with proper safeguards.

All user management methods correctly implement:

  • UTC timestamps throughout for consistency
  • Proper rollback semantics in RegenerateToken and DeleteUser
  • Error propagation from hashPassword
  • Repository name normalization
  • Thread-safe operations with appropriate locking

The rollback logic in RegenerateToken (lines 250-256) properly restores the old token on save failure, addressing the previously identified consistency issue.


408-436: Secure persistence with atomic writes and proper permissions.

The save method implements best practices:

  • Creates directory with restrictive permissions (0700)
  • Uses atomic write pattern with temp file
  • Sets secure file permissions (0600)
  • Proper error handling and wrapping

This prevents partial writes and unauthorized access to user credentials.


445-463: Authentication helpers correctly implemented with proper error handling.

The helper functions properly address all previously identified security concerns:

  • hashPassword returns errors from bcrypt (lines 445-451)
  • generateToken returns errors from crypto/rand and uses RawURLEncoding (lines 457-463)
  • Token size of 32 bytes (256 bits) provides strong entropy

All critical security improvements from past reviews have been successfully implemented.

@techplayz32 techplayz32 merged commit 376499a into main Oct 24, 2025
1 check passed
@techplayz32
Copy link
Member Author

@coderabbitai generate docstrings

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 30, 2025

✅ Actions performed

Initiated docstring generation; will generate only if new commits exist.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 30, 2025

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #10

coderabbitai bot added a commit that referenced this pull request Oct 30, 2025
Docstrings generation was requested by @techplayz32.

* #9 (comment)

The following files were modified:

* `cmd/workgit/user.go`
* `internal/auth/auth.go`
* `internal/auth/users.go`
* `internal/server/handler.go`
* `internal/server/server.go`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request priority: medium Should be fixed eventually status: in-progress Currently being worked on status: needs-review Waiting for code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(users): implement users for authentication security: switch from SHA-256 hash to something like Argon2

3 participants