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.
This commit is contained in:
2026-09-25 21:33:46 +05:30
parent 1723ab53b8
commit faf5b27328
49 changed files with 6795 additions and 747 deletions

233
legacy/go/auth/pam.go Normal file
View File

@@ -0,0 +1,233 @@
// auth — PAM authentication with CGO bindings.
//
// Uses runtime.LockOSThread to prevent goroutine migration during
// PAM transactions, as PAM modules rely on thread-local storage.
//
// Mirrors vendor/src/auth/pam.rs and vendor/src/auth/mod.rs.
package auth
/*
#cgo LDFLAGS: -lpam
#include <security/pam_appl.h>
#include <stdlib.h>
#include <stdint.h>
// Match the //export-generated signature.
extern int latchd_pam_conv(int, struct pam_message **,
struct pam_response **, void *);
// Store a uintptr_t handle in C heap so the Go GC never sees a Go pointer.
static void *storeHandle(uintptr_t h) {
uintptr_t *p = malloc(sizeof(uintptr_t));
if (p) *p = h;
return p;
}
static uintptr_t loadHandle(void *p) {
return *(uintptr_t *)p;
}
static struct pam_conv makePAMConv(void *data) {
struct pam_conv c;
c.conv = (int (*)(int, const struct pam_message **,
struct pam_response **, void *))latchd_pam_conv;
c.appdata_ptr = data;
return c;
}
*/
import "C"
import (
"fmt"
"os"
ouser "os/user"
"runtime"
"runtime/cgo"
"strings"
"unsafe"
)
type AuthError struct {
Kind int
Msg string
}
const (
ErrPAMService = iota
ErrAccountValidation
ErrHomeDirInvalid
ErrShellInvalid
ErrUsernameNotFound
ErrSessionOpen
)
func (e *AuthError) Error() string { return e.Msg }
type Credentials struct {
Username string
UID uint32
PrimaryGID uint32
AllGIDs []uint32
HomeDir string
Shell string
pamHandle *C.pam_handle_t
pamData unsafe.Pointer
pamCB cgo.Handle
}
//export latchd_pam_conv
func latchd_pam_conv(numMsg C.int, msg **C.struct_pam_message, resp **C.struct_pam_response, data unsafe.Pointer) C.int {
h := cgo.Handle(C.loadHandle(data))
pw := h.Value().(string)
r := (*C.struct_pam_response)(C.calloc(C.ulong(numMsg), C.sizeof_struct_pam_response))
if r == nil {
return C.PAM_BUF_ERR
}
*resp = r
msgSlice := unsafe.Slice(msg, int(numMsg))
respSlice := unsafe.Slice(r, int(numMsg))
for i := 0; i < int(numMsg); i++ {
switch msgSlice[i].msg_style {
case C.PAM_PROMPT_ECHO_OFF, C.PAM_PROMPT_ECHO_ON:
c := C.CString(pw)
if c == nil {
return C.PAM_BUF_ERR
}
respSlice[i].resp = c
respSlice[i].resp_retcode = 0
default:
respSlice[i].resp = nil
respSlice[i].resp_retcode = 0
}
}
return C.PAM_SUCCESS
}
func Validate(user, pass, service string) (*Credentials, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
cSvc := C.CString(service)
cUser := C.CString(user)
defer C.free(unsafe.Pointer(cSvc))
defer C.free(unsafe.Pointer(cUser))
h := cgo.NewHandle(pass)
cData := C.storeHandle(C.uintptr_t(h))
conv := C.makePAMConv(cData)
var pamh *C.pam_handle_t
ret := C.pam_start(cSvc, cUser, &conv, &pamh)
if ret != C.PAM_SUCCESS {
h.Delete()
C.free(cData)
return nil, &AuthError{ErrPAMService, fmt.Sprintf("pam_start: %s", pamErr(pamh, ret))}
}
ret = C.pam_authenticate(pamh, 0)
if ret != C.PAM_SUCCESS {
msg := fmt.Sprintf("auth failed: %s", pamErr(pamh, ret))
C.pam_end(pamh, ret)
h.Delete()
C.free(cData)
return nil, &AuthError{ErrAccountValidation, msg}
}
ret = C.pam_acct_mgmt(pamh, 0)
if ret != C.PAM_SUCCESS {
msg := fmt.Sprintf("account: %s", pamErr(pamh, ret))
C.pam_end(pamh, ret)
h.Delete()
C.free(cData)
return nil, &AuthError{ErrAccountValidation, msg}
}
u, err := ouser.Lookup(user)
if err != nil {
C.pam_end(pamh, ret)
h.Delete()
C.free(cData)
return nil, &AuthError{ErrUsernameNotFound, fmt.Sprintf("lookup %s: %v", user, err)}
}
uid := atou32(u.Uid)
gid := atou32(u.Gid)
gids := []uint32{gid}
if ids, err := u.GroupIds(); err == nil {
for _, s := range ids {
g := atou32(s)
if g != gid {
gids = append(gids, g)
}
}
}
return &Credentials{
Username: user,
UID: uid,
PrimaryGID: gid,
AllGIDs: gids,
HomeDir: u.HomeDir,
Shell: shellFromPasswd(uid),
pamHandle: pamh,
pamData: cData,
pamCB: h,
}, nil
}
func (c *Credentials) OpenSession() error {
ret := C.pam_open_session(c.pamHandle, 0)
if ret != C.PAM_SUCCESS {
return &AuthError{ErrSessionOpen, fmt.Sprintf("session: %s", pamErr(c.pamHandle, ret))}
}
return nil
}
func (c *Credentials) CloseSession() {
if c.pamHandle != nil {
C.pam_close_session(c.pamHandle, 0)
C.pam_end(c.pamHandle, 0)
c.pamHandle = nil
}
if c.pamData != nil {
C.free(c.pamData)
c.pamData = nil
}
c.pamCB.Delete()
}
func pamErr(h *C.pam_handle_t, e C.int) string {
return C.GoString(C.pam_strerror(h, e))
}
func atou32(s string) uint32 {
var n uint32
for _, c := range s {
if c < '0' || c > '9' {
break
}
n = n*10 + uint32(c-'0')
}
return n
}
func shellFromPasswd(uid uint32) string {
data, err := os.ReadFile("/etc/passwd")
if err != nil {
return "/bin/sh"
}
uidStr := fmt.Sprint(uid)
for _, line := range strings.Split(string(data), "\n") {
parts := strings.Split(line, ":")
if len(parts) >= 7 && parts[2] == uidStr && parts[6] != "" {
sh := parts[6]
if sh != "/sbin/nologin" && sh != "/usr/sbin/nologin" && sh != "/bin/false" {
return sh
}
}
}
return "/bin/sh"
}