Files
uopi/internal/pamauth/pam.go
T
2026-06-23 17:59:40 +02:00

107 lines
3.7 KiB
Go

//go:build pam
// Package pamauth authenticates a username/password pair against the host's PAM
// stack (/etc/pam.d/<service>). It is the backend for uopi's built-in HTTP Basic
// authentication: because the uopi host is typically already an SSSD/LDAP client,
// validating through PAM reuses the exact same login path as `login`/`ssh`
// (pam_sss → the site directory) without uopi needing any directory schema.
//
// This file is the real implementation, compiled only with the `pam` build tag
// (which also requires cgo + libpam). The default fully-static CGO_ENABLED=0
// build uses stub.go instead, where Authenticate reports PAM is unavailable.
package pamauth
/*
#cgo LDFLAGS: -lpam
#include <security/pam_appl.h>
#include <stdlib.h>
#include <string.h>
// uopiPamConv answers every password-style PAM prompt with the password passed
// through appdata_ptr. Informational/error messages get a NULL response. The PAM
// library takes ownership of the returned responses and frees them.
static int uopiPamConv(int num_msg, const struct pam_message **msg,
struct pam_response **resp, void *appdata_ptr) {
if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) {
return PAM_CONV_ERR;
}
struct pam_response *r = calloc((size_t)num_msg, sizeof(struct pam_response));
if (r == NULL) {
return PAM_BUF_ERR;
}
for (int i = 0; i < num_msg; i++) {
int style = msg[i]->msg_style;
if (style == PAM_PROMPT_ECHO_OFF || style == PAM_PROMPT_ECHO_ON) {
r[i].resp = strdup((const char *)appdata_ptr);
if (r[i].resp == NULL) {
for (int j = 0; j < i; j++) {
free(r[j].resp);
}
free(r);
return PAM_BUF_ERR;
}
}
r[i].resp_retcode = 0;
}
*resp = r;
return PAM_SUCCESS;
}
// uopiPamAuth runs authentication + account management for service/user using
// pass. Returns PAM_SUCCESS or the failing PAM error code.
static int uopiPamAuth(const char *service, const char *user, char *pass) {
struct pam_conv conv;
conv.conv = uopiPamConv;
conv.appdata_ptr = (void *)pass;
pam_handle_t *pamh = NULL;
int ret = pam_start(service, user, &conv, &pamh);
if (ret != PAM_SUCCESS) {
return ret;
}
ret = pam_authenticate(pamh, PAM_DISALLOW_NULL_AUTHTOK);
if (ret == PAM_SUCCESS) {
ret = pam_acct_mgmt(pamh, PAM_DISALLOW_NULL_AUTHTOK);
}
pam_end(pamh, ret);
return ret;
}
// uopiStrerror maps a PAM error code to a human-readable string. Linux-PAM
// ignores the handle, so NULL is fine after pam_end.
static const char *uopiStrerror(int code) {
return pam_strerror(NULL, code);
}
*/
import "C"
import (
"errors"
"fmt"
"unsafe"
)
// Available reports whether this build includes PAM support. True here.
const Available = true
// ErrUnavailable is returned by Authenticate in builds without PAM support. It
// is declared in both build variants so callers can compare against it.
var ErrUnavailable = errors.New("pamauth: PAM support not compiled in")
// Authenticate verifies username/password against the named PAM service
// (/etc/pam.d/<service>). It returns nil on success or an error describing the
// PAM failure. It is safe for concurrent use.
func Authenticate(service, username, password string) error {
cService := C.CString(service)
cUser := C.CString(username)
cPass := C.CString(password)
defer C.free(unsafe.Pointer(cService))
defer C.free(unsafe.Pointer(cUser))
defer C.free(unsafe.Pointer(cPass))
if ret := C.uopiPamAuth(cService, cUser, cPass); ret != C.PAM_SUCCESS {
return fmt.Errorf("pam: %s", C.GoString(C.uopiStrerror(ret)))
}
return nil
}