Files
latchd/legacy/go/auth/faillock.go
Zane Walker faf5b27328 feat: ship Rust login manager and GitLab release pipeline
Replace the legacy Go implementation as the production path, quarantine the reference tree, add deterministic release packaging and version metadata, and run verification through GitLab CI.
2026-09-25 21:33:46 +05:30

82 lines
1.7 KiB
Go

// auth — faillock integration.
//
// Reads /etc/security/faillock.conf for deny count, executes
// faillock --user to get tally, supports --reset via admin bypass.
package auth
import (
"bytes"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
)
type FaillockStatus struct {
Enabled bool
Failures int
MaxTries int
Remaining int
LockedOut bool
}
func GetFaillockStatus(user string) (FaillockStatus, error) {
s := FaillockStatus{}
data, err := os.ReadFile("/etc/security/faillock.conf")
if err != nil {
return s, nil
}
s.Enabled = true
s.MaxTries = 3
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "deny") {
parts := strings.Split(line, "=")
if len(parts) == 2 {
if n, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil {
s.MaxTries = n
}
}
}
}
var out bytes.Buffer
cmd := exec.Command("faillock", "--user", user)
cmd.Stdout = &out
cmd.Stderr = &out
if err := cmd.Run(); err != nil {
return s, fmt.Errorf("faillock: %w", err)
}
s.Failures = countFaillockLines(out.String())
s.Remaining = s.MaxTries - s.Failures
if s.Remaining < 0 {
s.Remaining = 0
}
s.LockedOut = s.Remaining <= 0
return s, nil
}
func ResetFaillock(user string) error {
return exec.Command("faillock", "--user", user, "--reset").Run()
}
func countFaillockLines(out string) int {
n := 0
for _, line := range strings.Split(out, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "When") ||
strings.HasPrefix(line, "Source") || strings.HasPrefix(line, "Date") {
continue
}
if strings.Contains(line, ":") {
continue
}
n++
}
return n
}