feat(auth): add local and OIDC management to tht

This commit is contained in:
2026-08-16 19:23:30 +02:00
parent d17ad0e95b
commit 9646ae09a0
15 changed files with 992 additions and 13 deletions
+14
View File
@@ -14,6 +14,7 @@ import (
"strconv"
"strings"
"github.com/aritmolab/thothii/tools/tht/internal/authconfig"
"github.com/aritmolab/thothii/tools/tht/internal/backup"
"github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/aritmolab/thothii/tools/tht/internal/config"
@@ -22,6 +23,7 @@ import (
"github.com/aritmolab/thothii/tools/tht/internal/output"
"github.com/aritmolab/thothii/tools/tht/internal/pi"
"github.com/aritmolab/thothii/tools/tht/internal/project"
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
"github.com/aritmolab/thothii/tools/tht/internal/serverops"
"github.com/aritmolab/thothii/tools/tht/internal/service"
"github.com/aritmolab/thothii/tools/tht/internal/setup"
@@ -38,6 +40,10 @@ Commands:
setup [--configure-only] [--installation-id ID] [--profile local|server]
Create or validate the local non-secret installation configuration.
version [--json] Show the host CLI build identity.
auth configure --mode local|oidc ...
Configure local users or OIDC group mapping; see tht auth for exact options.
auth status [--json] Show the redacted authentication configuration status.
auth user ... Manage local users; unavailable for OIDC installations.
status Show the Compose service state.
doctor [--json] Run non-mutating host, Compose, workflow, and Pi diagnostics.
logs Show the latest 200 sanitized service log lines (bounded; no follow mode).
@@ -130,6 +136,12 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
fmt.Fprintf(stderr, "tht: %s\n", output.Sanitize(err.Error(), nil))
return 2
}
if command != "auth" || len(commandArgs) == 0 || commandArgs[0] != "configure" {
if err := safeio.ValidatePrivateDirectory(installation.AuthenticationDirectory()); err != nil {
fmt.Fprintln(stderr, "tht: authentication configuration directory is unavailable or unsafe")
return 2
}
}
secretFiles, err := installation.SecretFiles()
if err != nil {
fmt.Fprintln(stderr, "tht: installation secret declarations could not be read")
@@ -144,6 +156,8 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
runner := compose.NewRunner("")
var result compose.Result
switch command {
case "auth":
return authconfig.Run(ctx, installation, commandArgs, os.Stdin, stdout, stderr)
case "status":
if len(commandArgs) != 0 {
return commandUsageError(stderr, "status does not accept arguments")
+8 -3
View File
@@ -201,7 +201,7 @@ func TestRootCommandIdentity(t *testing.T) {
name: "help banner",
args: []string{"--help"},
wantCode: 0,
wantText: "Usage: tht ",
wantText: " auth configure",
},
{
name: "version path",
@@ -1446,8 +1446,12 @@ func newCLIFixture(t *testing.T, envTemplate string) cliFixture {
}
}
envFile := filepath.Join(root, "installation.env")
authDirectory := filepath.Join(root, "auth")
if err := os.Mkdir(authDirectory, 0o700); err != nil {
t.Fatal(err)
}
installationPath := filepath.Join(root, "thothii-installation.yaml")
contents := "profile: local\nprojectDirectory: " + projectDirectory + "\nenvFile: " + envFile + "\n"
contents := "profile: local\nprojectDirectory: " + projectDirectory + "\nenvFile: " + envFile + "\nauthentication:\n configDirectory: " + authDirectory + "\n"
if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
@@ -1522,6 +1526,7 @@ func (f cliFixture) setEnvironment(t *testing.T, values ...string) {
func (f cliFixture) setEnvContents(t *testing.T, env string) {
t.Helper()
env += "THT_AUTH_CONFIG_ROOT=" + strconv.Quote(filepath.Join(f.root, "auth")) + "\n"
if err := os.WriteFile(f.envFile, []byte(env), 0o600); err != nil {
t.Fatal(err)
}
@@ -1547,7 +1552,7 @@ func (f cliFixture) setProfile(t *testing.T, profile string) {
if err := os.WriteFile(composePath, []byte("services: {}\n"), 0o600); err != nil {
t.Fatal(err)
}
contents := "profile: " + profile + "\nprojectDirectory: " + f.projectDirectory + "\nenvFile: " + f.envFile + "\n"
contents := "profile: " + profile + "\nprojectDirectory: " + f.projectDirectory + "\nenvFile: " + f.envFile + "\nauthentication:\n configDirectory: " + filepath.Join(f.root, "auth") + "\n"
if err := os.WriteFile(f.installationPath, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}