Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ EA_PASSWORD=

# Auth
AUTH_DISABLED=false
AUTH_ADMIN_USERNAME=standalone
# AUTH_ADMIN_PASSWORD: If not set, a random password is generated
# AUTH_ADMIN_USERNAME: resolved from keystore first; set here only as last fallback
AUTH_ADMIN_USERNAME=
# AUTH_ADMIN_PASSWORD: resolved from keystore first; set here only as last fallback
AUTH_ADMIN_PASSWORD=
AUTH_JWT_KEY=your_secret_jwt_key
# AUTH_JWT_KEY: set for shared multi-pod deployments; leave empty for standalone runtime-only key
AUTH_JWT_KEY=
AUTH_JWT_EXPIRATION=24h
AUTH_REDIRECTION_JWT_EXPIRATION=5m
# Ignored when AUTH_CLIENT_ID is set (OIDC).
Expand Down
11 changes: 10 additions & 1 deletion cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ func main() {
runHealthCheck()
}

handled, err := handleAdminCLI(os.Args[1:], newKeyringStorageFunc(), os.Stdout)
if err != nil {
log.Fatalf("Admin command error: %v", err)
}

if handled {
return
}

cfg, err := initializeConfigFunc()
if err != nil {
log.Fatalf("Config error: %s", err)
Expand All @@ -66,7 +75,7 @@ func main() {
l := logger.New(cfg.Level)

handleEncryptionKey(cfg)
handleAdminPassword(cfg)
handleAdminCredentials(cfg)

// Run with system tray (if built with tray tag and --tray flag) or standard mode
if config.TrayMode && !trayBuildEnabled {
Expand Down
177 changes: 166 additions & 11 deletions cmd/app/main_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package main

import (
"bufio"
"bytes"
"crypto/rsa"
"crypto/x509"
"errors"
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"

"github.com/device-management-toolkit/go-wsman-messages/v2/pkg/security"

Expand All @@ -16,16 +21,54 @@ import (
"github.com/device-management-toolkit/console/pkg/logger"
)

type mockCredentialStore struct {
values map[string]string
errMap map[string]error
deletedKeys []string
}

func (m *mockCredentialStore) GetKeyValue(key string) (string, error) {
if err, ok := m.errMap[key]; ok {
return "", err
}

if v, ok := m.values[key]; ok {
return v, nil
}

return "", security.ErrKeyNotFound
}

func (m *mockCredentialStore) SetKeyValue(key, value string) error {
if m.values == nil {
m.values = map[string]string{}
}

m.values[key] = value

return nil
}

func (m *mockCredentialStore) DeleteKeyValue(key string) error {
if err, ok := m.errMap[key+":delete"]; ok {
return err
}

m.deletedKeys = append(m.deletedKeys, key)
delete(m.values, key)

return nil
}

func TestMainFunction(_ *testing.T) { //nolint:paralleltest // cannot have simultaneous tests modifying env variables.
os.Setenv("GIN_MODE", "debug")

// Mock functions
initializeConfigFunc = func() (*config.Config, error) {
return &config.Config{
HTTP: config.HTTP{Port: "8080"},
App: config.App{EncryptionKey: "test"},
Log: config.Log{Level: "info"},
Auth: config.Auth{AdminPassword: "test"},
Auth: config.Auth{AdminUsername: "admin", AdminPassword: "test"},
}, nil
}

Expand All @@ -35,7 +78,6 @@ func TestMainFunction(_ *testing.T) { //nolint:paralleltest // cannot have simul

runAppFunc = func(_ *config.Config, _ logger.Interface) {}

// Mock certificate functions
loadOrGenerateRootCertFunc = func(_ security.Storager, _ bool, _, _, _ string, _ bool) (*x509.Certificate, *rsa.PrivateKey, error) {
return &x509.Certificate{}, &rsa.PrivateKey{}, nil
}
Expand All @@ -44,11 +86,9 @@ func TestMainFunction(_ *testing.T) { //nolint:paralleltest // cannot have simul
return &x509.Certificate{}, &rsa.PrivateKey{}, nil
}

// Call the main function
main()
}

// TestGenerateRandomPassword tests the password generation function.
func TestGenerateRandomPassword(t *testing.T) {
t.Parallel()

Expand All @@ -72,7 +112,6 @@ func TestGenerateRandomPassword(t *testing.T) {
}
}

// TestGenerateRandomPassword_Uniqueness ensures generated passwords are unique.
func TestGenerateRandomPassword_Uniqueness(t *testing.T) {
t.Parallel()

Expand All @@ -82,22 +121,138 @@ func TestGenerateRandomPassword_Uniqueness(t *testing.T) {
password, err := generateRandomPassword(16)
require.NoError(t, err)
assert.False(t, passwords[password], "generated duplicate password")

passwords[password] = true
}
}

// TestHandleAdminPassword_AlreadyConfigured tests when password is already set.
func TestHandleAdminPassword_AlreadyConfigured(t *testing.T) {
t.Parallel()

cfg := &config.Config{
Auth: config.Auth{
AdminPassword: "already-set",
},
Auth: config.Auth{AdminPassword: "already-set"},
}

handleAdminPassword(cfg)

assert.Equal(t, "already-set", cfg.AdminPassword)
}

func TestNormalizeAdminPasswordHash_PlainTextInput(t *testing.T) {
t.Parallel()

hash, converted, err := normalizeAdminPasswordHash("plain-password")
require.NoError(t, err)
assert.True(t, converted)
require.NoError(t, bcrypt.CompareHashAndPassword([]byte(hash), []byte("plain-password")))
}

func TestNormalizeAdminPasswordHash_AlreadyHashed(t *testing.T) {
t.Parallel()

existingHash, err := bcrypt.GenerateFromPassword([]byte("secret"), bcrypt.DefaultCost)
require.NoError(t, err)

hash, converted, err := normalizeAdminPasswordHash(string(existingHash))
require.NoError(t, err)
assert.False(t, converted)
assert.Equal(t, string(existingHash), hash)
}

func TestResolveAdminCredentialsFromSources_PriorityOrder(t *testing.T) {
t.Setenv(authAdminUsernameEnv, "env-user")
t.Setenv(authAdminSecretEnv, "env-pass")

cfg := &config.Config{Auth: config.Auth{AdminUsername: "cfg-user", AdminPassword: "cfg-pass"}}
store := &mockCredentialStore{values: map[string]string{
keyringAdminUsername: "keyring-user",
keyringAdminPassword: "keyring-pass",
}}

username, password := resolveAdminCredentialsFromSources(cfg, store, map[string]string{
authAdminUsernameEnv: "dotenv-user",
authAdminSecretEnv: "dotenv-pass",
})

assert.Equal(t, "keyring-user", username)
assert.Equal(t, "keyring-pass", password)
}

func TestResolveAdminCredentialsFromSources_FallbackToDotEnvThenConfig(t *testing.T) {
t.Setenv(authAdminUsernameEnv, "")
t.Setenv(authAdminSecretEnv, "")

cfg := &config.Config{Auth: config.Auth{AdminUsername: "cfg-user", AdminPassword: "cfg-pass"}}
store := &mockCredentialStore{errMap: map[string]error{
keyringAdminUsername: security.ErrKeyNotFound,
keyringAdminPassword: security.ErrKeyNotFound,
}}

username, password := resolveAdminCredentialsFromSources(cfg, store, map[string]string{
authAdminUsernameEnv: "dotenv-user",
})

assert.Equal(t, "dotenv-user", username)
assert.Equal(t, "cfg-pass", password)
}

func TestResolveAdminCredentialsFromSources_KeyringReadErrorFallsBack(t *testing.T) {
t.Parallel()

cfg := &config.Config{Auth: config.Auth{AdminUsername: "cfg-user", AdminPassword: "cfg-pass"}}
store := &mockCredentialStore{errMap: map[string]error{
keyringAdminUsername: errors.New("keyring unavailable"),
keyringAdminPassword: errors.New("keyring unavailable"),
}}

username, password := resolveAdminCredentialsFromSources(cfg, store, map[string]string{})

assert.Equal(t, "cfg-user", username)
assert.Equal(t, "cfg-pass", password)
}

func TestConfirmPersistCredentialsToConfig_Yes(t *testing.T) {
t.Parallel()

reader := bufio.NewReader(strings.NewReader("Y\n"))
assert.True(t, confirmPersistCredentialsToConfig(reader))
}

func TestConfirmPersistCredentialsToConfig_No(t *testing.T) {
t.Parallel()

reader := bufio.NewReader(strings.NewReader("n\n"))
assert.False(t, confirmPersistCredentialsToConfig(reader))
}

func TestHandleAdminCLI_ShowAdmin_HidesPassword(t *testing.T) {
t.Parallel()

store := &mockCredentialStore{values: map[string]string{
keyringAdminUsername: "alice",
keyringAdminPassword: "hash-value",
}}
buf := &bytes.Buffer{}

handled, err := handleAdminCLI([]string{"--show-admin"}, store, buf)
require.NoError(t, err)
assert.True(t, handled)
assert.Contains(t, buf.String(), "admin username: alice")
assert.NotContains(t, buf.String(), "hash-value")
}

func TestHandleAdminCLI_RemoveAdmin(t *testing.T) {
t.Parallel()

store := &mockCredentialStore{values: map[string]string{
keyringAdminUsername: "alice",
keyringAdminPassword: "super-secret",
}}
buf := &bytes.Buffer{}

handled, err := handleAdminCLI([]string{"--remove-admin"}, store, buf)
require.NoError(t, err)
assert.True(t, handled)
assert.Contains(t, store.deletedKeys, keyringAdminUsername)
assert.Contains(t, store.deletedKeys, keyringAdminPassword)
assert.Contains(t, buf.String(), "Admin credentials removed from keystore.")
}
Loading
Loading