Merge Go frontend (apiserver + xcecli) into meson build
This commit is contained in:
parent
43dc12a36c
commit
c9dc8c4676
205 changed files with 30793 additions and 328 deletions
84
src/frontend/apiserver/backend/auth/config_file.go
Normal file
84
src/frontend/apiserver/backend/auth/config_file.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// AuthFileConfig is the top-level structure of auth.yaml.
|
||||
type AuthFileConfig struct {
|
||||
Provider string `yaml:"provider"`
|
||||
ConfigFile *ConfigFileAuth `yaml:"config_file"`
|
||||
}
|
||||
|
||||
// ConfigFileAuth holds the user entries for config_file provider.
|
||||
type ConfigFileAuth struct {
|
||||
Users []UserEntry `yaml:"users"`
|
||||
}
|
||||
|
||||
// UserEntry represents a single user in auth.yaml.
|
||||
type UserEntry struct {
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Roles []string `yaml:"roles"`
|
||||
}
|
||||
|
||||
// ConfigFileProvider authenticates users against a YAML config file.
|
||||
type ConfigFileProvider struct {
|
||||
users map[string]UserEntry
|
||||
}
|
||||
|
||||
// NewConfigFileProvider creates a provider from raw YAML bytes.
|
||||
func NewConfigFileProvider(data []byte) (*ConfigFileProvider, error) {
|
||||
var cfg AuthFileConfig
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse auth config: %w", err)
|
||||
}
|
||||
|
||||
if len(cfg.ConfigFile.Users) == 0 {
|
||||
return nil, fmt.Errorf("no users defined in auth config")
|
||||
}
|
||||
|
||||
users := make(map[string]UserEntry, len(cfg.ConfigFile.Users))
|
||||
for _, u := range cfg.ConfigFile.Users {
|
||||
if u.Username == "" {
|
||||
return nil, fmt.Errorf("empty username in auth config")
|
||||
}
|
||||
users[u.Username] = u
|
||||
}
|
||||
|
||||
return &ConfigFileProvider{users: users}, nil
|
||||
}
|
||||
|
||||
// Authenticate checks username/password against the config file.
|
||||
func (p *ConfigFileProvider) Authenticate(username, password string) error {
|
||||
entry, ok := p.users[username]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid username or password")
|
||||
}
|
||||
if entry.Password != password {
|
||||
return fmt.Errorf("invalid username or password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserRoles returns roles defined in the config for the given user.
|
||||
func (p *ConfigFileProvider) GetUserRoles(username string) []string {
|
||||
if entry, ok := p.users[username]; ok {
|
||||
return entry.Roles
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the provider identifier.
|
||||
func (p *ConfigFileProvider) Name() string { return "config_file" }
|
||||
|
||||
// AllUsers returns all usernames known to this provider (implements UserRoleSeeder).
|
||||
func (p *ConfigFileProvider) AllUsers() []string {
|
||||
users := make([]string, 0, len(p.users))
|
||||
for u := range p.users {
|
||||
users = append(users, u)
|
||||
}
|
||||
return users
|
||||
}
|
||||
133
src/frontend/apiserver/backend/auth/config_file_test.go
Normal file
133
src/frontend/apiserver/backend/auth/config_file_test.go
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigFileProvider_Authenticate(t *testing.T) {
|
||||
yaml := `
|
||||
provider: config_file
|
||||
config_file:
|
||||
users:
|
||||
- username: admin
|
||||
password: secret
|
||||
roles: [cluster_admin]
|
||||
- username: viewer
|
||||
password: pass
|
||||
roles: [basic_user]
|
||||
`
|
||||
|
||||
provider, err := NewConfigFileProvider([]byte(yaml))
|
||||
if err != nil {
|
||||
t.Fatalf("NewConfigFileProvider: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
user string
|
||||
pass string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid admin", "admin", "secret", false},
|
||||
{"valid viewer", "viewer", "pass", false},
|
||||
{"wrong password", "admin", "wrong", true},
|
||||
{"unknown user", "nobody", "pass", true},
|
||||
{"empty user", "", "pass", true},
|
||||
{"empty password", "admin", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := provider.Authenticate(tt.user, tt.pass)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Authenticate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigFileProvider_GetUserRoles(t *testing.T) {
|
||||
yaml := `
|
||||
provider: config_file
|
||||
config_file:
|
||||
users:
|
||||
- username: admin
|
||||
password: admin
|
||||
roles: [cluster_admin, operator]
|
||||
- username: guest
|
||||
password: guest
|
||||
roles: []
|
||||
`
|
||||
|
||||
provider, err := NewConfigFileProvider([]byte(yaml))
|
||||
if err != nil {
|
||||
t.Fatalf("NewConfigFileProvider: %v", err)
|
||||
}
|
||||
|
||||
if roles := provider.GetUserRoles("admin"); len(roles) != 2 {
|
||||
t.Errorf("admin roles = %v, want 2", roles)
|
||||
}
|
||||
if roles := provider.GetUserRoles("guest"); len(roles) != 0 {
|
||||
t.Errorf("guest roles = %v, want empty", roles)
|
||||
}
|
||||
if roles := provider.GetUserRoles("unknown"); roles != nil {
|
||||
t.Errorf("unknown roles = %v, want nil", roles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigFileProvider_Name(t *testing.T) {
|
||||
p := &ConfigFileProvider{}
|
||||
if got := p.Name(); got != "config_file" {
|
||||
t.Errorf("Name() = %q, want %q", got, "config_file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfigFileProvider_Errors(t *testing.T) {
|
||||
t.Run("invalid yaml", func(t *testing.T) {
|
||||
_, err := NewConfigFileProvider([]byte("{{invalid"))
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid yaml")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no users", func(t *testing.T) {
|
||||
_, err := NewConfigFileProvider([]byte("provider: config_file\nconfig_file:\n users: []"))
|
||||
if err == nil {
|
||||
t.Error("expected error for empty users list")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty username", func(t *testing.T) {
|
||||
_, err := NewConfigFileProvider([]byte("provider: config_file\nconfig_file:\n users:\n - username: ''\n password: x"))
|
||||
if err == nil {
|
||||
t.Error("expected error for empty username")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPAMProvider_Name(t *testing.T) {
|
||||
p := &PAMProvider{}
|
||||
if got := p.Name(); got != "pam" {
|
||||
t.Errorf("Name() = %q, want %q", got, "pam")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLDAPProvider_Name(t *testing.T) {
|
||||
p := &LDAPProvider{}
|
||||
if got := p.Name(); got != "ldap" {
|
||||
t.Errorf("Name() = %q, want %q", got, "ldap")
|
||||
}
|
||||
if err := p.Authenticate("u", "p"); err == nil {
|
||||
t.Error("expected not-implemented error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOAuth2Provider_Name(t *testing.T) {
|
||||
p := &OAuth2Provider{}
|
||||
if got := p.Name(); got != "oauth2" {
|
||||
t.Errorf("Name() = %q, want %q", got, "oauth2")
|
||||
}
|
||||
if err := p.Authenticate("u", "p"); err == nil {
|
||||
t.Error("expected not-implemented error")
|
||||
}
|
||||
}
|
||||
44
src/frontend/apiserver/backend/auth/init.go
Normal file
44
src/frontend/apiserver/backend/auth/init.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var GlobalProvider AuthProvider
|
||||
|
||||
// InitAuthProvider reads auth.yaml and creates the appropriate provider.
|
||||
func InitAuthProvider(configPath string) error {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read auth config %s: %w", configPath, err)
|
||||
}
|
||||
|
||||
var cfg struct {
|
||||
Provider string `yaml:"provider"`
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return fmt.Errorf("failed to parse auth config: %w", err)
|
||||
}
|
||||
|
||||
switch cfg.Provider {
|
||||
case "config_file":
|
||||
provider, err := NewConfigFileProvider(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create config_file provider: %w", err)
|
||||
}
|
||||
GlobalProvider = provider
|
||||
case "pam":
|
||||
GlobalProvider = &PAMProvider{}
|
||||
case "ldap":
|
||||
GlobalProvider = &LDAPProvider{}
|
||||
case "oauth2":
|
||||
GlobalProvider = &OAuth2Provider{}
|
||||
default:
|
||||
return fmt.Errorf("unsupported auth provider: %s", cfg.Provider)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
17
src/frontend/apiserver/backend/auth/ldap.go
Normal file
17
src/frontend/apiserver/backend/auth/ldap.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package auth
|
||||
|
||||
import "fmt"
|
||||
|
||||
// LDAPProvider is a stub for future LDAP authentication.
|
||||
type LDAPProvider struct{}
|
||||
|
||||
// Authenticate verifies credentials via LDAP (not yet implemented).
|
||||
func (p *LDAPProvider) Authenticate(username, password string) error {
|
||||
return fmt.Errorf("LDAP authentication not yet implemented")
|
||||
}
|
||||
|
||||
// GetUserRoles returns nil (not yet implemented).
|
||||
func (p *LDAPProvider) GetUserRoles(username string) []string { return nil }
|
||||
|
||||
// Name returns the provider identifier.
|
||||
func (p *LDAPProvider) Name() string { return "ldap" }
|
||||
17
src/frontend/apiserver/backend/auth/oauth2.go
Normal file
17
src/frontend/apiserver/backend/auth/oauth2.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package auth
|
||||
|
||||
import "fmt"
|
||||
|
||||
// OAuth2Provider is a stub for future OAuth2/OIDC authentication.
|
||||
type OAuth2Provider struct{}
|
||||
|
||||
// Authenticate verifies credentials via OAuth2 (not yet implemented).
|
||||
func (p *OAuth2Provider) Authenticate(username, password string) error {
|
||||
return fmt.Errorf("OAuth2 authentication not yet implemented")
|
||||
}
|
||||
|
||||
// GetUserRoles returns nil (not yet implemented).
|
||||
func (p *OAuth2Provider) GetUserRoles(username string) []string { return nil }
|
||||
|
||||
// Name returns the provider identifier.
|
||||
func (p *OAuth2Provider) Name() string { return "oauth2" }
|
||||
11
src/frontend/apiserver/backend/auth/pam.go
Normal file
11
src/frontend/apiserver/backend/auth/pam.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package auth
|
||||
|
||||
// PAMProvider authenticates users via the system PAM stack.
|
||||
// Only available on Linux.
|
||||
type PAMProvider struct{}
|
||||
|
||||
// GetUserRoles returns nil — PAM users get roles from RBAC config.
|
||||
func (p *PAMProvider) GetUserRoles(username string) []string { return nil }
|
||||
|
||||
// Name returns the provider identifier.
|
||||
func (p *PAMProvider) Name() string { return "pam" }
|
||||
112
src/frontend/apiserver/backend/auth/pam_linux.go
Normal file
112
src/frontend/apiserver/backend/auth/pam_linux.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
//go:build linux && cgo
|
||||
|
||||
package auth
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -ldl
|
||||
#include <dlfcn.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// PAM types and constants (from <security/pam_appl.h>)
|
||||
#define PAM_SUCCESS 0
|
||||
#define PAM_PROMPT_ECHO_OFF 1
|
||||
|
||||
typedef struct pam_message {
|
||||
int msg_style;
|
||||
const char *msg;
|
||||
} pam_message;
|
||||
|
||||
typedef struct pam_response {
|
||||
char *resp;
|
||||
int resp_retcode;
|
||||
} pam_response;
|
||||
|
||||
typedef struct pam_conv {
|
||||
int (*conv)(int, const struct pam_message **, struct pam_response **, void *);
|
||||
void *appdata_ptr;
|
||||
} pam_conv;
|
||||
|
||||
typedef void* pam_handle_t;
|
||||
|
||||
// Function pointer types
|
||||
typedef int (*pam_start_fn)(const char*, const char*, const pam_conv*, pam_handle_t**);
|
||||
typedef int (*pam_authenticate_fn)(pam_handle_t*, int);
|
||||
typedef int (*pam_end_fn)(pam_handle_t*, int);
|
||||
|
||||
static void *pam_handle = NULL;
|
||||
static pam_start_fn fn_pam_start = NULL;
|
||||
static pam_authenticate_fn fn_pam_authenticate = NULL;
|
||||
static pam_end_fn fn_pam_end = NULL;
|
||||
|
||||
// load_pam opens libpam.so dynamically. Returns 0 on success.
|
||||
static int load_pam() {
|
||||
if (pam_handle) return 0;
|
||||
const char *libs[] = {"libpam.so.0", "libpam.so", NULL};
|
||||
for (int i = 0; libs[i]; i++) {
|
||||
pam_handle = dlopen(libs[i], RTLD_LAZY);
|
||||
if (pam_handle) break;
|
||||
}
|
||||
if (!pam_handle) return -1;
|
||||
fn_pam_start = (pam_start_fn) dlsym(pam_handle, "pam_start");
|
||||
fn_pam_authenticate = (pam_authenticate_fn) dlsym(pam_handle, "pam_authenticate");
|
||||
fn_pam_end = (pam_end_fn) dlsym(pam_handle, "pam_end");
|
||||
if (!fn_pam_start || !fn_pam_authenticate || !fn_pam_end) {
|
||||
dlclose(pam_handle);
|
||||
pam_handle = NULL;
|
||||
return -2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// conv_fn is the PAM conversation callback — returns the password.
|
||||
static int conv_fn(int num_msg, const pam_message **msg,
|
||||
pam_response **resp, void *appdata) {
|
||||
*resp = (pam_response*)calloc(num_msg, sizeof(pam_response));
|
||||
if (!*resp) return 4; // PAM_BUF_ERR
|
||||
for (int i = 0; i < num_msg; i++) {
|
||||
if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF) {
|
||||
(*resp)[i].resp = strdup((const char*)appdata);
|
||||
}
|
||||
}
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
// pam_auth_dynamic authenticates username/password via dlopen'd libpam.
|
||||
// Returns 0 on success, -1 if libpam not found, PAM error code otherwise.
|
||||
static int pam_auth_dynamic(const char *username, const char *password) {
|
||||
if (load_pam() != 0) return -1;
|
||||
pam_conv conv = { conv_fn, (void*)password };
|
||||
pam_handle_t *pamh = NULL;
|
||||
int ret = fn_pam_start("system-auth", username, &conv, &pamh);
|
||||
if (ret != PAM_SUCCESS) return ret;
|
||||
ret = fn_pam_authenticate(pamh, 0);
|
||||
fn_pam_end(pamh, ret);
|
||||
return ret;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Authenticate verifies credentials via dynamically loaded libpam.so.
|
||||
// The binary does not link against libpam at build time — it is loaded
|
||||
// at runtime only when this provider is active.
|
||||
func (p *PAMProvider) Authenticate(username, password string) error {
|
||||
cu := C.CString(username)
|
||||
cp := C.CString(password)
|
||||
defer C.free(unsafe.Pointer(cu))
|
||||
defer C.free(unsafe.Pointer(cp))
|
||||
|
||||
ret := C.pam_auth_dynamic(cu, cp)
|
||||
switch ret {
|
||||
case 0:
|
||||
return nil
|
||||
case -1:
|
||||
return fmt.Errorf("PAM library (libpam.so) not found on this system")
|
||||
default:
|
||||
return fmt.Errorf("invalid username or password")
|
||||
}
|
||||
}
|
||||
13
src/frontend/apiserver/backend/auth/pam_stub.go
Normal file
13
src/frontend/apiserver/backend/auth/pam_stub.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
//go:build !linux || !cgo
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Authenticate returns an error when PAM is not available (non-Linux or CGO disabled).
|
||||
func (p *PAMProvider) Authenticate(username, password string) error {
|
||||
return fmt.Errorf("PAM authentication is not supported on %s or without CGO; use config_file provider", runtime.GOOS)
|
||||
}
|
||||
14
src/frontend/apiserver/backend/auth/provider.go
Normal file
14
src/frontend/apiserver/backend/auth/provider.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package auth
|
||||
|
||||
// UserRoleSeeder is an optional interface for providers that can enumerate all users.
|
||||
// Used to pre-populate the role store at startup so permissions work after restart.
|
||||
type UserRoleSeeder interface {
|
||||
AllUsers() []string
|
||||
}
|
||||
|
||||
// AuthProvider is the interface for authentication strategies.
|
||||
type AuthProvider interface {
|
||||
Authenticate(username, password string) error
|
||||
GetUserRoles(username string) []string
|
||||
Name() string
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue