You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
181 lines
6.9 KiB
181 lines
6.9 KiB
|
2 months ago
|
package mock
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"path/filepath"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"tera-clone/services/tera-api/internal/domain"
|
||
|
|
"tera-clone/services/tera-api/internal/externalauth"
|
||
|
|
)
|
||
|
|
|
||
|
|
type dualExternalAuthenticator struct{}
|
||
|
|
|
||
|
|
func (dualExternalAuthenticator) Authenticate(_ context.Context, method string, server externalauth.Server, username, password string, _ time.Duration) (bool, error) {
|
||
|
|
return (method == "radius" || method == "tacacs") && server.Secret == method+"-test-secret" && username == "remote-user" && password == "remote-password", nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func newExternalSecretTestStore(t *testing.T) (*Store, StoreOptions) {
|
||
|
|
t.Helper()
|
||
|
|
directory := t.TempDir()
|
||
|
|
options := StoreOptions{
|
||
|
|
ConfigPath: filepath.Join(directory, "config.json"), SecretPath: filepath.Join(directory, "secrets.json"),
|
||
|
|
ExternalAuthenticator: dualExternalAuthenticator{},
|
||
|
|
}
|
||
|
|
store, err := NewStoreWithOptions(fixedClock, options)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
enabled, fallback, method, role := true, true, "radius", "port_operator"
|
||
|
|
_, err = store.UpdateExternalAuth(context.Background(), domain.ExternalAuthUpdate{
|
||
|
|
Enabled: &enabled, PrimaryMethod: &method, LocalFallback: &fallback, DefaultRole: &role,
|
||
|
|
RadiusServers: []domain.ExternalAuthServer{{ServerID: "radius-primary", Host: "radius.local", Port: 1645, Secret: "radius-test-secret", Enabled: true}},
|
||
|
|
TACACSServers: []domain.ExternalAuthServer{{ServerID: "tacacs-primary", Host: "tacacs.local", Port: 49, Secret: "tacacs-test-secret", Enabled: true}},
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := store.SaveAndApplyConfig(context.Background()); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
return store, options
|
||
|
|
}
|
||
|
|
|
||
|
|
func assertExternalTestSecrets(t *testing.T, store *Store, radius, tacacs string) {
|
||
|
|
t.Helper()
|
||
|
|
for method, expected := range map[string]string{"radius": radius, "tacacs": tacacs} {
|
||
|
|
for _, phase := range []string{"running", "persistent"} {
|
||
|
|
key := externalAuthSecretKey(phase, method, method+"-primary")
|
||
|
|
value, exists := store.secrets.Get(key)
|
||
|
|
if expected == "" {
|
||
|
|
if exists {
|
||
|
|
t.Errorf("cleared key %s was resurrected", key)
|
||
|
|
}
|
||
|
|
} else if !exists || string(value) != expected {
|
||
|
|
t.Errorf("key %s was not preserved", key)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestExternalAuthPolicyOnlyUpdatesPreserveBothSecrets(t *testing.T) {
|
||
|
|
for _, workflow := range []string{"apply-save", "save-apply", "save-and-apply"} {
|
||
|
|
t.Run(workflow, func(t *testing.T) {
|
||
|
|
store, options := newExternalSecretTestStore(t)
|
||
|
|
for _, method := range []string{"tacacs", "radius", "tacacs", "radius"} {
|
||
|
|
if _, err := store.UpdateExternalAuth(context.Background(), domain.ExternalAuthUpdate{PrimaryMethod: &method}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
role := "administrator"
|
||
|
|
if _, err := store.UpdateExternalAuth(context.Background(), domain.ExternalAuthUpdate{DefaultRole: &role}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
var err error
|
||
|
|
switch workflow {
|
||
|
|
case "apply-save":
|
||
|
|
_, err = store.ApplyConfig(context.Background())
|
||
|
|
if err == nil {
|
||
|
|
_, err = store.SaveConfig(context.Background())
|
||
|
|
}
|
||
|
|
case "save-apply":
|
||
|
|
_, err = store.SaveConfig(context.Background())
|
||
|
|
if err == nil {
|
||
|
|
_, err = store.ApplyConfig(context.Background())
|
||
|
|
}
|
||
|
|
case "save-and-apply":
|
||
|
|
_, err = store.SaveAndApplyConfig(context.Background())
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
assertExternalTestSecrets(t, store, "radius-test-secret", "tacacs-test-secret")
|
||
|
|
// Also verify persisted state after reopening the store.
|
||
|
|
store, err = NewStoreWithOptions(fixedClock, options)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
assertExternalTestSecrets(t, store, "radius-test-secret", "tacacs-test-secret")
|
||
|
|
session, err := store.Login(context.Background(), domain.AuthLoginRequest{Username: "remote-user", Password: "remote-password"})
|
||
|
|
if err != nil || session.Actor != "external-remote-user" || session.Role != role {
|
||
|
|
t.Fatalf("external %s login failed after policy update: %v", method, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestExternalAuthSingleProviderUpdatePreservesOtherSecret(t *testing.T) {
|
||
|
|
for _, method := range []string{"radius", "tacacs"} {
|
||
|
|
t.Run(method, func(t *testing.T) {
|
||
|
|
store, _ := newExternalSecretTestStore(t)
|
||
|
|
servers := []domain.ExternalAuthServer{{ServerID: method + "-primary", Host: method + ".local", Port: 49, Enabled: true}}
|
||
|
|
update := domain.ExternalAuthUpdate{}
|
||
|
|
if method == "radius" {
|
||
|
|
servers[0].Port = 1645
|
||
|
|
update.RadiusServers = servers
|
||
|
|
} else {
|
||
|
|
update.TACACSServers = servers
|
||
|
|
}
|
||
|
|
if _, err := store.UpdateExternalAuth(context.Background(), update); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := store.SaveAndApplyConfig(context.Background()); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
assertExternalTestSecrets(t, store, "radius-test-secret", "tacacs-test-secret")
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestExternalAuthExplicitClearSurvivesPolicyUpdate(t *testing.T) {
|
||
|
|
store, _ := newExternalSecretTestStore(t)
|
||
|
|
_, err := store.UpdateExternalAuth(context.Background(), domain.ExternalAuthUpdate{
|
||
|
|
TACACSServers: []domain.ExternalAuthServer{{ServerID: "tacacs-primary", Host: "tacacs.local", Port: 49, Enabled: false, ClearSecret: true}},
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
role := "administrator"
|
||
|
|
if _, err := store.UpdateExternalAuth(context.Background(), domain.ExternalAuthUpdate{DefaultRole: &role}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := store.SaveAndApplyConfig(context.Background()); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
assertExternalTestSecrets(t, store, "radius-test-secret", "")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestExternalAuthMissingSourceCannotEraseStoredSecret(t *testing.T) {
|
||
|
|
store, _ := newExternalSecretTestStore(t)
|
||
|
|
// Simulate a pending transaction written by the old implementation, which
|
||
|
|
// omitted unchanged secrets from the pending namespace.
|
||
|
|
store.pendingExternalAuth = cloneExternalAuthPointer(store.runningExternalAuth)
|
||
|
|
store.pendingExternalAuth.PrimaryMethod = "tacacs"
|
||
|
|
if _, err := store.ApplyConfig(context.Background()); err == nil {
|
||
|
|
t.Fatal("applying missing source secrets should fail instead of deleting destination secrets")
|
||
|
|
}
|
||
|
|
assertExternalTestSecrets(t, store, "radius-test-secret", "tacacs-test-secret")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestExternalAuthReportsMissingProtectedSecretAndRejectsPolicyOnlyUpdate(t *testing.T) {
|
||
|
|
store, _ := newExternalSecretTestStore(t)
|
||
|
|
for _, phase := range []string{"running", "persistent"} {
|
||
|
|
if err := store.secrets.Delete(externalAuthSecretKey(phase, "radius", "radius-primary")); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
settings, err := store.ExternalAuth(context.Background())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if settings.RadiusServers[0].SecretStored || !settings.TACACSServers[0].SecretStored {
|
||
|
|
t.Fatal("secret availability must reflect the protected store, not stale configuration markers")
|
||
|
|
}
|
||
|
|
role := "administrator"
|
||
|
|
if _, err := store.UpdateExternalAuth(context.Background(), domain.ExternalAuthUpdate{DefaultRole: &role}); err == nil {
|
||
|
|
t.Fatal("a policy-only update with a missing protected secret must fail")
|
||
|
|
}
|
||
|
|
assertExternalTestSecrets(t, store, "", "tacacs-test-secret")
|
||
|
|
}
|