From b86c757941b960daaf83f7975c667bfc4a4fa649 Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 19 Mar 2024 18:00:28 +0100 Subject: [PATCH 1/5] Adds a property to allow user login without domain specified --- cmd/aad-cli/cli/cli_test.go | 15 - cmd/aad-cli/cli/config_test.go | 239 ---------- cmd/aad-cli/cli/export_test.go | 53 --- cmd/aad-cli/cli/user_test.go | 325 ------------- cmd/aad-cli/cli/version_test.go | 73 --- internal/aad/aad_test.go | 66 --- internal/cache/cache_test.go | 550 ---------------------- internal/cache/export_test.go | 50 -- internal/cache/groupdb_test.go | 170 ------- internal/cache/helper_test.go | 24 - internal/cache/internal_test.go | 59 --- internal/cache/passwddb_test.go | 223 --------- internal/cache/shadowdb_test.go | 154 ------ internal/config/config.go | 1 + internal/config/config_test.go | 208 -------- internal/config/export_test.go | 8 - internal/config/internal_test.go | 71 --- internal/i18n/export_test.go | 21 - internal/i18n/i18n_test.go | 192 -------- internal/logger/internal_test.go | 28 -- internal/logger/logger_test.go | 113 ----- internal/logger/logrus_test.go | 72 --- internal/nss/errors_test.go | 50 -- internal/nss/export_test.go | 20 - internal/nss/group/export_test.go | 57 --- internal/nss/group/group_test.go | 264 ----------- internal/nss/group/util_c_test.go | 53 --- internal/nss/logger_test.go | 129 ----- internal/nss/passwd/export_test.go | 60 --- internal/nss/passwd/passwd_test.go | 263 ----------- internal/nss/passwd/util_c_test.go | 48 -- internal/nss/shadow/export_test.go | 64 --- internal/nss/shadow/shadow_test.go | 218 --------- internal/nss/shadow/util_c_test.go | 53 --- internal/pam/export_test.go | 12 - internal/pam/logger_test.go | 102 ---- internal/pam/msg_test.go | 54 --- internal/pam/pam.go | 9 +- internal/pam/pam_test.go | 95 ---- internal/user/user_test.go | 80 ---- nss/integration-tests/helper_test.go | 97 ---- nss/integration-tests/integration_test.go | 173 ------- nss/src/cache/mod_tests.rs | 2 - pam/integration_test.go | 237 ---------- pam/utils_c_test.go | 59 --- 45 files changed, 8 insertions(+), 4906 deletions(-) delete mode 100644 cmd/aad-cli/cli/cli_test.go delete mode 100644 cmd/aad-cli/cli/config_test.go delete mode 100644 cmd/aad-cli/cli/export_test.go delete mode 100644 cmd/aad-cli/cli/user_test.go delete mode 100644 cmd/aad-cli/cli/version_test.go delete mode 100644 internal/aad/aad_test.go delete mode 100644 internal/cache/cache_test.go delete mode 100644 internal/cache/export_test.go delete mode 100644 internal/cache/groupdb_test.go delete mode 100644 internal/cache/helper_test.go delete mode 100644 internal/cache/internal_test.go delete mode 100644 internal/cache/passwddb_test.go delete mode 100644 internal/cache/shadowdb_test.go delete mode 100644 internal/config/config_test.go delete mode 100644 internal/config/export_test.go delete mode 100644 internal/config/internal_test.go delete mode 100644 internal/i18n/export_test.go delete mode 100644 internal/i18n/i18n_test.go delete mode 100644 internal/logger/internal_test.go delete mode 100644 internal/logger/logger_test.go delete mode 100644 internal/logger/logrus_test.go delete mode 100644 internal/nss/errors_test.go delete mode 100644 internal/nss/export_test.go delete mode 100644 internal/nss/group/export_test.go delete mode 100644 internal/nss/group/group_test.go delete mode 100644 internal/nss/group/util_c_test.go delete mode 100644 internal/nss/logger_test.go delete mode 100644 internal/nss/passwd/export_test.go delete mode 100644 internal/nss/passwd/passwd_test.go delete mode 100644 internal/nss/passwd/util_c_test.go delete mode 100644 internal/nss/shadow/export_test.go delete mode 100644 internal/nss/shadow/shadow_test.go delete mode 100644 internal/nss/shadow/util_c_test.go delete mode 100644 internal/pam/export_test.go delete mode 100644 internal/pam/logger_test.go delete mode 100644 internal/pam/msg_test.go delete mode 100644 internal/pam/pam_test.go delete mode 100644 internal/user/user_test.go delete mode 100644 nss/integration-tests/helper_test.go delete mode 100644 nss/integration-tests/integration_test.go delete mode 100644 pam/integration_test.go delete mode 100644 pam/utils_c_test.go diff --git a/cmd/aad-cli/cli/cli_test.go b/cmd/aad-cli/cli/cli_test.go deleted file mode 100644 index 7bcb5a5a..00000000 --- a/cmd/aad-cli/cli/cli_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package cli_test - -import ( - "flag" - "testing" - - "github.com/ubuntu/aad-auth/internal/testutils" -) - -func TestMain(m *testing.M) { - testutils.InstallUpdateFlag() - flag.Parse() - - m.Run() -} diff --git a/cmd/aad-cli/cli/config_test.go b/cmd/aad-cli/cli/config_test.go deleted file mode 100644 index b1f99377..00000000 --- a/cmd/aad-cli/cli/config_test.go +++ /dev/null @@ -1,239 +0,0 @@ -package cli_test - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - "testing" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/cmd/aad-cli/cli" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -func TestConfigPrint(t *testing.T) { - tests := map[string]struct { - configFile string - domain string - - wantErr bool - }{ - "default domain": {}, - "custom domain": {domain: "example.com"}, - "homedir and shell optional fields missing": {configFile: "missing-homedir-and-shell-fields.conf"}, - "required entries only present in default domain": {domain: "example.com", configFile: "required-present-in-default-domain.conf"}, - - // error cases - "missing required entries": {configFile: "missing-required.conf", wantErr: true}, - "non-existent config": {configFile: "non-existent.conf", wantErr: true}, - "malformed config": {configFile: "malformed.conf", wantErr: true}, - "type mismatch": {configFile: "type-mismatch.conf", wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - cmdArgs := []string{"config"} - - if tc.domain != "" { - cmdArgs = append(cmdArgs, "--domain", tc.domain) - } - - if tc.configFile == "" { - tc.configFile = "aad.conf" - } - tc.configFile = filepath.Join("testdata", tc.configFile) - - c := cli.New(cli.WithConfigFile(tc.configFile)) - got, err := testutils.RunApp(t, c, cmdArgs...) - - if tc.wantErr { - require.Error(t, err, "expected command to return an error") - return - } - require.NoError(t, err, "expected command to succeed") - - want := testutils.LoadWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "expected output to match golden file") - }) - } -} - -func TestConfigEdit(t *testing.T) { - requiredConfig := "tenant_id = something\napp_id = something" - badConfig := "tenant_id = something" - malformedConfig := "aaaaaaaaaaaaa" - - tests := map[string]struct { - configFile string - newConfigContent string - - wantErr bool - wantEditorErr bool - }{ - "loads the previous config": {}, - - // This test asserts that the config template is loaded when executing the command with an absent config file - // (see TEMPORARY CONFIG CONTENTS in the editor mock). - // To avoid getting an error on save, we have to pass in a valid config - // since the template is commented out, hence the need for newConfigContent. - "loads the config template if previous is not present": {configFile: "nonexistent.conf", newConfigContent: requiredConfig}, - - // error cases - "editor returns an error": {wantEditorErr: true, wantErr: true}, - "cfg validation returns an error": {newConfigContent: badConfig, wantErr: true}, - "cfg loading returns an error": {newConfigContent: malformedConfig, wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - if tc.configFile == "" { - tc.configFile = "aad.conf" - } - tc.configFile = filepath.Join("testdata", tc.configFile) - - // Copy the config file to a temporary location, so that changes do - // not persist across tests. - tc.configFile = copyToTempPath(t, tc.configFile) - editorMock := newEditorMock(t, tc.configFile, tc.newConfigContent, tc.wantEditorErr) - - c := cli.New(cli.WithConfigFile(tc.configFile), cli.WithEditor(editorMock)) - got, err := testutils.RunApp(t, c, "config", "-e") - - tempConfigPath := tempConfigPathFromOutput(t, got) - if tc.wantErr { - require.Error(t, err, "expected command to return an error") - require.FileExists(t, tempConfigPath, "expected temporary config file to be present") - return - } - require.NoError(t, err, "expected command to succeed") - require.NoFileExists(t, tempConfigPath, "expected temporary config file not to be present") - - got = sanitizeTempPaths(t, got) - want := testutils.LoadWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "expected output to match golden file") - }) - } -} - -func TestConfigEditor(t *testing.T) { - // Custom editor - err := os.Setenv("EDITOR", "vim") - require.NoError(t, err, "Setup: failed to set EDITOR") - - c := cli.New() - require.Equal(t, "vim", c.Editor(), "expected editor to be vim") - - // Default behavior - err = os.Unsetenv("EDITOR") - require.NoError(t, err, "Setup: failed to unset EDITOR") - - c = cli.New() - require.Equal(t, "sensible-editor", c.Editor(), "expected default editor to be sensible-editor") -} - -func TestConfigMutuallyExclusiveFlags(t *testing.T) { - c := cli.New() - _, err := testutils.RunApp(t, c, "config", "--edit", "--domain", "example.com") - require.ErrorContains(t, err, "if any flags in the group [edit domain] are set none of the others can be", "expected command to return mutually exclusive flag error") - - // Short flags - _, err = testutils.RunApp(t, c, "config", "-e", "-d", "example.com") - require.ErrorContains(t, err, "if any flags in the group [edit domain] are set none of the others can be", "expected command to return mutually exclusive flag error") -} - -// newEditorMock returns the path to a shell script that overrides the default -// config editor. -// The script prints the previous config file, and if a new config is provided, -// it replaces the previous config file with the new contents. -func newEditorMock(t *testing.T, configFile, newConfig string, wantErr bool) string { - t.Helper() - - editor, err := os.CreateTemp(t.TempDir(), "editor-mock.*.sh") - require.NoError(t, err, "Setup: failed to create temporary file") - defer editor.Close() - - var b bytes.Buffer - b.WriteString("#!/bin/sh") - - b.WriteString(` -echo "TEMPORARY CONFIG PATH: $1" -echo "TEMPORARY CONFIG CONTENTS:" -cat $1`) - // Exit early with an error if requested - if wantErr { - b.WriteString("\nexit 1\n") - } - - b.WriteString(fmt.Sprintf(` -# Print previous config file -echo "PREVIOUS CONFIG FILE:" -cat %s - -`, configFile)) - - if newConfig != "" { - b.WriteString(fmt.Sprintf(`# Update with new config contents -echo "NEW CONFIG FILE:" -echo "%s" | tee $1 -`, newConfig)) - } - - err = editor.Chmod(0700) - require.NoError(t, err, "Setup: failed to set executable permissions on temporary file") - - _, err = editor.Write(b.Bytes()) - require.NoError(t, err, "Setup: failed to write temporary file") - - return editor.Name() -} - -// copyToTempPath copies the given config file to a temporary location, and returns the path to it. -// If the given config file is not present, a non-existent temporary path is returned. -func copyToTempPath(t *testing.T, file string) string { - t.Helper() - - tempdir := t.TempDir() - r, err := os.Open(file) - if err != nil { - // We assume a non-existent file was a deliberate choice, - // so return back a non-existent file in a temporary directory. - return filepath.Join(tempdir, filepath.Base(file)) - } - defer r.Close() - - w, err := os.Create(filepath.Join(tempdir, "aad.conf")) - require.NoError(t, err, "Setup: failed to create temporary file") - defer w.Close() - - _, err = w.ReadFrom(r) - require.NoError(t, err, "Setup: failed to copy file") - - return w.Name() -} - -// tempConfigPathFromOutput returns the path to the temporary config file used -// by the editor from the given string. -func tempConfigPathFromOutput(t *testing.T, output string) string { - t.Helper() - - lines := strings.Split(output, "\n") - for _, line := range lines { - if strings.Contains(line, "TEMPORARY CONFIG PATH:") { - return strings.TrimSpace(strings.Split(line, ":")[1]) - } - } - t.Fatalf("failed to find temporary config path in output") - return "" -} - -// sanitizesTempPaths replaces temporary config paths in the given string with a -// deterministic placeholder. -func sanitizeTempPaths(t *testing.T, output string) string { - t.Helper() - - tmpPaths := regexp.MustCompile(`/tmp/.*\.conf[^\s]*`) - return tmpPaths.ReplaceAllString(output, "/tmp/aad.conf") -} diff --git a/cmd/aad-cli/cli/export_test.go b/cmd/aad-cli/cli/export_test.go deleted file mode 100644 index 756e809f..00000000 --- a/cmd/aad-cli/cli/export_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package cli - -import "github.com/ubuntu/aad-auth/internal/cache" - -// WithDpkgQueryCmd specifies a custom dpkg-query command to use for the user command. -// This is only used in tests. -func WithDpkgQueryCmd(p string) func(o *options) { - return func(o *options) { - o.dpkgQueryCmd = p - } -} - -// WithCache specifies a personalized cache object to use for the app. -// Useful in tests for overriding the default cache. -func WithCache(c *cache.Cache) func(o *options) { - return func(o *options) { - o.cache = c - } -} - -// WithEditor specifies a custom editor to use when editing the config file. -// Will probably only be used in tests. -func WithEditor(p string) func(o *options) { - return func(o *options) { - o.editor = p - } -} - -// WithConfigFile specifies a custom config file to use for the config command. -func WithConfigFile(p string) func(o *options) { - return func(o *options) { - o.configFile = p - } -} - -// WithProcFs specifies a custom /proc path to use for the user command. -func WithProcFs(p string) func(o *options) { - return func(o *options) { - o.procFs = p - } -} - -// WithCurrentUser specifies a custom user to use by default for the user command. -func WithCurrentUser(p string) func(o *options) { - return func(o *options) { - o.currentUser = p - } -} - -// Editor returns the editor used by the program. -func (a App) Editor() string { - return a.options.editor -} diff --git a/cmd/aad-cli/cli/user_test.go b/cmd/aad-cli/cli/user_test.go deleted file mode 100644 index 9ddfdf2e..00000000 --- a/cmd/aad-cli/cli/user_test.go +++ /dev/null @@ -1,325 +0,0 @@ -package cli_test - -import ( - "context" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/spf13/cobra" - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/cmd/aad-cli/cli" - "github.com/ubuntu/aad-auth/internal/cache" - "github.com/ubuntu/aad-auth/internal/testutils" - "golang.org/x/exp/slices" -) - -func TestUserShellCompletion(t *testing.T) { - tests := map[string]struct { - args string - }{ - "get all users, short flag": {args: "user -n"}, - "get all users, long flag": {args: "user --name"}, - "get attributes for user": {args: "user"}, - "get attributes for overridden user": {args: "user --name myuser@domain.com"}, - "default completion for last argument": {args: "user gecos"}, - "default completion, overridden user": {args: "user gecos --name myuser@domain.com"}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - args := []string{cobra.ShellCompRequestCmd} - args = append(args, strings.Split(tc.args, " ")...) - args = append(args, "") - - cacheDir := t.TempDir() - cacheDB := "users_in_db" - testutils.PrepareDBsForTests(t, cacheDir, cacheDB) - cache := testutils.NewCacheForTests(t, cacheDir) - - c := cli.New(cli.WithCache(cache)) - got, err := testutils.RunApp(t, c, args...) - require.NoError(t, err, "failed to run completion") - - want := testutils.LoadWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "expected output to match golden file") - }) - } -} - -func TestUser(t *testing.T) { - tests := map[string]struct { - args string - shadowNotAvailable bool - - wantErr bool - }{ - "get all users": {args: "--all"}, - "get user": {args: "--name myuser@domain.com"}, - "get user, shadow not available": {args: "--name myuser@domain.com", shadowNotAvailable: true}, - - "get login": {args: "--name myuser@domain.com login"}, - "get password": {args: "--name myuser@domain.com password"}, - "get uid": {args: "--name myuser@domain.com uid"}, - "get gid": {args: "--name myuser@domain.com gid"}, - "get gecos": {args: "--name myuser@domain.com gecos"}, - "get home": {args: "--name myuser@domain.com home"}, - "get shell": {args: "--name myuser@domain.com shell"}, - "get last_online_auth": {args: "--name myuser@domain.com last_online_auth"}, - "get shadow_password": {args: "--name myuser@domain.com shadow_password"}, - - // error cases - "get nonexistent user": {args: "--name nouser@domain.com", wantErr: true}, - "get bad_attribute": {args: "--name myuser@domain.com bad_attribute", wantErr: true}, - "get shadow_password, shadow not available": {args: "--name myuser@domain.com shadow_password", shadowNotAvailable: true, wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - args := []string{"user"} - args = append(args, strings.Split(tc.args, " ")...) - - cacheDir := t.TempDir() - cacheDB := "users_in_db" - testutils.PrepareDBsForTests(t, cacheDir, cacheDB) - - shadowMode := -1 - if tc.shadowNotAvailable { - shadowMode = 0 - } - cache := testutils.NewCacheForTests(t, cacheDir, cache.WithShadowMode(shadowMode)) - c := cli.New(cli.WithCache(cache)) - - got, err := testutils.RunApp(t, c, args...) - if tc.wantErr { - require.Error(t, err, "expected command to return an error") - return - } - require.NoError(t, err, "expected command to succeed") - - if slices.Contains(args, "--name") { - username := args[slices.Index(args, "--name")+1] - user, err := cache.GetUserByName(context.Background(), username) - require.NoError(t, err, "Setup: failed to get user from cache") - - if len(args) < 4 || slices.Contains(args, "last_online_auth") { - tmp := strings.Index(got, user.LastOnlineAuth.Format(time.RFC3339)) - require.NotEqual(t, -1, tmp, "Expected to find the correct time") - } - - got = testutils.TimestampToWildcard(t, got, user.LastOnlineAuth) - } - want := testutils.LoadWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "expected output to match golden file") - }) - } -} - -func TestUserSetAttribute(t *testing.T) { - tests := map[string]struct { - args string - - badPerms bool - wantErr bool - }{ - "set gecos": {args: "user --name myuser@domain.com gecos newvalue"}, - "set home": {args: "user --name myuser@domain.com home newvalue"}, - "set shell": {args: "user --name myuser@domain.com shell newvalue"}, - "set shell on default user": {args: "user shell newvalue"}, - - // error cases - "set bad_attribute": {args: "user --name myuser@domain.com bad_attribute newvalue", wantErr: true}, - "set nonexistent user": {args: "user --name nouser@domain.com gecos newvalue", wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - args := strings.Split(tc.args, " ") - - usernameIndex := slices.Index(args, "--name") - username := args[usernameIndex+1] - - // Fallback when a username is not provided - if usernameIndex == -1 { - username = "myuser@domain.com" - } - - cacheDir := t.TempDir() - cacheDB := "users_in_db" - testutils.PrepareDBsForTests(t, cacheDir, cacheDB) - cache := testutils.NewCacheForTests(t, cacheDir) - c := cli.New(cli.WithCache(cache), cli.WithCurrentUser("myuser@domain.com")) - - // Gets the user time before running the cli - var wantTime time.Time - if username != "nouser@domain.com" { - aux, err := cache.GetUserByName(context.Background(), username) - require.NoError(t, err, "Expected no error but got one.") - wantTime = aux.LastOnlineAuth - } - - _, err := testutils.RunApp(t, c, args...) - if tc.wantErr { - require.Error(t, err, "expected command to return an error") - return - } - require.NoError(t, err, "expected command to succeed") - - user, err := cache.GetUserByName(context.Background(), username) - require.NoError(t, err, "Setup: failed to get user from cache") - - // Handles time comparison separately - require.Equal(t, wantTime, user.LastOnlineAuth, "Expected last_online_auth to not have changed") - - got, err := user.IniString() - require.NoError(t, err, "Setup: failed to get user representation as ini") - got = testutils.TimestampToWildcard(t, got, user.LastOnlineAuth) - - want := testutils.LoadWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "expected output to match golden file") - }) - } -} - -func TestUserMoveHomeDirectory(t *testing.T) { - tests := map[string]struct { - prevHomeDir string - newHomeDir string - userLoggedIn bool - - wantErr bool - }{ - "move home directory": {prevHomeDir: "oldhome", newHomeDir: "newhome"}, - - // Error cases - homedir attribute is updated - "fail if previous directory is absent": {prevHomeDir: "absent", newHomeDir: "newhome", wantErr: true}, - "fail if previous directory is a file": {prevHomeDir: "oldhomefile", newHomeDir: "newhome", wantErr: true}, - "fail if new directory already exists": {prevHomeDir: "oldhome", newHomeDir: "existingnewhome", wantErr: true}, - - // Error cases - homedir attribute is not updated - "fail if the user has open processses": {prevHomeDir: "oldhome", newHomeDir: "newhome", userLoggedIn: true, wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - tmpDir := t.TempDir() - cacheDir := filepath.Join(tmpDir, "cache") - testutils.PrepareDBsForTests(t, cacheDir, "db_with_expired_users") - cache := testutils.NewCacheForTests(t, cacheDir) - - // Set up test filesystem structure - err := os.MkdirAll(filepath.Join(tmpDir, "oldhome"), 0750) - require.NoError(t, err, "Setup: failed to create previous home directory") - err = os.MkdirAll(filepath.Join(tmpDir, "existingnewhome"), 0750) - require.NoError(t, err, "Setup: failed to create existing new home directory") - err = os.WriteFile(filepath.Join(tmpDir, "oldhomefile"), []byte("test content"), 0600) - require.NoError(t, err, "Setup: failed to create previous home directory file") - - // Set up fake /proc structure for checking if the user has open processes - procFs := filepath.Join("testdata", "not_in_use") - if tc.userLoggedIn { - procFs = filepath.Join("testdata", "in_use") - } - - require.DirExists(t, procFs, "Setup: failed to find fake /proc filesystem") - t.Cleanup(func() { - err := os.Remove(filepath.Join(procFs, "1", "root")) - require.NoError(t, err, "Teardown: failed to remove symlink") - err = os.Remove(filepath.Join(procFs, "2", "root")) - require.NoError(t, err, "Teardown: failed to remove symlink") - }) - - // Both processes run in our namespace - err = os.Symlink("/", filepath.Join(procFs, "1", "root")) - require.NoError(t, err, "Setup: failed to create symlink") - err = os.Symlink("/", filepath.Join(procFs, "2", "root")) - require.NoError(t, err, "Setup: failed to create symlink") - - prevHomeDir := filepath.Join(tmpDir, tc.prevHomeDir) - newHomeDir := filepath.Join(tmpDir, tc.newHomeDir) - - err = cache.UpdateUserAttribute(context.Background(), "futureuser@domain.com", "home", prevHomeDir) - require.NoError(t, err, "Setup: failed to set initial user home directory") - - c := cli.New(cli.WithCache(cache), cli.WithProcFs(procFs)) - _, runErr := testutils.RunApp(t, c, "user", "--name", "futureuser@domain.com", "home", newHomeDir, "--move-home") - - // We always expect the passwd attribute to be updated in this test, unless the user has open processes - home, err := cache.QueryPasswdAttribute(context.Background(), "futureuser@domain.com", "home") - require.NoError(t, err, "Setup: failed to get user home directory") - if tc.userLoggedIn { - require.Equal(t, prevHomeDir, home, "expected home directory not to be updated") - } else { - require.Equal(t, newHomeDir, home, "expected home directory to be updated") - } - - if !tc.wantErr { - require.NoError(t, runErr, "expected command to succeed") - require.DirExists(t, newHomeDir, "expected new home directory to exist") - require.NoDirExists(t, prevHomeDir, "expected previous home directory to not exist") - return - } - - require.Error(t, runErr, "expected command to return an error") - if tc.prevHomeDir == "oldhome" { - require.DirExists(t, prevHomeDir, "expected previous home directory to exist") - } - if tc.newHomeDir != "existingnewhome" { - require.NoDirExists(t, newHomeDir, "expected new home directory to not exist") - } - }) - } -} - -func TestUserMutuallyExclusiveFlags(t *testing.T) { - tests := map[string]struct { - args string - expectedErr string - }{ - "both --name and --all": { - args: "user --name myuser@domain.com --all", - expectedErr: "if any flags in the group [name all] are set none of the others can be", - }, - "both -n and -a": { - args: "user -n myuser@domain.com -a", - expectedErr: "if any flags in the group [name all] are set none of the others can be", - }, - "both --move-home and --all": { - args: "user --move-home --all home newvalue", - expectedErr: "if any flags in the group [move-home all] are set none of the others can be", - }, - "both -m and -a": { - args: "user -m -a home newvalue", - expectedErr: "if any flags in the group [move-home all] are set none of the others can be", - }, - "--move-home without argument to update": { - args: "user --move-home", - expectedErr: "move-home can only be used when modifying home attribute", - }, - "--move-home with incorrect argument to update": { - args: "user --move-home gecos newvalue", - expectedErr: "move-home can only be used when modifying home attribute", - }, - "--move-home without new value to update with": { - args: "user --move-home home", - expectedErr: "move-home can only be used when modifying home attribute", - }, - } - - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - cacheDir := t.TempDir() - cacheDB := "users_in_db" - testutils.PrepareDBsForTests(t, cacheDir, cacheDB) - cache := testutils.NewCacheForTests(t, cacheDir) - - c := cli.New(cli.WithCache(cache)) - _, err := testutils.RunApp(t, c, strings.Split(tc.args, " ")...) - - require.ErrorContains(t, err, tc.expectedErr, "expected command to return flag parsing error") - }) - } -} diff --git a/cmd/aad-cli/cli/version_test.go b/cmd/aad-cli/cli/version_test.go deleted file mode 100644 index fbfea55b..00000000 --- a/cmd/aad-cli/cli/version_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package cli_test - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/cmd/aad-cli/cli" - "github.com/ubuntu/aad-auth/internal/consts" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -func TestVersion(t *testing.T) { - tests := map[string]struct { - installedPkgs []string - }{ - "both libraries installed": {installedPkgs: []string{"libpam-aad", "libnss-aad"}}, - "only pam installed": {installedPkgs: []string{"libpam-aad"}}, - "only nss installed": {installedPkgs: []string{"libnss-aad"}}, - "both libraries not installed": {}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - mockCmd := newQueryMockCmd(t, tc.installedPkgs) - - c := cli.New(cli.WithDpkgQueryCmd(mockCmd)) - got, err := testutils.RunApp(t, c, "version") - require.NoError(t, err, "Version should not fail") - got = sanitizeDevVersion(got) - - want := testutils.LoadWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "Should get expected version output") - }) - } -} - -func newQueryMockCmd(t *testing.T, installedPkgs []string) string { - t.Helper() - - tmpfile, err := os.Create(filepath.Join(t.TempDir(), "dpkg-query.sh")) - require.NoError(t, err, "Setup: failed to create temporary file") - defer tmpfile.Close() - - var b bytes.Buffer - b.WriteString(`#!/bin/sh -# Loop to last argument which is the package name -echo "stderr should not be captured" >&2 -for pkgname; do :; done -`) - for _, pkg := range installedPkgs { - b.WriteString(fmt.Sprintf(`[ "$pkgname" = "%s" ] && printf ${pkgname}-ver && exit 0`, pkg)) - b.WriteRune('\n') - } - - b.WriteString("exit 1\n") - - err = tmpfile.Chmod(0700) - require.NoError(t, err, "Setup: failed to set executable permissions on temporary file") - - _, err = tmpfile.Write(b.Bytes()) - require.NoError(t, err, "Setup: failed to write temporary file") - - return tmpfile.Name() -} - -func sanitizeDevVersion(s string) string { - return strings.ReplaceAll(s, consts.Version, "dev") -} diff --git a/internal/aad/aad_test.go b/internal/aad/aad_test.go deleted file mode 100644 index 82343eb9..00000000 --- a/internal/aad/aad_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package aad_test - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/aad" - "github.com/ubuntu/aad-auth/internal/config" -) - -func TestAuthenticate(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - appID string - username string - - wantErr error - }{ - "can authenticate with password only": {}, - "can authenticate even with mfa required": {username: "requireMFA@domain.com"}, - - // error cases - "can't connect to authority": {appID: "connection failed", wantErr: aad.ErrNoNetwork}, - "public client disallowed": {appID: "public client disallowed", wantErr: aad.ErrDeny}, - "no tenant-wide consent": {appID: "no tenant-wide consent", wantErr: aad.ErrDeny}, - "unreadable server response": {username: "unreadable server response", wantErr: aad.ErrDeny}, - "invalid server response": {username: "invalid server response", wantErr: aad.ErrDeny}, - "invalid credentials": {username: "invalid credentials", wantErr: aad.ErrDeny}, - "no such user": {username: "no such user", wantErr: aad.ErrDeny}, - "unknown error code": {username: "unknown error code", wantErr: aad.ErrDeny}, - "unknown error type": {username: "unknown error type", wantErr: aad.ErrNoNetwork}, - - // multiple error cases - "multiple errors, first known (here mfa) wins": {username: "multiple errors, first known is mfa", wantErr: nil}, - "multiple errors, first known (here invalid credentials) wins": {username: "multiple errors, first known is invalid credential", wantErr: aad.ErrDeny}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - if tc.appID == "" { - tc.appID = "valid" - } - if tc.username == "" { - tc.username = "success@domain.com" - } - - auth := aad.NewWithMockClient() - cfg := config.AAD{ - TenantID: "tenant id", - AppID: tc.appID, - } - err := auth.Authenticate(context.Background(), cfg, tc.username, "password") - if tc.wantErr != nil { - require.Error(t, err) - require.True(t, errors.Is(err, tc.wantErr), "Error should be %v", tc.wantErr) - return - } - require.NoError(t, err) - }) - } -} diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go deleted file mode 100644 index 7eb3e89f..00000000 --- a/internal/cache/cache_test.go +++ /dev/null @@ -1,550 +0,0 @@ -package cache_test - -import ( - "context" - "flag" - "fmt" - "io/fs" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/cache" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -func TestNew(t *testing.T) { - t.Parallel() - - var noAccessFilePerm fs.FileMode = 0000 - var roFilePerm fs.FileMode = 0400 - - tests := map[string]struct { - reOpenCache bool - waitForClose bool - - // permission issues - isNotRootUIDGID bool - cantChownShadowOnCreation bool - changeFilePerm string - shadowCreationFilePerm *fs.FileMode - - wantShadowMode *int - wantErr bool - wantErrReopen bool - }{ - "create cache with all permissions": {}, - "reuse opened cache": {reOpenCache: true}, - "reuse closed cache (files exists)": {waitForClose: true, reOpenCache: true}, - - // Shadow files special cases - "can still open shadow file RO": {shadowCreationFilePerm: &roFilePerm, wantShadowMode: &cache.ShadowROMode}, - "no access to shadow file is still allowed": {shadowCreationFilePerm: &noAccessFilePerm, wantShadowMode: &cache.ShadowNotAvailableMode}, - - // error cases - "can't create DB not being root UID or GID": {isNotRootUIDGID: true, wantErr: true}, - "can't create a cache with Shadow group": {cantChownShadowOnCreation: true, wantErr: true}, - - // tempered/permission errors - "can't open existing cache with wrong passwd permission": {changeFilePerm: cache.PasswdDB, waitForClose: true, reOpenCache: true, wantErrReopen: true}, - "can't open existing cache with wrong shadow permission": {changeFilePerm: cache.ShadowDB, waitForClose: true, reOpenCache: true, wantErrReopen: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - cacheDir := t.TempDir() - uid, gid := 4243, 4243 - // mock current user as having permission to UID/GID - if !tc.isNotRootUIDGID { - uid, gid = testutils.GetCurrentUIDGID(t) - } - - shadowGid := 424242 - if !tc.cantChownShadowOnCreation { - shadowGid = gid - } - - opts := append([]cache.Option{}, cache.WithCacheDir(cacheDir), - cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(shadowGid)) - - if tc.shadowCreationFilePerm != nil { - opts = append(opts, cache.WithShadowPermission(*tc.shadowCreationFilePerm)) - } - - if tc.waitForClose { - opts = append(opts, cache.WithTeardownDuration(time.Second*0)) - } - - c, err := cache.New(context.Background(), opts...) - if tc.wantErr { - require.Error(t, err, "New should have returned an error but hasn’t") - return - } - require.NoError(t, err, "New should have not returned an error but did") - c.Close(context.Background()) - - wantShadowMode := 2 - if tc.wantShadowMode != nil { - wantShadowMode = *tc.wantShadowMode - } - require.Equal(t, wantShadowMode, c.ShadowMode(), "Shadow attached mode is not the expected one") - - if !tc.reOpenCache { - return - } - - // Wait for all files to be closed - if tc.waitForClose { - c.WaitForCacheClosed() - } - - if tc.changeFilePerm != "" { - require.NoError(t, os.Chmod(filepath.Join(cacheDir, tc.changeFilePerm), 0400), "Setup: could not make file Read Only") - } - - c2, err := cache.New(context.Background(), opts...) - if tc.wantErrReopen { - require.Error(t, err, "New should have returned an error but hasn’t") - return - } - require.NoError(t, err, "New should have not returned an error but did") - defer c2.Close(context.Background()) - - // c and c2 should be the same object - if !tc.waitForClose { - require.Equal(t, c2, c, "cache should still be the same object") - return - } - // c2 was a complete new cache, opened only from files - require.NotEqual(t, c2, c, "cache should be reloaded and recreated from files") - }) - } -} - -func TestCloseCacheRetention(t *testing.T) { - t.Parallel() - cacheDir := t.TempDir() - - uid, gid := testutils.GetCurrentUIDGID(t) - - opts := append([]cache.Option{}, cache.WithCacheDir(cacheDir), - cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid), - cache.WithTeardownDuration(time.Second*1)) - - // First grab - c, err := cache.New(context.Background(), opts...) - require.NoError(t, err, "New should have not returned an error but did") - - cleanedUp := make(chan struct{}) - go func() { - c.WaitForCacheClosed() - close(cleanedUp) - }() - - c.Close(context.Background()) - - // Second grab - c2, err := cache.New(context.Background(), opts...) - require.NoError(t, err, "New should have not returned an error but did") - - require.Equal(t, c2, c, "cache should still be the same object") - - // Ensure the cache is not cleaned up after more than a second - select { - case <-cleanedUp: - t.Fatal("cache was collected while still having one element grabbing it") - case <-time.After(time.Second * 2): - } - - // Release second grab - c2.Close(context.Background()) - - select { - case <-time.After(time.Second * 2): - t.Fatal("cache was not collected while having no more reference grabbing it") - case <-cleanedUp: - } -} - -func TestCloseCacheDifferentOptions(t *testing.T) { - t.Parallel() - cacheDir1, cacheDir2 := t.TempDir(), t.TempDir() - - uid, gid := testutils.GetCurrentUIDGID(t) - - opts := append([]cache.Option{}, - cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid), - cache.WithTeardownDuration(time.Second*1)) - - // First element - c1, err := cache.New(context.Background(), append(opts, cache.WithCacheDir(cacheDir1))...) - require.NoError(t, err, "New should have not returned an error but did") - defer c1.Close(context.Background()) - - // Second element - c2, err := cache.New(context.Background(), append(opts, cache.WithCacheDir(cacheDir2))...) - require.NoError(t, err, "New should have not returned an error but did") - defer c2.Close(context.Background()) - - require.NotEqual(t, c1, c2, "cache should be separate elements") -} - -func TestCleanupDB(t *testing.T) { - t.Parallel() - - var zeroDuration int - offlineAuthDisabled := -1 - - tests := map[string]struct { - offlineCredentialsExpirationTime *int - - wantKeepOldUsers bool - }{ - "clean up old users": {}, - "clean up old users with default cleanup policy": {offlineCredentialsExpirationTime: &offlineAuthDisabled}, - "do not clean up anyone": {offlineCredentialsExpirationTime: &zeroDuration, wantKeepOldUsers: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := append([]cache.Option{}, cache.WithCacheDir(cacheDir), - cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)) - - if tc.offlineCredentialsExpirationTime != nil { - opts = append(opts, cache.WithOfflineCredentialsExpiration(*tc.offlineCredentialsExpirationTime)) - } - - testutils.PrepareDBsForTests(t, cacheDir, "db_with_expired_users") - - // This triggers a database cleanup if offlineCredentialsExpirationTime is not 0 - c, err := cache.New(context.Background(), opts...) - require.NoError(t, err, "Should be able to create a cache and clean up") - t.Cleanup(func() { c.Close(context.Background()) }) - - _, errUserPurged := c.GetUserByName(context.Background(), "purgeduser@domain.com") - _, errUserExpired := c.GetUserByName(context.Background(), "expireduser@domain.com") - _, errUserValid := c.GetUserByName(context.Background(), "futureuser@domain.com") - - if tc.wantKeepOldUsers { - assert.NoError(t, errUserPurged, "Very old user should not be cleaned up due to duration being 0") - assert.NoError(t, errUserExpired, "Not that old user should not be cleaned up due to duration being 0") - } else { - assert.Error(t, errUserPurged, "Very old user should be cleaned up") - assert.NoError(t, errUserExpired, "Expired user should not be cleaned up") - } - - assert.NoError(t, errUserValid, "Really recent of valid user should not be cleaned up") - }) - } -} - -func TestUpdate(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - shadowMode *int - userNames []string - - doRefreshWithShadowMode *int - - wantErr bool - wantErrRefresh bool - wantUIDCollision bool - }{ - "insert a new user": {}, - "insert 2 new users": {userNames: []string{"firstuser@domain.com", "seconduser@domain.com"}}, - "we don’t create about the user case": {userNames: []string{"MyUser"}}, - - "update an existing user should refresh password and last online login": {doRefreshWithShadowMode: &cache.ShadowRWMode}, - "collide generated uids": {userNames: []string{"firstuser@domain.com", "userfirst@domain.com"}, wantUIDCollision: true}, - - // error cases - "can't insert with shadow unavailable Only": {shadowMode: &cache.ShadowNotAvailableMode, wantErr: true}, - "can't insert with shadow Read Only": {shadowMode: &cache.ShadowROMode, wantErr: true}, - "can't update an existing user failed if no access to shadow": {doRefreshWithShadowMode: &cache.ShadowNotAvailableMode, wantErrRefresh: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - if tc.userNames == nil { - tc.userNames = []string{"myuser@domain.com"} - } - - // First, try to get user - cacheDir := t.TempDir() - - opts := []cache.Option{} - if tc.shadowMode != nil { - opts = append(opts, cache.WithShadowMode(*tc.shadowMode)) - } - c := testutils.NewCacheForTests(t, cacheDir, opts...) - - var lastUID int64 - for _, n := range tc.userNames { - start := time.Now() - err := c.Update(context.Background(), n, "my password", "/home/%f", "/bin/bash") - end := time.Now() - if tc.wantErr { - require.Error(t, err, "Update should have returned an error but hasn't") - return - } - require.NoError(t, err, "Update should not have returned an error but has") - - // Check the user exists in DB - u, err := c.GetUserByName(context.Background(), n) - require.NoError(t, err, "GetUserByName should get the user we just inserted") - - if lastUID != 0 && tc.wantUIDCollision { - assert.Equal(t, lastUID+1, u.UID, "Colliding user should have existing user UID+1") - } - lastUID = u.UID - - if tc.doRefreshWithShadowMode == nil { - continue - } - - require.True(t, testutils.TimeBetweenOrEquals(u.LastOnlineAuth, start, end), "LastOnlineAuth (%s) for the user should be between start (%s) and end (%s)", u.LastOnlineAuth.String(), start.String(), end.String()) - - firstEncryptedPass := u.ShadowPasswd - firstOnlineLoginTime := u.LastOnlineAuth - - // Close and reload a new cache object to ensure we do reload everything from files - c.Close(context.Background()) - c.WaitForCacheClosed() - c = testutils.NewCacheForTests(t, cacheDir, cache.WithShadowMode(*tc.doRefreshWithShadowMode)) - - // we need one second as we are storing an unix timestamp for last online auth - time.Sleep(time.Second) - - err = c.Update(context.Background(), n, "other password", "/home/%f", "/bin/bash") - if tc.wantErrRefresh { - require.Error(t, err, "Second update should have returned an error but hasn't") - return - } - require.NoError(t, err, "Second update should not have returned an error but has") - - // Get updated user information in DB - u, err = c.GetUserByName(context.Background(), n) - require.NoError(t, err, "GetUserByName should get the user we just inserted") - - require.NotEqual(t, u.ShadowPasswd, firstEncryptedPass, "Password should have been updated") - require.True(t, firstOnlineLoginTime.Before(u.LastOnlineAuth), "Should have updated last login time") - } - }) - } -} - -func TestCanAuthenticate(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - userPasswords map[string]string - withoutCredentialsExpiration bool - shadowMode *int - initialCache string - disabledOfflineAuth bool - - wantErr bool - }{ - "can authenticate one user": {userPasswords: map[string]string{"myuser@domain.com": "my password"}}, - "handle separately multiple users and password": {userPasswords: map[string]string{"myuser@domain.com": "my password", "otheruser@domain.com": "other password"}}, - "can authenticate even with shadow file RO": {userPasswords: map[string]string{"myuser@domain.com": "my password"}, shadowMode: &cache.ShadowROMode}, - "can authenticate even with expired user if expiration is disabled": {userPasswords: map[string]string{"expireduser@domain.com": "my password"}, withoutCredentialsExpiration: true, initialCache: "db_with_expired_users"}, - "can authenticate even with purged user if expiration is disabled": {userPasswords: map[string]string{"purgeduser@domain.com": "my password"}, withoutCredentialsExpiration: true, initialCache: "db_with_expired_users"}, - - // error cases - "error on wrong password": {userPasswords: map[string]string{"myuser@domain.com": "wrong password"}, wantErr: true}, - "error on wrong user": {userPasswords: map[string]string{"does not exist user": "my password"}, wantErr: true}, - "error on checking when can’t access shadow file": {userPasswords: map[string]string{"myuser@domain.com": "my password"}, shadowMode: &cache.ShadowNotAvailableMode, wantErr: true}, - "error on trying to authenticate expired user": {userPasswords: map[string]string{"expireduser@domain.com": "my password"}, initialCache: "db_with_expired_users", wantErr: true}, - "error on offline authentication disabled": {userPasswords: map[string]string{"myuser@domain.com": "my password"}, disabledOfflineAuth: true, wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - - opts := []cache.Option{} - if tc.shadowMode != nil { - opts = append(opts, cache.WithShadowMode(*tc.shadowMode)) - } - - initialCache := "users_in_db" - if tc.initialCache != "" { - initialCache = tc.initialCache - } - - if tc.withoutCredentialsExpiration { - opts = append(opts, cache.WithOfflineCredentialsExpiration(0)) - } - - if tc.disabledOfflineAuth { - opts = append(opts, cache.WithOfflineCredentialsExpiration(-1)) - } - - testutils.PrepareDBsForTests(t, cacheDir, initialCache, opts...) - - c := testutils.NewCacheForTests(t, cacheDir, opts...) - for username, password := range tc.userPasswords { - err := c.CanAuthenticate(context.Background(), username, password) - if tc.wantErr { - require.Error(t, err, "CanAuthenticate should return an error but hasn't") - if tc.initialCache == "db_with_expired_users" { - require.ErrorIs(t, err, cache.ErrOfflineCredentialsExpired, "CanAuthenticate should return a certain error type for expired unpurged users") - } - - if tc.disabledOfflineAuth { - require.ErrorIs(t, err, cache.ErrOfflineAuthDisabled, "CanAuthenticate should return a certain error type for disabled offline authentication") - } - return - } - assert.NoError(t, err, "CanAuthenticate should not have returned an error but has") - } - }) - } -} - -func TestUpdateUserAttribute(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - username string - attribute string - value any - - wantErr bool - }{ - "gecos": {attribute: "gecos", value: "new gecos"}, - "home": {attribute: "home", value: "new home"}, - "shell": {attribute: "shell", value: "new shell"}, - - // error cases - "unsupported attribute": {attribute: "uid", value: 1, wantErr: true}, - "unsupported value": {attribute: "gecos", value: []string{"a"}, wantErr: true}, - "nonexistent user": {username: "nonexistentuser@domain.com", attribute: "gecos", wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - if tc.username == "" { - tc.username = "myuser@domain.com" - } - - cacheDir := t.TempDir() - cacheDB := "users_in_db" - testutils.PrepareDBsForTests(t, cacheDir, cacheDB) - c := testutils.NewCacheForTests(t, cacheDir) - - var wantTime time.Time - if tc.username != "nonexistentuser@domain.com" { - user, err := c.GetUserByName(context.Background(), tc.username) - require.NoError(t, err, "Expected no error but got one.") - wantTime = user.LastOnlineAuth - } - - err := c.UpdateUserAttribute(context.Background(), tc.username, tc.attribute, tc.value) - if tc.wantErr { - require.Error(t, err, "UpdateUserAttribute should return an error but hasn't") - return - } - assert.NoError(t, err, "UpdateUserAttribute should not have returned an error but has") - - user, err := c.GetUserByName(context.Background(), tc.username) - require.NoError(t, err, "Setup: GetUserByName should not have returned an error but has") - - require.Equal(t, wantTime, user.LastOnlineAuth, "Expected last_online_auth to not change.") - - got, err := user.IniString() - require.NoError(t, err, "Setup: failed to get user representation as ini") - got = testutils.TimestampToWildcard(t, got, user.LastOnlineAuth) - - want := testutils.LoadWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "expected output to match golden file") - }) - } -} - -func TestQueryPasswdAttribute(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - username string - attribute string - - wantErr bool - }{ - "get login": {attribute: "login"}, - "get password": {attribute: "password"}, - "get uid": {attribute: "uid"}, - "get gid": {attribute: "gid"}, - "get gecos": {attribute: "gecos"}, - "get home": {attribute: "home"}, - "get shell": {attribute: "shell"}, - "get last_online_auth": {attribute: "last_online_auth"}, - - // error cases - "get nonexistent user": {username: "nouser@domain.com", attribute: "uid", wantErr: true}, - "get bad_attribute": {attribute: "bad_attribute", wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - start := time.Now() - - if tc.username == "" { - tc.username = "myuser@domain.com" - } - - cacheDir := t.TempDir() - cacheDB := "users_in_db" - testutils.PrepareDBsForTests(t, cacheDir, cacheDB) - c := testutils.NewCacheForTests(t, cacheDir) - - value, err := c.QueryPasswdAttribute(context.Background(), tc.username, tc.attribute) - if tc.wantErr { - require.Error(t, err, "QueryPasswdAttribute should return an error but hasn't") - return - } - assert.NoError(t, err, "QueryPasswdAttribute should not have returned an error but has") - - got := fmt.Sprintf("%#v\n", value) - if tc.attribute == "last_online_auth" { - i, ok := value.(int64) - require.True(t, ok, "Value must be an int64") - - gotTime := time.Unix(i, 0) - start = start.Add(-48 * time.Hour) - end := testutils.ParseTimeWildcard("RECENT_TIME") - require.True(t, testutils.TimeBetweenOrEquals(gotTime, start, end), "Got time does not match wanted time") - return - } - - want := testutils.LoadWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "expected output to match golden file") - }) - } -} - -func TestMain(m *testing.M) { - testutils.InstallUpdateFlag() - flag.Parse() - - m.Run() -} diff --git a/internal/cache/export_test.go b/internal/cache/export_test.go deleted file mode 100644 index ba8732d6..00000000 --- a/internal/cache/export_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package cache - -import ( - "io/fs" - "time" -) - -const ( - PasswdDB = passwdDB - ShadowDB = shadowDB -) - -// Those are var, as we are using their addresses. -var ( - ShadowNotAvailableMode = shadowNotAvailableMode - ShadowROMode = shadowROMode - ShadowRWMode = shadowRWMode -) - -// WithPasswdPermission allows to change default, safe, passwd filemode. -func WithPasswdPermission(perm fs.FileMode) func(o *options) error { - return func(o *options) error { - o.passwdPermission = perm - return nil - } -} - -// WithShadowPermission allows to change default, safe, shadow filemode. -func WithShadowPermission(perm fs.FileMode) func(o *options) error { - return func(o *options) error { - o.shadowPermission = perm - return nil - } -} - -func (c *Cache) WaitForCacheClosed() { - for { - openedCachesMu.Lock() - if _, ok := openedCaches[c.sig]; !ok { - openedCachesMu.Unlock() - return - } - openedCachesMu.Unlock() - time.Sleep(time.Millisecond * 100) - } -} - -func (c *Cache) ShadowMode() int { - return c.shadowMode -} diff --git a/internal/cache/groupdb_test.go b/internal/cache/groupdb_test.go deleted file mode 100644 index c38f7f80..00000000 --- a/internal/cache/groupdb_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package cache_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/cache" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -func TestGetGroupByName(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - name string - - wantErr bool - }{ - "get existing group by name": {name: "myuser@domain.com"}, - - // error cases - "error on non existing group": {name: "notexist@domain.com", wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - c := testutils.NewCacheForTests(t, cacheDir) - - g, err := c.GetGroupByName(context.Background(), tc.name) - if tc.wantErr { - require.Error(t, err, "GetGroupByName should have returned an error and hasn’t") - assert.ErrorIs(t, err, cache.ErrNoEnt, "Known error returned should be of type ErrNoEnt") - return - } - require.NoError(t, err, "GetGroupByName should not have returned an error and has") - - wantGroup := cache.GroupRecord{ - Name: tc.name, - GID: usersForTests[tc.name].uid, // GID match user with same name GID. - Password: "x", - Members: []string{tc.name}, // there is one member, which is the user with the same name. - } - - assert.Equal(t, wantGroup, g, "Group should match input") - }) - } -} - -func TestGetGroupByGID(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - gid uint - - wantErr bool - }{ - "get existing group by gid": {gid: 1929326240}, - - // error cases - "error on non existing group": {gid: 4242, wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - c := testutils.NewCacheForTests(t, cacheDir) - - g, err := c.GetGroupByGID(context.Background(), tc.gid) - if tc.wantErr { - require.Error(t, err, "GetGroupByGID should have returned an error and hasn’t") - assert.ErrorIs(t, err, cache.ErrNoEnt, "Known error returned should be of type ErrNoEnt") - return - } - require.NoError(t, err, "GetGroupByGID should not have returned an error and has") - - wantGroup := cache.GroupRecord{ - Name: usersForTestsByUID[tc.gid].name, // Name match user with same name UID/GID. - GID: int64(tc.gid), - Password: "x", - Members: []string{usersForTestsByUID[tc.gid].name}, // there is one member, which is the user with the same UID/GID.. - } - - assert.Equal(t, wantGroup, g, "Group should match input") - }) - } -} - -func TestNextGroupEntry(t *testing.T) { - t.Parallel() - - // We iterate over all entries in the DB to ensure we have listed them all - wanted := make(map[string]cache.GroupRecord) - - for n, info := range usersForTests { - wanted[n] = cache.GroupRecord{ - Name: n, // username is the group name - GID: info.uid, // GID match user with same name GID. - Password: "x", - Members: []string{n}, // there is one member, which is the user with the same name. - } - } - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - c := testutils.NewCacheForTests(t, cacheDir) - - // Iterate over all entries - numIteration := len(wanted) - for i := 0; i < numIteration; i++ { - g, err := c.NextGroupEntry(context.Background()) - require.NoError(t, err, "numIteration should initiate and returns values without any error") - - wantGroup, found := wanted[g.Name] - require.True(t, found, "%v should be in %v", g.Name, wanted) - assert.Equal(t, wantGroup, g, "Group should match what we inserted") - } - - // Final iteration: should return ENoEnt to ends it - g, err := c.NextGroupEntry(context.Background()) - require.ErrorIs(t, err, cache.ErrNoEnt, "final iteration should return ENOENT, but we got %v", g) -} - -func TestNextGroupEntryNoGroup(t *testing.T) { - t.Parallel() - - c := testutils.NewCacheForTests(t, t.TempDir()) - g, err := c.NextGroupEntry(context.Background()) - require.ErrorIs(t, err, cache.ErrNoEnt, "first and final iteration should return ENOENT, but we got %v", g) -} - -func TestNextGroupCloseBeforeIterationEnds(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - c := testutils.NewCacheForTests(t, cacheDir) - - _, err := c.NextGroupEntry(context.Background()) - require.NoError(t, err, "NextGroupEntry should initiate and returns values without any error") - - // This closes underlying iterator - err = c.CloseGroupIterator(context.Background()) - require.NoError(t, err, "No error should occur when closing the iterator in tests") - - // Trying to iterate for all entries - numIteration := len(usersForTests) - for i := 0; i < numIteration; i++ { - _, err := c.NextGroupEntry(context.Background()) - require.NoError(t, err, "NextGroupEntry should initiate and returns values without any error") - } - - // Final iteration: should return ENoEnt to ends it - g, err := c.NextGroupEntry(context.Background()) - require.ErrorIs(t, err, cache.ErrNoEnt, "final iteration should return ENOENT, but we got %v", g) - - c.Close(context.Background()) - c.WaitForCacheClosed() -} diff --git a/internal/cache/helper_test.go b/internal/cache/helper_test.go deleted file mode 100644 index a2a1ca9d..00000000 --- a/internal/cache/helper_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package cache_test - -type userInfos struct { - name string - uid int64 - password string - gecos string -} - -var ( - usersForTests = map[string]userInfos{ - "myuser@domain.com": {"myuser@domain.com", 1929326240, "my password", "My User"}, - "otheruser@domain.com": {"otheruser@domain.com", 165119648, "other password", "Other User"}, - "user@otherdomain.com": {"user@otherdomain.com", 165119649, "other user domain password", "User"}, - } - usersForTestsByUID = make(map[uint]userInfos) -) - -func init() { - // populate usersForTestByUid - for _, info := range usersForTests { - usersForTestsByUID[uint(info.uid)] = info - } -} diff --git a/internal/cache/internal_test.go b/internal/cache/internal_test.go deleted file mode 100644 index 91c89687..00000000 --- a/internal/cache/internal_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package cache - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestParseHomeDir(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - path string - username string - - want string - wantErr bool - }{ - "handle %f": {path: "/home/%f", want: "/home/user1@test.com"}, - "handle %u": {path: "/home/%u", want: "/home/user1"}, - "handle %U": {path: "/home/%U", want: "/home/42"}, - "handle %d": {path: "/home/%d", want: "/home/test.com"}, - "handle %f without domain attached": {username: "userWithoutDomain", path: "/home/%f", want: "/home/userWithoutDomain"}, - "handle %l": {path: "/home/%l", want: "/home/u"}, - "handle %%": {path: "/home/user%%test.com", want: "/home/user%test.com"}, - "pattern after string": {path: "/home/whyDoThis%u", want: "/home/whyDoThisuser1"}, - - // multiple patterns - "multiple consecutive patterns": {path: "/home/%d/%l/%u%U", want: "/home/test.com/u/user142"}, - "multiple patterns separated with characters": {path: "/home/%u-%d", want: "/home/user1-test.com"}, - - // special cases - "full path without modifier is returned as is": {path: "/home/username", want: "/home/username"}, - - // error cases - "error out on path with invalid pattern": {path: "/home/%a", wantErr: true}, - } - - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - username, id := "user1@test.com", "42" - if tc.username != "" { - username = tc.username - } - - got, err := parseHomeDir(context.Background(), tc.path, username, id) - if tc.wantErr { - require.Error(t, err, "parseHomeDir should have returned an error but did not") - return - } - require.NoError(t, err, "parseHomeDir should have not have errored out but did") - require.Equal(t, tc.want, got, "Should get expected parsed path but did not") - }) - } -} diff --git a/internal/cache/passwddb_test.go b/internal/cache/passwddb_test.go deleted file mode 100644 index 68d4b20c..00000000 --- a/internal/cache/passwddb_test.go +++ /dev/null @@ -1,223 +0,0 @@ -package cache_test - -import ( - "context" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/cache" - "github.com/ubuntu/aad-auth/internal/testutils" - "golang.org/x/crypto/bcrypt" -) - -func TestGetUserByName(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - name string - shadowMode int - - wantErr bool - }{ - "get existing user by name with encrypted password": {name: "myuser@domain.com", shadowMode: cache.ShadowROMode}, - "have access to encrypted password in RW too": {name: "myuser@domain.com", shadowMode: cache.ShadowRWMode}, - "no encrypted password": {name: "myuser@domain.com", shadowMode: cache.ShadowNotAvailableMode}, - - // error cases - "error on non existing user": {name: "notexist@domain.com", wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db", cache.WithShadowMode(tc.shadowMode)) - - c := testutils.NewCacheForTests(t, cacheDir, cache.WithShadowMode(tc.shadowMode)) - - u, err := c.GetUserByName(context.Background(), tc.name) - if tc.wantErr { - require.Error(t, err, "GetUserByName should have returned an error and hasn’t") - assert.ErrorIs(t, err, cache.ErrNoEnt, "Known error returned should be of type ErrNoEnt") - return - } - require.NoError(t, err, "GetUserByName should not have returned an error and has") - - // Handle dynamic fields - // Checks if the lastOnlineAuth value was loaded properly. - assert.False(t, u.LastOnlineAuth.IsZero(), "Last Online should not be zero.") - u.LastOnlineAuth = time.Unix(0, 0) - - // Validate password - if tc.shadowMode > 0 { - err := bcrypt.CompareHashAndPassword([]byte(u.ShadowPasswd), []byte(usersForTests[tc.name].password)) - assert.NoError(t, err, "Encrypted passwords should match the insertion") - u.ShadowPasswd = "" - } - - wantUser := cache.UserRecord{ - Name: tc.name, - Passwd: "x", - UID: usersForTests[tc.name].uid, - GID: usersForTests[tc.name].uid, // GID match UID - Gecos: usersForTests[tc.name].gecos, - Home: filepath.Join("/home", tc.name), // Default (fallback) home - Shell: "/bin/bash", // Default (fallback) home - ShadowPasswd: "", // already hanlded - LastOnlineAuth: time.Unix(0, 0), // we will match it manually - } - - assert.Equal(t, wantUser, u, "User should match input") - }) - } -} - -func TestGetUserByUID(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - uid uint - shadowMode int - - wantErr bool - }{ - "get existing user by uid with encrypted password": {uid: 1929326240, shadowMode: cache.ShadowROMode}, - "have access to encrypted password in RW too": {uid: 1929326240, shadowMode: cache.ShadowRWMode}, - "no encrypted password": {uid: 1929326240, shadowMode: cache.ShadowNotAvailableMode}, - - // error cases - "error on non existing user": {uid: 4242, wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db", cache.WithShadowMode(tc.shadowMode)) - - c := testutils.NewCacheForTests(t, cacheDir, cache.WithShadowMode(tc.shadowMode)) - - u, err := c.GetUserByUID(context.Background(), tc.uid) - if tc.wantErr { - require.Error(t, err, "GetUserByName should have returned an error and hasn’t") - assert.ErrorIs(t, err, cache.ErrNoEnt, "Known error returned should be of type ErrNoEnt") - return - } - require.NoError(t, err, "GetUserByName should not have returned an error and has") - - // Handle dynamic fields - // Checks if the lastOnlineAuth value was loaded properly. - assert.False(t, u.LastOnlineAuth.IsZero(), "Last Online should not be zero.") - u.LastOnlineAuth = time.Unix(0, 0) - - // Validate password - if tc.shadowMode > 0 { - err := bcrypt.CompareHashAndPassword([]byte(u.ShadowPasswd), []byte(usersForTestsByUID[tc.uid].password)) - assert.NoError(t, err, "Encrypted passwords should match the insertion") - u.ShadowPasswd = "" - } - - wantUser := cache.UserRecord{ - Name: usersForTestsByUID[tc.uid].name, - Passwd: "x", - UID: int64(tc.uid), - GID: int64(tc.uid), // GID match UID - Gecos: usersForTestsByUID[tc.uid].gecos, - Home: filepath.Join("/home", usersForTestsByUID[tc.uid].name), // Default (fallback) home - Shell: "/bin/bash", // Default (fallback) home - ShadowPasswd: "", // already hanlded - LastOnlineAuth: time.Unix(0, 0), // we will match it manually - } - - assert.Equal(t, wantUser, u, "User should match input") - }) - } -} - -func TestNextPasswdEntry(t *testing.T) { - t.Parallel() - - // We iterate over all entries in the DB to ensure we have listed them all - wanted := make(map[string]cache.UserRecord) - - for n, info := range usersForTests { - wanted[n] = cache.UserRecord{ - Name: n, - Passwd: "x", - UID: info.uid, - GID: info.uid, // GID match UID - Gecos: info.gecos, - Home: filepath.Join("/home", n), // Default (fallback) home - Shell: "/bin/bash", // Default (fallback) home - ShadowPasswd: "", // we don’t have access to shadow password in this mode. - LastOnlineAuth: time.Unix(0, 0), // we will match it manually - } - } - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - c := testutils.NewCacheForTests(t, cacheDir) - - // Iterate over all entries - numIteration := len(wanted) - for i := 0; i < numIteration; i++ { - u, err := c.NextPasswdEntry(context.Background()) - require.NoError(t, err, "NextPasswdEntry should initiate and returns values without any error") - - // Checks if the lastOnlineAuth value was loaded properly. - assert.False(t, u.LastOnlineAuth.IsZero(), "Last Online should not be zero.") - u.LastOnlineAuth = time.Unix(0, 0) - - wantUser, found := wanted[u.Name] - require.True(t, found, "%v should be in %v", u.Name, wanted) - assert.Equal(t, wantUser, u, "User should match what we inserted") - } - - // Final iteration: should return ENoEnt to ends it - u, err := c.NextPasswdEntry(context.Background()) - require.ErrorIs(t, err, cache.ErrNoEnt, "final iteration should return ENOENT, but we got %v", u) -} - -func TestNextPasswdEntryNoUser(t *testing.T) { - t.Parallel() - - c := testutils.NewCacheForTests(t, t.TempDir()) - u, err := c.NextPasswdEntry(context.Background()) - require.ErrorIs(t, err, cache.ErrNoEnt, "first and final iteration should return ENOENT, but we got %v", u) -} - -func TestNextPasswdCloseBeforeIterationEnds(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - c := testutils.NewCacheForTests(t, cacheDir) - - _, err := c.NextPasswdEntry(context.Background()) - require.NoError(t, err, "NextPasswdEntry should initiate and returns values without any error") - - // This closes underlying iterator - err = c.ClosePasswdIterator(context.Background()) - require.NoError(t, err, "No error should occur when closing the iterator in tests") - - // Trying to iterate for all entries - numIteration := len(usersForTests) - for i := 0; i < numIteration; i++ { - _, err := c.NextPasswdEntry(context.Background()) - require.NoError(t, err, "NextPasswdEntry should initiate and returns values without any error") - } - - // Final iteration: should return ENoEnt to ends it - u, err := c.NextPasswdEntry(context.Background()) - require.ErrorIs(t, err, cache.ErrNoEnt, "final iteration should return ENOENT, but we got %v", u) - - c.Close(context.Background()) - c.WaitForCacheClosed() -} diff --git a/internal/cache/shadowdb_test.go b/internal/cache/shadowdb_test.go deleted file mode 100644 index cca732e6..00000000 --- a/internal/cache/shadowdb_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package cache_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/cache" - "github.com/ubuntu/aad-auth/internal/testutils" - "golang.org/x/crypto/bcrypt" -) - -func TestGetShadowByName(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - name string - shadowMode int - - wantErr bool - wantErrENOENT bool - }{ - "get existing shadow information for user by name with encrypted password": {name: "myuser@domain.com", shadowMode: cache.ShadowROMode}, - "have access to encrypted password in RW too": {name: "myuser@domain.com", shadowMode: cache.ShadowRWMode}, - - // error cases - "error on non existing user shadow": {name: "notexist@domain.com", shadowMode: cache.ShadowROMode, wantErr: true, wantErrENOENT: true}, - "error on no access to shadow file": {name: "myuser@domain.com", shadowMode: cache.ShadowNotAvailableMode, wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db", cache.WithShadowMode(tc.shadowMode)) - - c := testutils.NewCacheForTests(t, cacheDir, cache.WithShadowMode(tc.shadowMode)) - - s, err := c.GetShadowByName(context.Background(), tc.name) - if tc.wantErr { - require.Error(t, err, "GetShadowByName should have returned an error and hasn’t") - if tc.wantErrENOENT { - assert.ErrorIs(t, err, cache.ErrNoEnt, "Known error returned should be of type ErrNoEnt") - } - return - } - require.NoError(t, err, "GetShadowByName should not have returned an error and has") - - // Validate password (dynamic field) - err = bcrypt.CompareHashAndPassword([]byte(s.Password), []byte(usersForTests[tc.name].password)) - assert.NoError(t, err, "Encrypted passwords should match the insertion") - s.Password = "" - - wantUser := cache.ShadowRecord{ - Name: tc.name, - Password: "", - LastPwdChange: -1, - MaxPwdAge: -1, - PwdWarnPeriod: -1, - PwdInactivity: -1, - MinPwdAge: -1, - ExpirationDate: -1, - } - - assert.Equal(t, wantUser, s, "User should match input") - }) - } -} - -func TestNextShadowEntry(t *testing.T) { - t.Parallel() - - // We iterate over all entries in the DB to ensure we have listed them all - wanted := make(map[string]cache.ShadowRecord) - - for n := range usersForTests { - wanted[n] = cache.ShadowRecord{ - Name: n, - Password: "", - LastPwdChange: -1, - MaxPwdAge: -1, - PwdWarnPeriod: -1, - PwdInactivity: -1, - MinPwdAge: -1, - ExpirationDate: -1, - } - } - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - c := testutils.NewCacheForTests(t, cacheDir) - - // Iterate over all entries - numIteration := len(wanted) - for i := 0; i < numIteration; i++ { - s, err := c.NextShadowEntry(context.Background()) - require.NoError(t, err, "NextShadowEntry should initiate and returns values without any error") - - wantUser, found := wanted[s.Name] - require.True(t, found, "%v should be in %v", s.Name, wanted) - - // Validate password (dynamic field) - err = bcrypt.CompareHashAndPassword([]byte(s.Password), []byte(usersForTests[s.Name].password)) - assert.NoError(t, err, "Encrypted passwords should match the insertion") - s.Password = "" - - assert.Equal(t, wantUser, s, "Shadow should match the user what we inserted") - } - - // Final iteration: should return ENoEnt to ends it - u, err := c.NextShadowEntry(context.Background()) - require.ErrorIs(t, err, cache.ErrNoEnt, "final iteration should return ENOENT, but we got %v", u) -} - -func TestNextShadowEntryNoShadow(t *testing.T) { - t.Parallel() - - c := testutils.NewCacheForTests(t, t.TempDir()) - s, err := c.NextShadowEntry(context.Background()) - require.ErrorIs(t, err, cache.ErrNoEnt, "first and final iteration should return ENOENT, but we got %v", s) -} - -func TestNextShadowCloseBeforeIterationEnds(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - c := testutils.NewCacheForTests(t, cacheDir) - - _, err := c.NextShadowEntry(context.Background()) - require.NoError(t, err, "NextShadowEntry should initiate and returns values without any error") - - // This closes underlying iterator - err = c.CloseShadowIterator(context.Background()) - require.NoError(t, err, "No error should occur when closing the iterator in tests") - - // Trying to iterate for all entries - numIteration := len(usersForTests) - for i := 0; i < numIteration; i++ { - _, err := c.NextShadowEntry(context.Background()) - require.NoError(t, err, "NextShadowEntry should initiate and returns values without any error") - } - - // Final iteration: should return ENoEnt to ends it - s, err := c.NextShadowEntry(context.Background()) - require.ErrorIs(t, err, cache.ErrNoEnt, "final iteration should return ENOENT, but we got %v", s) - - c.Close(context.Background()) - c.WaitForCacheClosed() -} diff --git a/internal/config/config.go b/internal/config/config.go index ea8f49fd..4e7caf17 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,6 +26,7 @@ type AAD struct { OfflineCredentialsExpiration *int `ini:"offline_credentials_expiration"` HomeDirPattern string `ini:"homedir"` Shell string `ini:"shell"` + LoginDomain string `ini:"login_domain"` } // ToIni reflects the configuration values to an ini.File representation. diff --git a/internal/config/config_test.go b/internal/config/config_test.go deleted file mode 100644 index 022691a6..00000000 --- a/internal/config/config_test.go +++ /dev/null @@ -1,208 +0,0 @@ -package config_test - -import ( - "context" - "flag" - "path/filepath" - "strings" - "testing" - - "github.com/go-ini/ini" - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/config" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -func TestLoadConfig(t *testing.T) { - t.Parallel() - testFilesPath := filepath.Join("testdata", "TestLoadConfig") - - tests := map[string]struct { - aadConfigPath string - addUserPath string - domain string - - wantErr bool - }{ - - // All values - "aad.conf, all values, no domain": { - aadConfigPath: "aad-all_values-no_domain.conf", - }, - "aad.conf, all values, with domain": { - aadConfigPath: "aad-all_values-with_domain.conf", - }, - "aad.conf, all values, mismatch domain": { - aadConfigPath: "aad-all_values-with_domain.conf", - domain: "doesNotExist.com", - }, - "aad.conf, all values, only in domain": { - aadConfigPath: "aad-all_values_only_in_domain.conf", - }, - - // Missing values in domain - "aad.conf with missing 'homedirpattern' value in domain": { - aadConfigPath: "aad-missing_homedirpattern-domain.conf", - }, - "aad.conf with missing 'shell' value in domain": { - aadConfigPath: "aad-missing_shell-domain.conf", - }, - "aad.conf with missing 'homedirpattern' and 'shell' values in domain": { - aadConfigPath: "aad-missing_homedirpattern_and_shell-domain.conf", - }, - "aad.conf with missing 'offline_credentials_expiration' value in domain": { - aadConfigPath: "aad-missing_expiration-domain.conf", - }, - "aad.conf with missing required 'tenant_id' value in domain": { - aadConfigPath: "aad-missing_tenantId-domain.conf", - }, - "aad.conf with missing required 'app_id' value in domain": { - aadConfigPath: "aad-missing_appId-domain.conf", - }, - - // Missing values in file - "aad.conf with missing 'homedirpattern'": { - aadConfigPath: "aad-missing_homedirpattern.conf", - }, - "aad.conf with missing 'shell'": { - aadConfigPath: "aad-missing_shell.conf", - }, - "aad.conf with missing 'homedirpattern' and 'shell'": { - aadConfigPath: "aad-missing_homedirpattern_and_shell.conf", - }, - "aad.conf with missing 'offline_credentials_expiration'": { - aadConfigPath: "aad-missing_expiration.conf", - }, - - // Values only in domain - "aad.conf with 'homedirpattern' only in domain": { - aadConfigPath: "aad-homedirpattern_only_in_domain.conf", - }, - "add.conf with 'shell' only in domain": { - aadConfigPath: "aad-shell_only_in_domain.conf", - }, - "aad.conf with 'homedirpattern' and 'shell' only in domain": { - aadConfigPath: "aad-homedirpattern_and_shell_only_in_domain.conf", - }, - "aad.conf with 'offline_credentials_expiration' only in domain": { - aadConfigPath: "aad-expiration_only_in_domain.conf", - }, - "aad.conf with 'tenant_id' only in domain": { - aadConfigPath: "aad-tenantId_only_in_domain.conf", - }, - "aad.conf with 'app_id' only in domain": { - aadConfigPath: "aad-appId_only_in_domain.conf", - }, - - // Special Cases - "aad.conf with missing 'homedir' and 'shell' values, but valid adduser.conf": { - aadConfigPath: "aad-missing_homedirpattern_and_shell.conf", - addUserPath: "valid_adduser.conf", - }, - "aad.conf with missing 'homedir' and 'shell' values and wrong adduser.conf": { - aadConfigPath: "aad-missing_homedirpattern_and_shell.conf", - addUserPath: "doesnotexist.conf", - }, - - // Error cases - "aad.conf does not exist": { - aadConfigPath: "doestnotexists.conf", - wantErr: true, - }, - "aad.conf missing 'tenant_id' value": { - aadConfigPath: "aad-missing_tenantId.conf", - wantErr: true, - }, - "aad.conf missing 'app_id' value": { - aadConfigPath: "aad-missing_appId.conf", - wantErr: true, - }, - "aad.conf with invalid 'offline_credentials_expiration' value": { - aadConfigPath: "aad-invalid_expiration.conf", - wantErr: true, - }, - "aad.conf with invalid 'offline_credentials_expiration' value in domain": { - aadConfigPath: "aad-invalid_expiration-domain.conf", - wantErr: true, - }, - } - - for name, tc := range tests { - def := strings.ToLower(strings.ReplaceAll(name, " ", "_")) - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - tc.aadConfigPath = filepath.Join(testFilesPath, tc.aadConfigPath) - - domain := "domain.com" - if tc.domain != "" { - domain = tc.domain - } - - got, err := config.Load(context.Background(), tc.aadConfigPath, domain, config.WithAddUserConfPath(tc.addUserPath)) - if tc.wantErr { - require.Error(t, err, "LoadConfig should have failed, but didn't") - return - } - require.NoError(t, err, "LoadConfig failed when it shouldn't") - - goldenPath := filepath.Join(testFilesPath, "golden", def) - want := testutils.LoadYAMLWithUpdateFromGolden(t, got, testutils.WithGoldPath(goldenPath)) - require.Equal(t, want, got, "Got config and expected config are different") - }) - } -} - -func TestToIni(t *testing.T) { - t.Parallel() - - expiration := 90 - aad := config.AAD{TenantID: "tenantID", AppID: "appID", HomeDirPattern: "homeDirPattern", Shell: "shell", OfflineCredentialsExpiration: &expiration} - - want := ini.Empty() - err := ini.ReflectFrom(want, &aad) - require.NoError(t, err, "Setup: failed to reflect config to ini") - - got, err := aad.ToIni() - require.NoError(t, err, "Setup: failed to reflect config to ini") - - require.Equal(t, want, got, "Got and expected ini files are different") -} - -func TestValidate(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - configFile string - wantErr bool - }{ - "valid config, default domain": {configFile: "valid.conf"}, - "valid config, multiple domains": {configFile: "valid-multiple-domains.conf"}, - - // Error cases - "invalid config, default domain": {configFile: "invalid.conf", wantErr: true}, - "invalid config, commented values": {configFile: "invalid-commented.conf", wantErr: true}, - "invalid config, multiple domains": {configFile: "invalid-multiple-domains.conf", wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - configFile := filepath.Join("testdata", tc.configFile) - err := config.Validate(context.Background(), configFile) - if tc.wantErr { - require.Error(t, err, "Validate should have failed, but didn't") - return - } - require.NoError(t, err, "Validate failed but shouldn't have") - }) - } -} - -func TestMain(m *testing.M) { - testutils.InstallUpdateFlag() - flag.Parse() - m.Run() -} diff --git a/internal/config/export_test.go b/internal/config/export_test.go deleted file mode 100644 index e9d30e2d..00000000 --- a/internal/config/export_test.go +++ /dev/null @@ -1,8 +0,0 @@ -package config - -// WithAddUserConfPath overrides /etc/adduser.conf path. -func WithAddUserConfPath(path string) Option { - return func(o *options) { - o.addUserConfPath = path - } -} diff --git a/internal/config/internal_test.go b/internal/config/internal_test.go deleted file mode 100644 index fbb467a9..00000000 --- a/internal/config/internal_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package config - -import ( - "context" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestLoadDefaultHomeAndShell(t *testing.T) { - testFilesPath := filepath.Join("testdata", "TestLoadDefaultHomeAndShell") - t.Parallel() - - tests := map[string]struct { - path string - - wantHome string - wantShell string - }{ - "file with both home and shell": { - path: "adduser-both-values.conf", - wantHome: "/home/users/%f", - wantShell: "/bin/fish", - }, - - "file with only dhome": { - path: "adduser-dhome-only.conf", - wantHome: "/home/users/%f", - wantShell: "", - }, - "file with only dshell": { - path: "adduser-dshell-only.conf", - wantHome: "", - wantShell: "/bin/fish", - }, - "file with no values": { - path: "adduser-commented.conf", - wantHome: "", - wantShell: "", - }, - - // Special cases - "file does not exists returns empty values": { - path: "/foo/doesnotexists.conf", - wantHome: "", - wantShell: "", - }, - "empty path to adduser.conf returns empty values": { - path: "", - wantHome: "", - wantShell: "", - }, - } - - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - path := filepath.Join(testFilesPath, tc.path) - if tc.path == "" { - path = "" - } - - home, shell := loadDefaultHomeAndShell(context.Background(), path) - require.Equal(t, tc.wantHome, home, "Got expected homedir") - require.Equal(t, tc.wantShell, shell, "Got expected shell") - }) - } -} diff --git a/internal/i18n/export_test.go b/internal/i18n/export_test.go deleted file mode 100644 index c70b8f9c..00000000 --- a/internal/i18n/export_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package i18n - -// WithLocaleDir enables overriding locale directory in tests. -func WithLocaleDir(path string) func(l *i18n) { - return func(l *i18n) { - l.localeDir = path - } -} - -// WithLoc enables overriding loc settings in tests. -func WithLoc(loc string) func(l *i18n) { - return func(l *i18n) { - l.loc = loc - } -} - -// ResetGlobals resets G and GN to their empty func. -func ResetGlobals() { - G = func(msgid string) string { return msgid } - NG = func(msgid string, msgidPlural string, n uint32) string { return msgid } -} diff --git a/internal/i18n/i18n_test.go b/internal/i18n/i18n_test.go deleted file mode 100644 index 5d48dc31..00000000 --- a/internal/i18n/i18n_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package i18n_test - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/ubuntu/aad-auth/internal/i18n" -) - -const ( - defaultDomain = "aad-test" - defaultLoc = "en_DK" - secondaryLoc = "en" -) - -var ( - defaultPo = fmt.Sprintf(` -msgid "" -msgstr "" -"Project-Id-Version: %s\n" -"Report-Msgid-Bugs-To: aad-devel@lists.ubuntu.com\n" -"POT-Creation-Date: 2013-10-05 14:08+0200\n" -"Language: %s\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=n != 1;>\n" -msgid "plural_1" -msgid_plural "plural_2" -msgstr[0] "translated plural_1" -msgstr[1] "translated plural_2" -msgid "singular" -msgstr "translated singular" -`, defaultDomain, defaultLoc) - - localePo = map[string]string{ - defaultLoc: defaultPo, - secondaryLoc: strings.ReplaceAll( - strings.ReplaceAll(defaultPo, defaultLoc, secondaryLoc), - "translated singular", "secondary translated singular"), - } -) - -func TestTranslations(t *testing.T) { - defaultLocaleDir := filepath.Join(t.TempDir(), "locale") - compileMoFiles(t, defaultLocaleDir) - - tests := map[string]struct { - // default is singular/translated singular - text []string - want string - - localeDir string - lcmessages string // lcmessages can be set to "-" to ensure it's empty - lang string - domain string - loc string // loc can be set to "-" to ensure it's empty - - rename map[string]string - noinit bool - }{ - "One text elem, prefer en_DK over en": {}, - "Multiple text elems": {text: []string{"plural_1", "plural_2"}, want: "translated plural_1"}, - - // Locale preferences - "en_DK@ is en_DK": {loc: defaultLoc + "@foo"}, - "en_DK. is en_DK": {loc: defaultLoc + ".foo"}, - "Fallback to en if en_DK isn't present": { - want: "secondary translated singular", - rename: map[string]string{filepath.Join(defaultLocaleDir, "en_DK"): filepath.Join(defaultLocaleDir, "other")}, - }, - "Prefer locale-langpack to locale": { - want: "secondary translated singular", - rename: map[string]string{ - filepath.Join(defaultLocaleDir, "en"): filepath.Join(strings.ReplaceAll(defaultLocaleDir, "locale", "locale-langpack"), "en_DK"), - }, - }, - - "No loc prefers LC_MESSAGES first": {lcmessages: "en_DK", loc: "-"}, - "No loc fallbacks to LANG if no LC_MESSAGES": {lang: "en_DK", loc: "-", lcmessages: "-"}, - - "Untranslated elem": {text: []string{"untranslated"}, want: "untranslated"}, - "Missing locale": {loc: "doesntexists", want: "singular"}, - "Missing domain": {domain: "doesntexists", want: "singular"}, - "Invalid locale directory": {localeDir: "/doesntexists", want: "singular"}, - "Init wasn't ran": {noinit: true, want: "singular"}, - } - - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - // We can't run those subtests in parallel as we want defer of global functions to end once all subtests are. - - // As we can't run those tests in parallel, we are doing the file switches and env changes to test priorities - // in subtests here and reset globals. - defer i18n.ResetGlobals() - if tc.text == nil { - tc.text = []string{"singular"} - } - if tc.want == "" { - tc.want = "translated singular" - } - if tc.localeDir == "" { - tc.localeDir = defaultLocaleDir - } - if tc.lcmessages == "" { - tc.lcmessages = "FR_fr" - } else if tc.lcmessages == "-" { - tc.lcmessages = "" - } - t.Setenv("LC_MESSAGES", tc.lcmessages) - if tc.lang == "" { - tc.lang = "FR_fr" - } - t.Setenv("LANG", tc.lang) - if tc.loc == "" { - tc.loc = defaultLoc - } else if tc.loc == "-" { - tc.loc = "" - } - if tc.domain == "" { - tc.domain = defaultDomain - } - if tc.rename != nil { - for old, new := range tc.rename { - renameElem(t, old, new) - } - } - - if !tc.noinit { - i18n.InitI18nDomain(tc.domain, i18n.WithLocaleDir(tc.localeDir), i18n.WithLoc(tc.loc)) - } - switch len(tc.text) { - case 1: - assert.Equal(t, tc.want, i18n.G(tc.text[0])) - case 2: - assert.Equal(t, tc.want, i18n.NG(tc.text[0], tc.text[1], 1)) - default: - t.Fatalf("unexpected case: %v", tc.text) - } - }) - } -} - -func compileMoFiles(t *testing.T, localeDir string) { - t.Helper() - - for loc, poContent := range localePo { - fullLocaleDir := filepath.Join(localeDir, loc, "LC_MESSAGES") - if err := os.MkdirAll(fullLocaleDir, 0750); err != nil { - t.Fatalf("couldn't create temporary directory %q: %v", fullLocaleDir, err) - } - - po := filepath.Join(localeDir, defaultDomain+".po") - mo := filepath.Join(fullLocaleDir, defaultDomain+".mo") - - if err := os.WriteFile(po, []byte(poContent), 0600); err != nil { - t.Fatalf("couldn't write po file: %v", err) - } - - // nolint:gosec // G204 false positive, the binary is hardcoded - cmd := exec.Command("msgfmt", po, "--output-file", mo) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - t.Fatalf("couldn't compile %q to %q: %v", po, mo, err) - } - } -} - -// renameElem rename old file to new. -// The rename is reverted when the test ends. -func renameElem(t *testing.T, old, new string) { - t.Helper() - - if err := os.MkdirAll(filepath.Dir(new), 0750); err != nil { - t.Fatalf("couldn't create parent directory %q to be renamed: %v", new, err) - } - if err := os.Rename(old, new); err != nil { - t.Fatalf("couldn't rename %q to %q: %v", old, new, err) - } - t.Cleanup(func() { - if err := os.Rename(new, old); err != nil { - t.Fatalf("couldn't restore %q to %q: %v", new, old, err) - } - }) -} diff --git a/internal/logger/internal_test.go b/internal/logger/internal_test.go deleted file mode 100644 index f152c07a..00000000 --- a/internal/logger/internal_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package logger - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNormalizeMsg(t *testing.T) { - t.Parallel() - tests := map[string]struct { - format string - a string - want string - }{ - "msg will always end by EOL": {format: "My %s", a: "message", want: "My message\n"}, - "msg with EOL is unchanged": {format: "My %s with EOL\n", a: "message", want: "My message with EOL\n"}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - got := normalizeMsg(tc.format, tc.a) - require.Equal(t, tc.want, got, "got expected message with EOL") - }) - } -} diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go deleted file mode 100644 index 6743a216..00000000 --- a/internal/logger/logger_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package logger_test - -import ( - "context" - "fmt" - "io" - "log" - "strings" - "testing" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/logger" -) - -func TestCtxWithLogger(t *testing.T) { - ctx := logger.CtxWithLogger(context.Background(), &dummyLogger{}) - err := logger.CloseLoggerFromContext(ctx) - require.NoError(t, err, "CloseLoggerFromContext should not error as attached to context and closing logger works") -} - -func TestCloseLoggerFromContextNoLogger(t *testing.T) { - err := logger.CloseLoggerFromContext(context.Background()) - require.Error(t, err, "CloseLoggerFromContext should error as context has no logger attached") -} - -func TestLogging(t *testing.T) { - tests := map[string]struct { - logFn func(ctx context.Context, format string, a ...any) - hasLoggerInContext bool - - wantLoggerPrint string - }{ - "debug, with logger": {logFn: logger.Debug, hasLoggerInContext: true, wantLoggerPrint: "DEBUG: my log message"}, - "debug, on stderr": {logFn: logger.Debug, hasLoggerInContext: false, wantLoggerPrint: "DEBUG: my log message"}, - - "info, with logger": {logFn: logger.Info, hasLoggerInContext: true, wantLoggerPrint: "INFO: my log message"}, - "info, on stderr": {logFn: logger.Info, hasLoggerInContext: false, wantLoggerPrint: "INFO: my log message"}, - - "warn, with logger": {logFn: logger.Warn, hasLoggerInContext: true, wantLoggerPrint: "WARNING: my log message"}, - "warn, on stderr": {logFn: logger.Warn, hasLoggerInContext: false, wantLoggerPrint: "WARNING: my log message"}, - - "err, with logger": {logFn: logger.Err, hasLoggerInContext: true, wantLoggerPrint: "ERROR: my log message"}, - "err, on stderr": {logFn: logger.Err, hasLoggerInContext: false, wantLoggerPrint: "ERROR: my log message"}, - - "crit, with logger": {logFn: logger.Crit, hasLoggerInContext: true, wantLoggerPrint: "CRITICAL: my log message"}, - "crit, on stderr": {logFn: logger.Crit, hasLoggerInContext: false, wantLoggerPrint: "CRITICAL: my log message"}, - - // special cases - "message already have an EOL": {logFn: logger.Debug, hasLoggerInContext: true, wantLoggerPrint: "DEBUG: my log message\n"}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - ctx := context.Background() - - done := make(chan struct{}) - l := &dummyLogger{} - var contentLog []byte - - r, w := io.Pipe() - if tc.hasLoggerInContext { - ctx = logger.CtxWithLogger(ctx, l) - defer logger.CloseLoggerFromContext(ctx) - close(done) - } else { - origOut := log.Writer() - log.SetOutput(w) - defer log.SetOutput(origOut) - go func() { - defer close(done) - var err error - contentLog, err = io.ReadAll(r) - require.NoError(t, err, "read from redirected output should not fail") - }() - } - - tc.logFn(ctx, "my %s message", "log") - - w.Close() - <-done - - content := l.content - if !tc.hasLoggerInContext { - content = string(contentLog) - } - require.Contains(t, content, tc.wantLoggerPrint, "Logged expected content") - require.True(t, strings.HasSuffix(content, "\n"), "Logged message always ends with EOL") - }) - } -} - -type dummyLogger struct { - content string -} - -func (d *dummyLogger) Debug(format string, a ...any) { - d.content = fmt.Sprintf("DEBUG: "+format, a...) -} -func (d *dummyLogger) Info(format string, a ...any) { - d.content = fmt.Sprintf("INFO: "+format, a...) -} -func (d *dummyLogger) Warn(format string, a ...any) { - d.content = fmt.Sprintf("WARNING: "+format, a...) -} -func (d *dummyLogger) Err(format string, a ...any) { - d.content = fmt.Sprintf("ERROR: "+format, a...) -} -func (d *dummyLogger) Crit(format string, a ...any) { - d.content = fmt.Sprintf("CRITICAL: "+format, a...) -} -func (d dummyLogger) Close() error { - return nil -} diff --git a/internal/logger/logrus_test.go b/internal/logger/logrus_test.go deleted file mode 100644 index 106df2bc..00000000 --- a/internal/logger/logrus_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package logger_test - -import ( - "context" - "io" - "testing" - - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/logger" -) - -func TestCtxWithLogrusLogger(t *testing.T) { - ctx := logger.CtxWithLogger(context.Background(), &logger.LogrusLogger{}) - err := logger.CloseLoggerFromContext(ctx) - require.NoError(t, err, "CloseLoggerFromContext should not error as attached to context and closing logger works") -} - -func TestLogrusLogging(t *testing.T) { - tests := map[string]struct { - logFn func(ctx context.Context, format string, a ...any) - loglevel int - - wantLoggerPrint string - }{ - "debug with default verbosity": {logFn: logger.Debug}, - "debug with verbosity": {logFn: logger.Debug, wantLoggerPrint: "DEBUG: my log message", loglevel: 2}, - "debug with caller": {logFn: logger.Debug, wantLoggerPrint: "DEBUG:github.com/ubuntu/aad-auth/internal/logger_test.TestLogrusLogging.func1:63: my log message", loglevel: 3}, - "info with default verbosity": {logFn: logger.Info}, - "info with verbosity": {logFn: logger.Info, wantLoggerPrint: "INFO: my log message", loglevel: 1}, - "warning": {logFn: logger.Warn, wantLoggerPrint: "WARNING: my log message"}, - "error": {logFn: logger.Err, wantLoggerPrint: "ERROR: my log message"}, - - // special cases - "message already have an EOL": {logFn: logger.Warn, wantLoggerPrint: "WARNING: my log message\n"}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - ctx := context.Background() - - done := make(chan struct{}) - l := &logger.LogrusLogger{FieldLogger: logrus.StandardLogger()} - logrus.SetFormatter(&logger.LogrusFormatter{}) - logger.SetVerboseMode(tc.loglevel) - defer logrus.SetReportCaller(false) - - var contentLog []byte - - r, w := io.Pipe() - ctx = logger.CtxWithLogger(ctx, l) - defer logger.CloseLoggerFromContext(ctx) - origOut := logrus.StandardLogger().Out - logrus.StandardLogger().SetOutput(w) - defer logrus.StandardLogger().SetOutput(origOut) - go func() { - defer close(done) - var err error - contentLog, err = io.ReadAll(r) - require.NoError(t, err, "read from redirected output should not fail") - }() - - tc.logFn(ctx, "my %s message", "log") - - w.Close() - <-done - - content := string(contentLog) - require.Contains(t, content, tc.wantLoggerPrint, "Logged expected content") - }) - } -} diff --git a/internal/nss/errors_test.go b/internal/nss/errors_test.go deleted file mode 100644 index bbad7668..00000000 --- a/internal/nss/errors_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package nss_test - -import ( - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/cache" - "github.com/ubuntu/aad-auth/internal/nss" -) - -func TestConvertErr(t *testing.T) { - t.Parallel() - - errMsg := "My error" - tests := map[string]struct { - origErr error - - wantErrorType error - }{ - "wrapped ErrTryAgainEAgain error retains original error type": {origErr: fmt.Errorf("%s. Wrapped: %w", errMsg, nss.ErrTryAgainEAgain), wantErrorType: nss.ErrTryAgainEAgain}, - "wrapped ErrTryAgainERange error retains original error type": {origErr: fmt.Errorf("%s. Wrapped: %w", errMsg, nss.ErrTryAgainERange), wantErrorType: nss.ErrTryAgainERange}, - "wrapped ErrUnavailableENoEnt error retains original error type": {origErr: fmt.Errorf("%s. Wrapped: %w", errMsg, nss.ErrUnavailableENoEnt), wantErrorType: nss.ErrUnavailableENoEnt}, - "wrapped ErrNotFoundENoEnt error retains original error type": {origErr: fmt.Errorf("%s. Wrapped: %w", errMsg, nss.ErrNotFoundENoEnt), wantErrorType: nss.ErrNotFoundENoEnt}, - "wrapped ErrNotFoundSuccess error retains original error type": {origErr: fmt.Errorf("%s. Wrapped: %w", errMsg, nss.ErrNotFoundSuccess), wantErrorType: nss.ErrNotFoundSuccess}, - - // special cases - "wrapped ErrNoEnt error is converted to ErrNotFoundENoEnt": {origErr: fmt.Errorf("%s. Wrapped: %w", errMsg, cache.ErrNoEnt), wantErrorType: nss.ErrNotFoundENoEnt}, - "random error is converted to ErrUnavailableENoEnt": {origErr: errors.New(errMsg), wantErrorType: nss.ErrUnavailableENoEnt}, - "nil error should return nil": {origErr: nil, wantErrorType: nil}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - err := nss.ConvertErr(tc.origErr) - - if tc.wantErrorType == nil { - require.NoError(t, err, "Nil input should return nil output") - return - } - - assert.Contains(t, err.Error(), errMsg, "Should containing original error message") - require.True(t, errors.Is(err, tc.wantErrorType), "error (%v) should be of type: %v", err, tc.wantErrorType) - }) - } -} diff --git a/internal/nss/export_test.go b/internal/nss/export_test.go deleted file mode 100644 index 5cd0e9bb..00000000 --- a/internal/nss/export_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package nss - -const ( - // NssLogEnv is the env variable name to force debug. - NssLogEnv = nssLogEnv -) - -// WithDebug forces debug mode, whatever environment variable is set. -func WithDebug() Option { - return func(o *options) { - o.debug = true - } -} - -// WithLogWriter override the syslog writer we assign. -func WithLogWriter(w logWriter) Option { - return func(o *options) { - o.writer = w - } -} diff --git a/internal/nss/group/export_test.go b/internal/nss/group/export_test.go deleted file mode 100644 index 45c6e797..00000000 --- a/internal/nss/group/export_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package group - -import ( - "fmt" - - "gopkg.in/yaml.v3" -) - -type publicGroup struct { - Name string - Passwd string - GID uint - Members []string -} - -// MarshalYAML use a public object to Marhsal to a yaml format. -func (g Group) MarshalYAML() (interface{}, error) { - return publicGroup{ - Name: g.name, - Passwd: g.passwd, - GID: g.gid, - Members: g.members, - }, nil -} - -// UnmarshalYAML use a public object to Unmarhsal to. -func (g *Group) UnmarshalYAML(value *yaml.Node) error { - o := publicGroup{} - if err := value.Decode(&o); err != nil { - return err - } - - *g = Group{ - name: o.Name, - passwd: o.Passwd, - gid: o.GID, - members: o.Members, - } - return nil -} - -// NewTestGroup return a new Group entry for tests. -func NewTestGroup(nMembers int) Group { - members := make([]string, 0, nMembers) - - members = append(members, "testusername@domain.com") - for i := 1; i < nMembers; i++ { - members = append(members, fmt.Sprintf("testusername-%d@domain.com", i)) - } - - return Group{ - name: "testusername@domain.com", - passwd: "x", - gid: 2345, - members: members, - } -} diff --git a/internal/nss/group/group_test.go b/internal/nss/group/group_test.go deleted file mode 100644 index f9c4bb5b..00000000 --- a/internal/nss/group/group_test.go +++ /dev/null @@ -1,264 +0,0 @@ -package group_test - -import ( - "context" - "flag" - "testing" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/cache" - "github.com/ubuntu/aad-auth/internal/nss" - "github.com/ubuntu/aad-auth/internal/nss/group" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -//nolint:dupl // TestNewByName and TestNewByGID have similar code that triggers dupl, despite being different. -func TestNewByName(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - name string - failingCache bool - - wantErrType error - }{ - "get existing group by name": {name: "myuser@domain.com"}, - - // error cases - "error on non existing group": {name: "notexists@domain.com", wantErrType: nss.ErrNotFoundENoEnt}, - "shadow group is ignored, as not part of aad": {name: "shadow", wantErrType: nss.ErrNotFoundENoEnt}, - "error on cache not available": {name: "myuser@domain.com", failingCache: true, wantErrType: nss.ErrUnavailableENoEnt}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - if tc.failingCache { - opts = append(opts, cache.WithRootUID(4242)) - } - - got, err := group.NewByName(context.Background(), tc.name, opts...) - if tc.wantErrType != nil { - require.Error(t, err, "NewByName should have returned an error and hasn’t") - require.ErrorIs(t, err, tc.wantErrType, "NewByName has not returned expected error type") - return - } - require.NoError(t, err, "NewByName should not have returned an error and has") - - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "Group object is the expected one") - }) - } -} - -//nolint:dupl // TestNewByName and TestNewByGID have similar code that triggers dupl, despite being different. -func TestNewByGID(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - gid uint - failingCache bool - - wantErrType error - }{ - "get existing group by gid": {gid: 1929326240}, - - // error cases - "error on non existing group": {gid: 4242, wantErrType: nss.ErrNotFoundENoEnt}, - "error on cache not available": {gid: 1929326240, failingCache: true, wantErrType: nss.ErrUnavailableENoEnt}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - if tc.failingCache { - opts = append(opts, cache.WithRootUID(4242)) - } - - got, err := group.NewByGID(context.Background(), tc.gid, opts...) - if tc.wantErrType != nil { - require.Error(t, err, "NewByGID should have returned an error and hasn’t") - require.ErrorIs(t, err, tc.wantErrType, "NewByGID has not returned expected error type") - return - } - require.NoError(t, err, "NewByGID should not have returned an error and has") - - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "Group object is the expected one") - }) - } -} - -func TestNextEntry(t *testing.T) { - tests := map[string]struct { - numNextIteration int - hasNoGroup bool - noIterationInit bool - - wantEndErrType error - }{ - "get all groups": {numNextIteration: 3, wantEndErrType: nss.ErrNotFoundENoEnt}, - "no group in db does not fail": {hasNoGroup: true, numNextIteration: 0, wantEndErrType: nss.ErrNotFoundENoEnt}, - "partial iteration then ends works": {numNextIteration: 1, wantEndErrType: nil}, - - // error cases - "error on iteration not being initialized first": {noIterationInit: true, numNextIteration: 0, wantEndErrType: nss.ErrUnavailableENoEnt}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - cacheDir := t.TempDir() - if !tc.hasNoGroup { - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - } - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - - if !tc.noIterationInit { - err := group.StartEntryIteration(context.Background(), opts...) - require.NoError(t, err, "StartEntryIteration should succeed") - defer group.EndEntryIteration(context.Background()) - } - - var got []group.Group - for i := 0; i < tc.numNextIteration; i++ { - p, err := group.NextEntry(context.Background()) - require.NoError(t, err, "Should return groups without any errors") - got = append(got, p) - } - _, err := group.NextEntry(context.Background()) - if tc.wantEndErrType != nil { - require.ErrorIs(t, err, tc.wantEndErrType, "Should return ENOENT once there is no more groups") - } else { - require.NoError(t, err, "We iterated over an existing group and shouldn’t get an error") - } - - if tc.noIterationInit { - return // no need to deserialize anything - } - - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - if len(want) == 0 { - want = nil - } - require.Equal(t, want, got, "Should list requested groups only") - }) - } -} - -func TestStartEndEntryIteration(t *testing.T) { - tests := map[string]struct { - alreadyIterationInProgress bool - noStartIteration bool - cacheOpenError bool - - wantStartIterationErr bool - }{ - "can start and end iteration": {}, - "no error when ending a not started iteration": {noStartIteration: true}, - - // error cases - "error in start when iteration already in progress": {alreadyIterationInProgress: true, wantStartIterationErr: true}, - "error in start when error on opening cache": {cacheOpenError: true, wantStartIterationErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - cacheDir := t.TempDir() - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - - if tc.alreadyIterationInProgress { - err := group.StartEntryIteration(context.Background(), opts...) - require.NoError(t, err, "Setup: first startEntryIteration should have failed by hasn’t") - defer group.EndEntryIteration(context.Background()) - } - - if tc.cacheOpenError { - opts = append(opts, cache.WithRootUID(4242)) - } - - if !tc.noStartIteration { - err := group.StartEntryIteration(context.Background(), opts...) - if tc.wantStartIterationErr { - require.Error(t, err, "StartEntryIteration should have failed by hasn’t") - require.ErrorIs(t, err, nss.ErrUnavailableENoEnt, "Error should be of type Unavailable") - return - } - require.NoError(t, err, "StartEntryIteration should have failed by hasn’t") - } - - err := group.EndEntryIteration(context.Background()) - require.NoError(t, err, "EndEntryIteration should never fail but had") - }) - } -} - -func TestRestartIterationWithoutEndingPreviousOne(t *testing.T) { - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - - // First iteration group - err := group.StartEntryIteration(context.Background(), opts...) - require.NoError(t, err, "StartEntryIteration should succeed") - defer group.EndEntryIteration(context.Background()) // in case of an error in the middle of the test. No-op otherwise - - p, err := group.NextEntry(context.Background()) - require.NoError(t, err, "Should return first group without any errors") - require.NotNil(t, p, "Should return first group") - - err = group.EndEntryIteration(context.Background()) - require.NoError(t, err, "EndEntryIteration while iterating should work") - - // Second iteration group - err = group.StartEntryIteration(context.Background(), opts...) - require.NoError(t, err, "restart a second entry iteration should succeed") - defer group.EndEntryIteration(context.Background()) - - var got []group.Group - for i := 0; i < 3; i++ { - p, err := group.NextEntry(context.Background()) - require.NoError(t, err, "Should return groups without any errors") - got = append(got, p) - } - _, err = group.NextEntry(context.Background()) - require.ErrorIs(t, err, nss.ErrNotFoundENoEnt, "Should return ENOENT once there is no more groups") - - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - if len(want) == 0 { - want = nil - } - require.Equal(t, want, got, "Should list all groups from the start") -} - -func TestString(t *testing.T) { - g := group.NewTestGroup(2) - - got := g.String() - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "Group strings must match") -} - -func TestMain(m *testing.M) { - testutils.InstallUpdateFlag() - flag.Parse() - - m.Run() -} diff --git a/internal/nss/group/util_c_test.go b/internal/nss/group/util_c_test.go deleted file mode 100644 index de5cafb2..00000000 --- a/internal/nss/group/util_c_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package group_test - -import ( - "testing" - "unsafe" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/nss" - "github.com/ubuntu/aad-auth/internal/nss/group" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -func TestToCgroup(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - bufsize int - nMembers int - - wantErr bool - }{ - "can convert group to C group": {bufsize: 100000}, - "can convert group with five members to C group": {bufsize: 100000, nMembers: 5}, - "can't allocate with buffer too small": {bufsize: 5, wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - if tc.nMembers == 0 { - tc.nMembers = 1 - } - g := group.NewTestGroup(tc.nMembers) - - got := testutils.NewCGroup() - buf := (*group.CChar)(testutils.AllocCBuffer(t, testutils.CSizeT(tc.bufsize))) - //#nosec:G103 - We need to use unsafe.Pointer because Go thinks that testutils._Ctype_struct_group is different than group._Ctype_struct_group - err := g.ToCgroup(group.CGroup(unsafe.Pointer(got)), buf, group.CSizeT(tc.bufsize)) - if tc.wantErr { - require.Error(t, err, "ToCgroup should have returned an error but hasn't") - require.ErrorIs(t, err, nss.ErrTryAgainERange, "Error should be of type ErrTryAgainERange") - return - } - require.NoError(t, err, "ToCgroup should have not returned an error but hasn’t") - - grpGot := got.ToPublicCGroup(tc.nMembers) - want := testutils.LoadYAMLWithUpdateFromGolden(t, grpGot) - - require.Equal(t, want, grpGot, "Should have C group with expected fields content") - }) - } -} diff --git a/internal/nss/logger_test.go b/internal/nss/logger_test.go deleted file mode 100644 index 5100699c..00000000 --- a/internal/nss/logger_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package nss_test - -import ( - "context" - "fmt" - "os" - "testing" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/logger" - "github.com/ubuntu/aad-auth/internal/nss" -) - -func TestCtxWithSyslogLogger(t *testing.T) { - t.Parallel() - l := &dummyLogger{} - ctx := nss.CtxWithSyslogLogger(context.Background(), nss.WithLogWriter(l)) - err := logger.CloseLoggerFromContext(ctx) - require.NoError(t, err, "CloseLoggerFromContext should not error as attached to context and closing logger works") -} - -func TestCtxWithSyslogLoggerDebugWithEnVariable(t *testing.T) { - tests := map[string]struct { - nssLogEnv string - - want string - }{ - "log debug message when in debug mode": {nssLogEnv: "1", want: "DEBUG: nss_aad: NSS AAD DEBUG enabled\n"}, - "log to stderr when set to": {nssLogEnv: "stderr", want: ""}, - "don't log anything when not in debug": {want: ""}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - if tc.nssLogEnv != "" { - err := os.Setenv(nss.NssLogEnv, tc.nssLogEnv) - require.NoError(t, err, "Setup: can’t set environment variable for debug log") - defer func() { - err := os.Unsetenv(nss.NssLogEnv) - require.NoError(t, err, "Teardown: can’t restore by unsetting environment variable for debug log") - }() - } - - l := &dummyLogger{} - ctx := nss.CtxWithSyslogLogger(context.Background(), nss.WithLogWriter(l)) - defer logger.CloseLoggerFromContext(ctx) - - if tc.nssLogEnv == "stderr" { - v := ctx.Value("loggerCtxKey") - require.Empty(t, v, "Context should not have a logger attached") - } - - require.Equal(t, tc.want, l.content, "Should log expected debug message or nothing if nssLogEnv is not set") - }) - } -} - -func TestLogging(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - logFn func(ctx context.Context, format string, a ...any) - forceDebug bool - - wantLoggerPrint string - }{ - "debug": {logFn: logger.Debug, forceDebug: true, wantLoggerPrint: "DEBUG: nss_aad: my log message\n"}, - "info": {logFn: logger.Info, forceDebug: true, wantLoggerPrint: "INFO: nss_aad: my log message\n"}, - "warn": {logFn: logger.Warn, forceDebug: true, wantLoggerPrint: "WARNING: nss_aad: my log message\n"}, - "err": {logFn: logger.Err, forceDebug: true, wantLoggerPrint: "ERROR: nss_aad: my log message\n"}, - "crit": {logFn: logger.Crit, forceDebug: true, wantLoggerPrint: "CRITICAL: nss_aad: my log message\n"}, - - // log level - "debug is not printed with default log level": {logFn: logger.Debug, forceDebug: false, wantLoggerPrint: ""}, - "info message is printed with default log level": {logFn: logger.Info, forceDebug: false, wantLoggerPrint: "INFO: nss_aad: my log message\n"}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - l := &dummyLogger{} - opts := []nss.Option{nss.WithLogWriter(l)} - if tc.forceDebug { - opts = append(opts, nss.WithDebug()) - } - - ctx := nss.CtxWithSyslogLogger(context.Background(), opts...) - defer func() { logger.CloseLoggerFromContext(ctx) }() - - tc.logFn(ctx, "my %s message", "log") - - content := l.content - if tc.wantLoggerPrint == "" { - require.Empty(t, content, "Should have not logged anything") - return - } - require.Contains(t, content, tc.wantLoggerPrint, "Logged expected content") - }) - } -} - -type dummyLogger struct { - content string -} - -func (d *dummyLogger) Debug(msg string) error { - d.content = fmt.Sprintf("DEBUG: %s", msg) - return nil -} -func (d *dummyLogger) Info(msg string) error { - d.content = fmt.Sprintf("INFO: %s", msg) - return nil -} -func (d *dummyLogger) Warning(msg string) error { - d.content = fmt.Sprintf("WARNING: %s", msg) - return nil -} -func (d *dummyLogger) Err(msg string) error { - d.content = fmt.Sprintf("ERROR: %s", msg) - return nil -} -func (d *dummyLogger) Crit(msg string) error { - d.content = fmt.Sprintf("CRITICAL: %s", msg) - return nil -} -func (d dummyLogger) Close() error { - return nil -} diff --git a/internal/nss/passwd/export_test.go b/internal/nss/passwd/export_test.go deleted file mode 100644 index 61f4c25b..00000000 --- a/internal/nss/passwd/export_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package passwd - -import ( - "gopkg.in/yaml.v3" -) - -type publicPasswd struct { - Name string - Passwd string - UID uint - GID uint - Gecos string - Dir string - Shell string -} - -// MarshalYAML use a public object to Marhsal to a yaml format. -func (p Passwd) MarshalYAML() (interface{}, error) { - return publicPasswd{ - Name: p.name, - Passwd: p.passwd, - UID: p.uid, - GID: p.gid, - Gecos: p.gecos, - Dir: p.dir, - Shell: p.shell, - }, nil -} - -// UnmarshalYAML use a public object to Unmarhsal to. -func (p *Passwd) UnmarshalYAML(value *yaml.Node) error { - o := publicPasswd{} - if err := value.Decode(&o); err != nil { - return err - } - - *p = Passwd{ - name: o.Name, - passwd: o.Passwd, - uid: o.UID, - gid: o.GID, - gecos: o.Gecos, - dir: o.Dir, - shell: o.Shell, - } - return nil -} - -// NewTestPasswd return a new passwd entry for tests. -func NewTestPasswd() Passwd { - return Passwd{ - name: "testusername@domain.com", - passwd: "x", - uid: 1234, - gid: 2345, - gecos: "", - dir: "/home/testusername@domain.com", - shell: "/bin/bash", - } -} diff --git a/internal/nss/passwd/passwd_test.go b/internal/nss/passwd/passwd_test.go deleted file mode 100644 index 6e9e5ea0..00000000 --- a/internal/nss/passwd/passwd_test.go +++ /dev/null @@ -1,263 +0,0 @@ -package passwd_test - -import ( - "context" - "flag" - "testing" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/cache" - "github.com/ubuntu/aad-auth/internal/nss" - "github.com/ubuntu/aad-auth/internal/nss/passwd" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -//nolint:dupl // TestNewByName and TestNewByGID have similar code that triggers dupl, despite being different. -func TestNewByName(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - name string - failingCache bool - - wantErrType error - }{ - "get existing user by name": {name: "myuser@domain.com"}, - - // error cases - "error on non existing user": {name: "notexists@domain.com", wantErrType: nss.ErrNotFoundENoEnt}, - "error on cache not available": {name: "myuser@domain.com", failingCache: true, wantErrType: nss.ErrUnavailableENoEnt}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - if tc.failingCache { - opts = append(opts, cache.WithRootUID(4242)) - } - - got, err := passwd.NewByName(context.Background(), tc.name, opts...) - if tc.wantErrType != nil { - require.Error(t, err, "NewByName should have returned an error and hasn’t") - require.ErrorIs(t, err, tc.wantErrType, "NewByName has not returned expected error type") - return - } - require.NoError(t, err, "NewByName should not have returned an error and has") - - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "Passwd object is the expected one") - }) - } -} - -//nolint:dupl // TestNewByName and TestNewByUID have similar code that triggers dupl, despite being different. -func TestNewByUID(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - uid uint - failingCache bool - - wantErrType error - }{ - "get existing user by uid": {uid: 1929326240}, - - // error cases - "error on non existing user": {uid: 4242, wantErrType: nss.ErrNotFoundENoEnt}, - "error on cache not available": {uid: 1929326240, failingCache: true, wantErrType: nss.ErrUnavailableENoEnt}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - if tc.failingCache { - opts = append(opts, cache.WithRootUID(4242)) - } - - got, err := passwd.NewByUID(context.Background(), tc.uid, opts...) - if tc.wantErrType != nil { - require.Error(t, err, "NewByUID should have returned an error and hasn’t") - require.ErrorIs(t, err, tc.wantErrType, "NewByUID has not returned expected error type") - return - } - require.NoError(t, err, "NewByUID should not have returned an error and has") - - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "Passwd object is the expected one") - }) - } -} - -func TestNextEntry(t *testing.T) { - tests := map[string]struct { - numNextIteration int - hasNoUser bool - noIterationInit bool - - wantEndErrType error - }{ - "get all users": {numNextIteration: 3, wantEndErrType: nss.ErrNotFoundENoEnt}, - "no user in db does not fail": {hasNoUser: true, numNextIteration: 0, wantEndErrType: nss.ErrNotFoundENoEnt}, - "partial iteration then ends works": {numNextIteration: 1, wantEndErrType: nil}, - - // error cases - "error on iteration not being initialized first": {noIterationInit: true, numNextIteration: 0, wantEndErrType: nss.ErrUnavailableENoEnt}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - cacheDir := t.TempDir() - if !tc.hasNoUser { - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - } - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - - if !tc.noIterationInit { - err := passwd.StartEntryIteration(context.Background(), opts...) - require.NoError(t, err, "StartEntryIteration should succeed") - defer passwd.EndEntryIteration(context.Background()) - } - - var got []passwd.Passwd - for i := 0; i < tc.numNextIteration; i++ { - p, err := passwd.NextEntry(context.Background()) - require.NoError(t, err, "Should return users without any errors") - got = append(got, p) - } - _, err := passwd.NextEntry(context.Background()) - if tc.wantEndErrType != nil { - require.ErrorIs(t, err, tc.wantEndErrType, "Should return ENOENT once there is no more users") - } else { - require.NoError(t, err, "We iterated over an existing user and shouldn’t get an error") - } - - if tc.noIterationInit { - return // no need to deserialize anything - } - - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - if len(want) == 0 { - want = nil - } - require.Equal(t, want, got, "Should list requested users only") - }) - } -} - -func TestStartEndEntryIteration(t *testing.T) { - tests := map[string]struct { - alreadyIterationInProgress bool - noStartIteration bool - cacheOpenError bool - - wantStartIterationErr bool - }{ - "can start and end iteration": {}, - "no error when ending a not started iteration": {noStartIteration: true}, - - // error cases - "error in start when iteration already in progress": {alreadyIterationInProgress: true, wantStartIterationErr: true}, - "error in start when error on opening cache": {cacheOpenError: true, wantStartIterationErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - cacheDir := t.TempDir() - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - - if tc.alreadyIterationInProgress { - err := passwd.StartEntryIteration(context.Background(), opts...) - require.NoError(t, err, "Setup: first startEntryIteration should have failed by hasn’t") - defer passwd.EndEntryIteration(context.Background()) - } - - if tc.cacheOpenError { - opts = append(opts, cache.WithRootUID(4242)) - } - - if !tc.noStartIteration { - err := passwd.StartEntryIteration(context.Background(), opts...) - if tc.wantStartIterationErr { - require.Error(t, err, "StartEntryIteration should have failed by hasn’t") - require.ErrorIs(t, err, nss.ErrUnavailableENoEnt, "Error should be of type Unavailable") - return - } - require.NoError(t, err, "StartEntryIteration should have failed by hasn’t") - } - - err := passwd.EndEntryIteration(context.Background()) - require.NoError(t, err, "EndEntryIteration should never fail but had") - }) - } -} - -func TestRestartIterationWithoutEndingPreviousOne(t *testing.T) { - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - - // First iteration group - err := passwd.StartEntryIteration(context.Background(), opts...) - require.NoError(t, err, "StartEntryIteration should succeed") - defer passwd.EndEntryIteration(context.Background()) // in case of an error in the middle of the test. No-op otherwise - - p, err := passwd.NextEntry(context.Background()) - require.NoError(t, err, "Should return first user without any errors") - require.NotNil(t, p, "Should return first user") - - err = passwd.EndEntryIteration(context.Background()) - require.NoError(t, err, "EndEntryIteration while iterating should work") - - // Second iteration group - err = passwd.StartEntryIteration(context.Background(), opts...) - require.NoError(t, err, "restart a second entry iteration should succeed") - defer passwd.EndEntryIteration(context.Background()) - - var got []passwd.Passwd - for i := 0; i < 3; i++ { - p, err := passwd.NextEntry(context.Background()) - require.NoError(t, err, "Should return users without any errors") - got = append(got, p) - } - _, err = passwd.NextEntry(context.Background()) - require.ErrorIs(t, err, nss.ErrNotFoundENoEnt, "Should return ENOENT once there is no more users") - - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - if len(want) == 0 { - want = nil - } - require.Equal(t, want, got, "Should list all users from the start") -} - -func TestString(t *testing.T) { - p := passwd.NewTestPasswd() - - got := p.String() - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "Passwd strings must match") -} - -func TestMain(m *testing.M) { - testutils.InstallUpdateFlag() - flag.Parse() - - m.Run() -} diff --git a/internal/nss/passwd/util_c_test.go b/internal/nss/passwd/util_c_test.go deleted file mode 100644 index 2a2ebd54..00000000 --- a/internal/nss/passwd/util_c_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package passwd_test - -import ( - "testing" - "unsafe" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/nss" - "github.com/ubuntu/aad-auth/internal/nss/passwd" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -func TestToCpasswd(t *testing.T) { - t.Parallel() - p := passwd.NewTestPasswd() - - tests := map[string]struct { - bufsize int - - wantErr bool - }{ - "can convert to C pwd": {bufsize: 100000}, - - "can't allocate with buffer too small": {bufsize: 5, wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - got := testutils.NewCPasswd() - buf := (*passwd.CChar)(testutils.AllocCBuffer(t, testutils.CSizeT(tc.bufsize))) - //#nosec:G103 - We need to use unsafe.Pointer because Go thinks that testutils._Ctype_struct_passwd is different than passwd._Ctype_struct_passwd - err := p.ToCpasswd(passwd.CPasswd(unsafe.Pointer(got)), buf, passwd.CSizeT(tc.bufsize)) - if tc.wantErr { - require.Error(t, err, "ToCpasswd should have returned an error but hasn't") - require.ErrorIs(t, err, nss.ErrTryAgainERange, "Error should be of type ErrTryAgainERange") - return - } - require.NoError(t, err, "ToCpasswd should have not returned an error but hasn’t") - - pwdGot := got.ToPublicCPasswd() - want := testutils.LoadYAMLWithUpdateFromGolden(t, pwdGot) - - require.Equal(t, want, pwdGot, "Should have C pwd with expected fields content") - }) - } -} diff --git a/internal/nss/shadow/export_test.go b/internal/nss/shadow/export_test.go deleted file mode 100644 index 08e72f10..00000000 --- a/internal/nss/shadow/export_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package shadow - -import ( - "gopkg.in/yaml.v3" -) - -type publicShadow struct { - Name string - Passwd string - Lstchg int - Min int - Max int - Warn int - Inact int - Expire int -} - -// MarshalYAML use a public object to Marhsal to a yaml format. -func (s Shadow) MarshalYAML() (interface{}, error) { - return publicShadow{ - Name: s.name, - Passwd: s.passwd, - Lstchg: s.lstchg, - Min: s.min, - Max: s.max, - Warn: s.warn, - Inact: s.inact, - Expire: s.expire, - }, nil -} - -// UnmarshalYAML use a public object to Unmarhsal to. -func (s *Shadow) UnmarshalYAML(value *yaml.Node) error { - o := publicShadow{} - if err := value.Decode(&o); err != nil { - return err - } - - *s = Shadow{ - name: o.Name, - passwd: o.Passwd, - lstchg: o.Lstchg, - min: o.Min, - max: o.Max, - warn: o.Warn, - inact: o.Inact, - expire: o.Expire, - } - return nil -} - -// NewTestShadow return a new Shadow entry for tests. -func NewTestShadow() Shadow { - return Shadow{ - name: "testusername@domain.com", - passwd: "*", - lstchg: -1, - min: -1, - max: -1, - warn: -1, - expire: -1, - inact: -1, - } -} diff --git a/internal/nss/shadow/shadow_test.go b/internal/nss/shadow/shadow_test.go deleted file mode 100644 index 2d885d29..00000000 --- a/internal/nss/shadow/shadow_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package shadow_test - -import ( - "context" - "flag" - "testing" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/cache" - "github.com/ubuntu/aad-auth/internal/nss" - "github.com/ubuntu/aad-auth/internal/nss/shadow" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -func TestNewByName(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - name string - failingCache bool - - wantErrType error - }{ - "get existing user by name": {name: "myuser@domain.com"}, - - // error cases - "error on non existing user": {name: "notexists@domain.com", wantErrType: nss.ErrNotFoundENoEnt}, - "error on cache not available": {name: "myuser@domain.com", failingCache: true, wantErrType: nss.ErrUnavailableENoEnt}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - if tc.failingCache { - opts = append(opts, cache.WithRootUID(4242)) - } - - got, err := shadow.NewByName(context.Background(), tc.name, opts...) - if tc.wantErrType != nil { - require.Error(t, err, "NewByName should have returned an error and hasn’t") - require.ErrorIs(t, err, tc.wantErrType, "NewByName has not returned expected error type") - return - } - require.NoError(t, err, "NewByName should not have returned an error and has") - - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "Shadow object is the expected one") - }) - } -} - -func TestNextEntry(t *testing.T) { - tests := map[string]struct { - numNextIteration int - hasNoUser bool - noIterationInit bool - - wantEndErrType error - }{ - "get all users": {numNextIteration: 3, wantEndErrType: nss.ErrNotFoundENoEnt}, - "no user in db does not fail": {hasNoUser: true, numNextIteration: 0, wantEndErrType: nss.ErrNotFoundENoEnt}, - "partial iteration then ends works": {numNextIteration: 1, wantEndErrType: nil}, - - // error cases - "error on iteration not being initialized first": {noIterationInit: true, numNextIteration: 0, wantEndErrType: nss.ErrUnavailableENoEnt}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - cacheDir := t.TempDir() - if !tc.hasNoUser { - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - } - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - - if !tc.noIterationInit { - err := shadow.StartEntryIteration(context.Background(), opts...) - require.NoError(t, err, "StartEntryIteration should succeed") - defer shadow.EndEntryIteration(context.Background()) - } - - var got []shadow.Shadow - for i := 0; i < tc.numNextIteration; i++ { - p, err := shadow.NextEntry(context.Background()) - require.NoError(t, err, "Should return users without any errors") - got = append(got, p) - } - _, err := shadow.NextEntry(context.Background()) - if tc.wantEndErrType != nil { - require.ErrorIs(t, err, tc.wantEndErrType, "Should return ENOENT once there is no more users") - } else { - require.NoError(t, err, "We iterated over an existing user and shouldn’t get an error") - } - - if tc.noIterationInit { - return // no need to deserialize anything - } - - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - if len(want) == 0 { - want = nil - } - require.Equal(t, want, got, "Should list requested users only") - }) - } -} - -func TestStartEndEntryIteration(t *testing.T) { - tests := map[string]struct { - alreadyIterationInProgress bool - noStartIteration bool - cacheOpenError bool - - wantStartIterationErr bool - }{ - "can start and end iteration": {}, - "no error when ending a not started iteration": {noStartIteration: true}, - - // error cases - "error in start when iteration already in progress": {alreadyIterationInProgress: true, wantStartIterationErr: true}, - "error in start when error on opening cache": {cacheOpenError: true, wantStartIterationErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - cacheDir := t.TempDir() - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - - if tc.alreadyIterationInProgress { - err := shadow.StartEntryIteration(context.Background(), opts...) - require.NoError(t, err, "Setup: first startEntryIteration should have failed by hasn’t") - defer shadow.EndEntryIteration(context.Background()) - } - - if tc.cacheOpenError { - opts = append(opts, cache.WithRootUID(4242)) - } - - if !tc.noStartIteration { - err := shadow.StartEntryIteration(context.Background(), opts...) - if tc.wantStartIterationErr { - require.Error(t, err, "StartEntryIteration should have failed by hasn’t") - require.ErrorIs(t, err, nss.ErrUnavailableENoEnt, "Error should be of type Unavailable") - return - } - require.NoError(t, err, "StartEntryIteration should have failed by hasn’t") - } - - err := shadow.EndEntryIteration(context.Background()) - require.NoError(t, err, "EndEntryIteration should never fail but had") - }) - } -} - -func TestRestartIterationWithoutEndingPreviousOne(t *testing.T) { - cacheDir := t.TempDir() - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - - uid, gid := testutils.GetCurrentUIDGID(t) - opts := []cache.Option{cache.WithCacheDir(cacheDir), cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - - // First iteration group - err := shadow.StartEntryIteration(context.Background(), opts...) - require.NoError(t, err, "StartEntryIteration should succeed") - defer shadow.EndEntryIteration(context.Background()) // in case of an error in the middle of the test. No-op otherwise - - p, err := shadow.NextEntry(context.Background()) - require.NoError(t, err, "Should return first user without any errors") - require.NotNil(t, p, "Should return first user") - - err = shadow.EndEntryIteration(context.Background()) - require.NoError(t, err, "EndEntryIteration while iterating should work") - - // Second iteration group - err = shadow.StartEntryIteration(context.Background(), opts...) - require.NoError(t, err, "restart a second entry iteration should succeed") - defer shadow.EndEntryIteration(context.Background()) - - var got []shadow.Shadow - for i := 0; i < 3; i++ { - p, err := shadow.NextEntry(context.Background()) - require.NoError(t, err, "Should return users without any errors") - got = append(got, p) - } - _, err = shadow.NextEntry(context.Background()) - require.ErrorIs(t, err, nss.ErrNotFoundENoEnt, "Should return ENOENT once there is no more users") - - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - if len(want) == 0 { - want = nil - } - require.Equal(t, want, got, "Should list all users from the start") -} - -func TestString(t *testing.T) { - s := shadow.NewTestShadow() - - got := s.String() - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "Shadow strings must match") -} - -func TestMain(m *testing.M) { - testutils.InstallUpdateFlag() - flag.Parse() - - m.Run() -} diff --git a/internal/nss/shadow/util_c_test.go b/internal/nss/shadow/util_c_test.go deleted file mode 100644 index b70ea1d9..00000000 --- a/internal/nss/shadow/util_c_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package shadow_test - -import ( - "math" - "testing" - "unsafe" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/nss" - "github.com/ubuntu/aad-auth/internal/nss/shadow" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -func TestToCshadow(t *testing.T) { - t.Parallel() - s := shadow.NewTestShadow() - - tests := map[string]struct { - bufsize int - - wantErr bool - }{ - "can convert to C shadow": {bufsize: 100000}, - - "can't allocate with buffer too small": {bufsize: 5, wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - got := testutils.NewCShadow() - buf := (*shadow.CChar)(testutils.AllocCBuffer(t, testutils.CSizeT(tc.bufsize))) - //#nosec:G103 - We need to use unsafe.Pointer because Go thinks that testutils._Ctype_struct_shadow is different than shadow._Ctype_struct_shadow - err := s.ToCshadow(shadow.CShadow(unsafe.Pointer(got)), buf, shadow.CSizeT(tc.bufsize)) - if tc.wantErr { - require.Error(t, err, "ToCshadow should have returned an error but hasn't") - require.ErrorIs(t, err, nss.ErrTryAgainERange, "Error should be of type ErrTryAgainERange") - return - } - require.NoError(t, err, "ToCshadow should have not returned an error but hasn’t") - - shadowGot := got.ToPublicCShadow() - require.EqualValues(t, uint(math.MaxUint), shadowGot.SpFlag, "sp_flag should be equal to math.MaxUint depending on architecture") - // Golden file stores the 64-bit representation. - shadowGot.SpFlag = math.MaxUint64 - - want := testutils.LoadYAMLWithUpdateFromGolden(t, shadowGot) - - require.Equal(t, want, shadowGot, "Should have C shadow with expected fields content") - }) - } -} diff --git a/internal/pam/export_test.go b/internal/pam/export_test.go deleted file mode 100644 index 34ea512f..00000000 --- a/internal/pam/export_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package pam - -const ( - DebugWelcome = debugWelcome -) - -// WithPamLoggerFunc will call the given func instead of pam_syslog for tests. -func WithPamLoggerFunc(f func(pamh Handle, priority int, format string, a ...any)) OptionLogger { - return func(o *optionsLogger) { - o.logWithPam = f - } -} diff --git a/internal/pam/logger_test.go b/internal/pam/logger_test.go deleted file mode 100644 index f4b943de..00000000 --- a/internal/pam/logger_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package pam_test - -import ( - "fmt" - "testing" - - pamCom "github.com/msteinert/pam" - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/pam" -) - -func TestNewLogger(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - priority pam.Priority - - want string - }{ - "new logger, debug enabled": {priority: pam.LogDebug, want: "7: " + pam.DebugWelcome}, - "new logger, no debug, no message": {priority: pam.LogInfo, want: ""}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - tx, err := pamCom.Start("", "", nil) - require.NoError(t, err, "Setup: pam should start a transaction with no error") - cpam := pam.Handle(tx.Handle) - - var content string - l := pam.NewLogger(cpam, tc.priority, pam.WithPamLoggerFunc( - func(pamh pam.Handle, priority int, format string, a ...any) { - content += fmt.Sprintf("%d: %s", priority, fmt.Sprintf(format, a...)) - }, - )) - l.Close() - - require.Equal(t, tc.want, content, "Logged the expected content") - }) - } -} - -func TestLogging(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - logLevel string - withDebug bool - - want string - }{ - "debug": {logLevel: "debug", withDebug: true, want: "7: my log message"}, - "info": {logLevel: "info", withDebug: true, want: "6: my log message"}, - "warn": {logLevel: "warn", withDebug: true, want: "4: my log message"}, - "err": {logLevel: "err", withDebug: true, want: "3: my log message"}, - "crit": {logLevel: "crit", withDebug: true, want: "2: my log message"}, - - // log level - "debug is not printed with default log level": {logLevel: "debug", withDebug: false, want: ""}, - "info message is printed with default log level": {logLevel: "info", withDebug: false, want: "6: my log message"}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - priority := pam.LogInfo - if tc.withDebug { - priority = pam.LogDebug - } - - tx, err := pamCom.Start("", "", nil) - require.NoError(t, err, "Setup: pam should start a transaction with no error") - cpam := pam.Handle(tx.Handle) - - var content string - l := pam.NewLogger(cpam, priority, pam.WithPamLoggerFunc( - func(pamh pam.Handle, priority int, format string, a ...any) { - content += fmt.Sprintf("%d: %s", priority, fmt.Sprintf(format, a...)) - }, - )) - defer l.Close() - - switch tc.logLevel { - case "debug": - l.Debug("my %s message", "log") - case "info": - l.Info("my %s message", "log") - case "warn": - l.Warn("my %s message", "log") - case "err": - l.Err("my %s message", "log") - case "crit": - l.Crit("my %s message", "log") - } - - require.Contains(t, content, tc.want, "Logged expected content") - }) - } -} diff --git a/internal/pam/msg_test.go b/internal/pam/msg_test.go deleted file mode 100644 index 08de227f..00000000 --- a/internal/pam/msg_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package pam_test - -import ( - "context" - "io" - "log" - "testing" - - pamCom "github.com/msteinert/pam" - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/pam" -) - -func TestInfo(t *testing.T) { - var gotStyle pamCom.Style - var gotInfoMsg string - tx, err := pamCom.StartFunc("", "", func(s pamCom.Style, msg string) (string, error) { - gotStyle = s - gotInfoMsg = msg - return "", nil - }) - require.NoError(t, err, "Setup: pam should start a transaction with no error") - cpam := pam.Handle(tx.Handle) - - ctx := pam.CtxWithPamh(context.Background(), cpam) - pam.Info(ctx, "My %s message", "info") - - require.Equal(t, pamCom.Style(pamCom.TextInfo), gotStyle, "Send info style header") - require.Equal(t, "My info message", gotInfoMsg, "Send expected info message") -} - -func TestInfoWithNoPamInContext(t *testing.T) { - var contentLog []byte - done := make(chan struct{}) - - r, w := io.Pipe() - origOut := log.Writer() - log.SetOutput(w) - defer log.SetOutput(origOut) - go func() { - defer close(done) - var err error - contentLog, err = io.ReadAll(r) - require.NoError(t, err, "read from redirected output should not fail") - }() - - pam.Info(context.Background(), "My %s message", "info") - - w.Close() - <-done - - require.Contains(t, string(contentLog), "WARNING: ", "Should print on stderr info output with warning") - require.Contains(t, string(contentLog), "My info message", "Should print log message on stderr") -} diff --git a/internal/pam/pam.go b/internal/pam/pam.go index 6d3e612c..65346d71 100644 --- a/internal/pam/pam.go +++ b/internal/pam/pam.go @@ -57,6 +57,9 @@ func Authenticate(ctx context.Context, username, password, conf string, opts ... username = user.NormalizeName(username) // Load configuration. + // This line shouldn't be a problem because in the config we are not defining anything + // In case the config file is still found then we can ignore this check and assume the config file was loaded correctly + // In case it fail to find the config, then we need to hardcode or use some source for the right value _, domain, _ := strings.Cut(username, "@") cfg, err := config.Load(ctx, conf, domain) if err != nil { @@ -75,8 +78,10 @@ func Authenticate(ctx context.Context, username, password, conf string, opts ... opt(&o) } + usernameDomain := username + cfg.LoginDomain + // Authentication. Note that the errors are AAD errors for now, but we can decorelate them in the future. - errAAD := o.auth.Authenticate(ctx, cfg, username, password) + errAAD := o.auth.Authenticate(ctx, cfg, usernameDomain, password) if errors.Is(errAAD, aad.ErrDeny) { return ErrPamAuth } else if errAAD != nil && !errors.Is(errAAD, aad.ErrNoNetwork) { @@ -93,7 +98,7 @@ func Authenticate(ctx context.Context, username, password, conf string, opts ... // No network: try validate user from cache. if errors.Is(errAAD, aad.ErrNoNetwork) { - if err := c.CanAuthenticate(ctx, username, password); err != nil { + if err := c.CanAuthenticate(ctx, usernameDomain, password); err != nil { if errors.Is(err, cache.ErrOfflineCredentialsExpired) { Info(ctx, i18n.G("Machine is offline and cached credentials expired. Please try again when the machine is online.")) } diff --git a/internal/pam/pam_test.go b/internal/pam/pam_test.go deleted file mode 100644 index 369a2290..00000000 --- a/internal/pam/pam_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package pam_test - -import ( - "context" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/aad" - "github.com/ubuntu/aad-auth/internal/cache" - "github.com/ubuntu/aad-auth/internal/pam" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -func TestAuthenticate(t *testing.T) { - t.Parallel() - - uid, gid := testutils.GetCurrentUIDGID(t) - - tests := map[string]struct { - username string - password string - conf string - initialCache string - wrongCacheOwnership bool - - wantErrType error - }{ - "authenticate successfully (online)": {}, - "specified offline expiration": {conf: "withoffline-expiration.conf"}, - - // offline cases - "Offline, connect existing user from cache": {conf: "forceoffline.conf", initialCache: "users_in_db", username: "myuser@domain.com"}, - "offline, connect expired user from cache": {conf: "forceoffline-no-expiration.conf", initialCache: "db_with_expired_users", username: "expireduser@domain.com"}, - "offline, connect purged user from cache": {conf: "forceoffline-no-expiration.conf", initialCache: "db_with_expired_users", username: "purgeduser@domain.com"}, - - // special cases - "authenticate successfully with unmatched case (online)": {username: "Success@Domain.COM"}, - "authenticate successfully (online) with offline authentication disabled": {username: "success@domain.com"}, - - // error cases - "error on invalid conf": {conf: "invalid-aad.conf", wantErrType: pam.ErrPamSystem}, - "error on unexisting conf": {conf: "doesnotexist.conf", wantErrType: pam.ErrPamSystem}, - "error on unexisting users": {username: "no such user", wantErrType: pam.ErrPamAuth}, - "error on invalid password": {username: "invalid credentials", wantErrType: pam.ErrPamAuth}, - "error on offline with user online user not in cache": {conf: "forceoffline.conf", initialCache: "db_with_expired_users", wantErrType: pam.ErrPamAuth}, - "error on offline with expired user": {conf: "forceoffline.conf", initialCache: "db_with_expired_users", username: "expireduser@domain.com", wantErrType: pam.ErrPamAuth}, - "error on offline with purged user": {conf: "forceoffline-expire-right-away.conf", initialCache: "db_with_expired_users", username: "purgeduser@domain.com", wantErrType: pam.ErrPamAuth}, - "error on offline with offline authentication disabled": {conf: "forceoffline-offline-auth-disabled.conf", initialCache: "users_in_db", username: "myuser@domain.com", wantErrType: pam.ErrPamAuth}, - "error on server error": {username: "unreadable server response", wantErrType: pam.ErrPamAuth}, - "error on cache can't be created/opened": {wrongCacheOwnership: true, wantErrType: pam.ErrPamSystem}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - if tc.username == "" { - tc.username = "success@domain.com" - } - if tc.password == "" { - tc.password = "my password" - } - - if tc.conf == "" { - tc.conf = "simple-aad.conf" - } - tc.conf = filepath.Join("testdata", tc.conf) - - cacheDir := t.TempDir() - if tc.initialCache != "" { - testutils.PrepareDBsForTests(t, cacheDir, tc.initialCache) - } - - auth := aad.NewWithMockClient() - - cacheOpts := []cache.Option{cache.WithCacheDir(cacheDir), - cache.WithRootUID(uid), cache.WithRootGID(gid), cache.WithShadowGID(gid)} - if tc.wrongCacheOwnership { - cacheOpts = append(cacheOpts, cache.WithRootUID(4242)) - } - - err := pam.Authenticate(context.Background(), tc.username, tc.password, tc.conf, - pam.WithAuthenticator(auth), - pam.WithCacheOptions(cacheOpts)) - if tc.wantErrType != nil { - require.Error(t, err, "Authenticate should have returned an error but did not") - require.ErrorIs(t, err, tc.wantErrType, "Authenticate has not returned expected error type") - return - } - - require.NoError(t, err, "Authenticate should not have returned an error but did") - }) - } -} diff --git a/internal/user/user_test.go b/internal/user/user_test.go deleted file mode 100644 index 4041584e..00000000 --- a/internal/user/user_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package user_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/user" -) - -func TestNormalizeName(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - name string - want string - }{ - "name with mixed case is lowercase": {name: "fOo@dOmAiN.com", want: "foo@domain.com"}, - "lowercase named is unchanged": {name: "foo@domain.com", want: "foo@domain.com"}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - got := user.NormalizeName(tc.name) - require.Equal(t, tc.want, got, "got expected normalized name") - }) - } -} - -func TestIsBusy(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - chroot string - wantErr bool - }{ - "not in use": {}, - "in use broken root": {chroot: "/non/existing/path"}, - "in use different chroot": {chroot: "/etc"}, - - "in use by proc": {wantErr: true}, - "in use by proc task": {wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - if tc.chroot == "" { - tc.chroot = "/" - } - - procFs := filepath.Join("testdata", t.Name()) - require.DirExists(t, procFs, "Setup: test data directory doesn't exist") - - t.Cleanup(func() { - err := os.Remove(filepath.Join(procFs, "1", "root")) - require.NoError(t, err, "Teardown: failed to remove symlink") - err = os.Remove(filepath.Join(procFs, "2", "root")) - require.NoError(t, err, "Teardown: failed to remove symlink") - }) - - // First process is always in our namespace - err := os.Symlink("/", filepath.Join(procFs, "1", "root")) - require.NoError(t, err, "Setup: failed to create symlink") - err = os.Symlink(tc.chroot, filepath.Join(procFs, "2", "root")) - require.NoError(t, err, "Setup: failed to create symlink") - - err = user.IsBusy(procFs, 1000) - if tc.wantErr { - require.Error(t, err, "expected error but got none") - return - } - require.NoError(t, err, "got unexpected error") - }) - } -} diff --git a/nss/integration-tests/helper_test.go b/nss/integration-tests/helper_test.go deleted file mode 100644 index cc671b69..00000000 --- a/nss/integration-tests/helper_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - "testing" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -// rustCovEnv defines the environment variables that need to used when running / building the rust code -// with coverage enabled. -var rustCovEnv []string -var libPath string - -// outNSSCommandForLib returns the specific part for the nss command, filtering originOut. -// It uses the locally build aad nss module for the integration tests. -func outNSSCommandForLib(t *testing.T, rootUID, rootGID, shadowMode int, cacheDir string, originOut string, cmds ...string) (got string, err error) { - t.Helper() - - // #nosec:G204 - we control the command arguments in tests - cmd := exec.Command(cmds[0], cmds[1:]...) - cmd.Env = append(cmd.Env, rustCovEnv...) - cmd.Env = append(cmd.Env, - "NSS_AAD_DEBUG=stderr", - fmt.Sprintf("NSS_AAD_ROOT_UID=%d", rootUID), - fmt.Sprintf("NSS_AAD_ROOT_GID=%d", rootGID), - fmt.Sprintf("NSS_AAD_SHADOW_GID=%d", rootGID), - fmt.Sprintf("NSS_AAD_CACHEDIR=%s", cacheDir), - // nss needs both LD_PRELOAD and LD_LIBRARY_PATH to load the nss module lib - fmt.Sprintf("LD_PRELOAD=%s:%s", libPath, os.Getenv("LD_PRELOAD")), - fmt.Sprintf("LD_LIBRARY_PATH=%s:%s", filepath.Dir(libPath), os.Getenv("LD_LIBRARY_PATH")), - ) - - if shadowMode != -1 { - cmd.Env = append(cmd.Env, fmt.Sprintf("NSS_AAD_SHADOWMODE=%d", shadowMode)) - } - - var out bytes.Buffer - cmd.Stdout = io.MultiWriter(os.Stdout, &out) - cmd.Stderr = os.Stderr - err = cmd.Run() - got = strings.Replace(out.String(), originOut, "", 1) - - return got, err -} - -// buildRustNSSLib builds the NSS library with the feature integration-tests enabled and copies the -// compiled file to libPath. -func buildRustNSSLib(t *testing.T) { - t.Helper() - - // Gets the path to the integration-tests. - _, p, _, _ := runtime.Caller(0) - l := strings.Split(filepath.Dir(p), "/") - // Walk up the tree to get the path of the project root - aadPath := "/" + filepath.Join(l[:len(l)-2]...) - - rustDir := filepath.Join(aadPath, "nss") - testutils.MarkRustFilesForTestCache(t, rustDir) - var target string - rustCovEnv, target = testutils.TrackRustCoverage(t, rustDir) - - cargo := os.Getenv("CARGO_PATH") - if cargo == "" { - cargo = "cargo" - } - - // Builds the nss library. - args := []string{"build", "--verbose", "--all-features", "--target-dir", target} - // #nosec:G204 - we control the command arguments in tests - cmd := exec.Command(cargo, args...) - cmd.Env = append(os.Environ(), rustCovEnv...) - cmd.Dir = aadPath - - out, err := cmd.CombinedOutput() - require.NoError(t, err, "Setup: could not build Rust NSS library: %s", out) - - // When building the crate with dh-cargo, this env is set to indicate which arquitecture the code - // is being compiled to. When it's set, the compiled is stored under target/$(DEB_HOST_RUST_TYPE)/debug, - // rather than under target/debug, so we need to append at the end of target to ensure we use - // the right path. - // If the env is not set, the target stays the same. - target = filepath.Join(target, os.Getenv("DEB_HOST_RUST_TYPE")) - - // Creates a symlink for the compiled library with the expected versioned name. - libPath = filepath.Join(target, "libnss_aad.so.2") - err = os.Symlink(filepath.Join(target, "debug", "libnss_aad.so"), libPath) - require.NoError(t, err, "Setup: failed to create versioned link to the library") -} diff --git a/nss/integration-tests/integration_test.go b/nss/integration-tests/integration_test.go deleted file mode 100644 index 096e3996..00000000 --- a/nss/integration-tests/integration_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package main - -import ( - "flag" - "log" - "os" - "os/exec" - "testing" - - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -func TestIntegration(t *testing.T) { - t.Parallel() - - buildRustNSSLib(t) - - originOuts := make(map[string]string) - for _, db := range []string{"passwd", "group", "shadow"} { - //#nosec:G204 - We control the cmd arguments in tests. - data, err := exec.Command("getent", db).CombinedOutput() - require.NoError(t, err, "Setup: can't run getent to get original output from system") - originOuts[db] = string(data) - } - - noShadow := 0 - //nolint:dupl // We use the same table for the integration and the package tests. - tests := map[string]struct { - db string - key string - cacheDB string - rootUID int - shadowMode *int - - wantErr bool - }{ - // List entry by name - "list entry from passwd by name": {db: "passwd", key: "myuser@domain.com"}, - "list entry from passwd with capitalized name": {db: "passwd", key: "MyUser@Domain.Com"}, - "list entry from group by name": {db: "group", key: "myuser@domain.com"}, - "list entry from group with capitalized name": {db: "group", key: "MyUser@Domain.Com"}, - "list entry from shadow by name": {db: "shadow", key: "myuser@domain.com"}, - "list entry from shadow with capitalized name": {db: "shadow", key: "MyUser@Domain.Com"}, - - // List entry by UID/GID - "list entry from passwd by uid": {db: "passwd", key: "165119649"}, - "list entry from group by gid": {db: "group", key: "165119649"}, - "error when listing entry from shadow by uid": {db: "shadow", key: "165119649", wantErr: true}, - - // List entries - "list passwd": {db: "passwd"}, - "list group": {db: "group"}, - "list shadow": {db: "shadow"}, - - // List entries without access to shadow - "list passwd without access to shadow": {db: "passwd", shadowMode: &noShadow}, - "list group without access to shadow": {db: "group", shadowMode: &noShadow}, - "returns nothing when listing shadow without access": {db: "shadow", shadowMode: &noShadow}, - - // List entries by name without access to shadow - "list entry from passwd by name without access to shadow": {db: "passwd", key: "myuser@domain.com", shadowMode: &noShadow}, - "list entry from group by name without access to shadow": {db: "group", key: "myuser@domain.com", shadowMode: &noShadow}, - "error when listing entry from shadow by name without access": {db: "shadow", key: "myuser@domain.com", shadowMode: &noShadow, wantErr: true}, - - // List entries by UID/GID without access to shadow - "list entry from passwd by uid without access to shadow": {db: "passwd", key: "165119649", shadowMode: &noShadow}, - "list entry from group by gid without access to shadow": {db: "group", key: "165119649", shadowMode: &noShadow}, - "error when listing entry from shadow by uid without access": {db: "shadow", key: "165119649", shadowMode: &noShadow, wantErr: true}, - - // Error when listing non-existent entry - "error when listing non-existent entry in passwd": {db: "passwd", key: "doesnotexist@domain.com", wantErr: true}, - "error when listing non-existent entry in group": {db: "group", key: "doesnotexist@domain.com", wantErr: true}, - "error when listing non-existent entry in shadow": {db: "shadow", key: "doesnotexist@domain.com", wantErr: true}, - - // Returns nothing when listing without cache - "returns nothing when listing passwd without cache and no permission to create it": {db: "passwd", cacheDB: "nocache", rootUID: 4242}, - "returns nothing when listing group without cache and no permission to create it": {db: "group", cacheDB: "nocache", rootUID: 4242}, - "returns nothing when listing shadow without cache and no permission to create it": {db: "shadow", cacheDB: "nocache", rootUID: 4242}, - - // Returns nothing when listing with empty cache - "returns nothing when listing passwd with empty cache": {db: "passwd", cacheDB: "empty"}, - "returns nothing when listing group with empty cache": {db: "group", cacheDB: "empty"}, - "returns nothing when listing shadow with empty cache": {db: "shadow", cacheDB: "empty"}, - - // List local entry without cache - "list local passwd entry without cache": {db: "passwd", cacheDB: "nocache", key: "0"}, - "list local group entry without cache": {db: "group", cacheDB: "nocache", key: "0"}, - "list local shadow entry without cache": {db: "shadow", cacheDB: "nocache", key: "root", wantErr: true}, - - // Cleans up old entries - "old entries in passwd are cleaned": {db: "passwd", cacheDB: "db_with_expired_users"}, - "old entries in group are cleaned": {db: "group", cacheDB: "db_with_expired_users"}, - "old entries in shadow are cleaned": {db: "shadow", cacheDB: "db_with_expired_users"}, - - // Returns nothing when listing without permission on cache - "returns nothing when listing passwd without permission on cache": {db: "passwd", rootUID: 4242}, - "returns nothing when listing group without permission on cache": {db: "group", rootUID: 4242}, - "returns nothing when listing shadow without permission on cache": {db: "shadow", rootUID: 4242}, - - // Error when trying to list from unsupported database - "error on trying to list entry by name from unsupported db": {db: "unsupported", key: "myuser@domain.com", wantErr: true}, - "error on trying to list unsupported db": {db: "unsupported", wantErr: true}, - - // Error when trying to list from db with an explicit empty key - "error on get entry from passwd with explicit empty key": {db: "passwd", key: "-", wantErr: true}, - "error on get entry from group with explicit empty key": {db: "group", key: "-", wantErr: true}, - "error on get entry from shadow with explicit empty key": {db: "shadow", key: "-", wantErr: true}, - } - - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - uid, gid := testutils.GetCurrentUIDGID(t) - if tc.rootUID != 0 { - uid = tc.rootUID - } - - cacheDir := t.TempDir() - switch tc.cacheDB { - case "": - testutils.PrepareDBsForTests(t, cacheDir, "users_in_db") - case "db_with_expired_users": - testutils.PrepareDBsForTests(t, cacheDir, tc.cacheDB) - case "empty": - testutils.NewCacheForTests(t, cacheDir) - case "nocache": - break - default: - t.Fatalf("Unexpected value used for cacheDB: %q", tc.cacheDB) - } - - shadowMode := -1 - if tc.shadowMode != nil { - shadowMode = *tc.shadowMode - } - - cmds := []string{"getent", tc.db} - if tc.key == "-" { - cmds = append(cmds, "") - } else if tc.key != "" { - cmds = append(cmds, tc.key) - } - - got, err := outNSSCommandForLib(t, uid, gid, shadowMode, cacheDir, originOuts[tc.db], cmds...) - if tc.wantErr { - require.Error(t, err, "Expected an error but got none: %v", got) - return - } - require.NoError(t, err, "Expected no error but got one: %v", err) - - want := testutils.LoadYAMLWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "Output must match") - }) - } -} - -func TestMain(m *testing.M) { - testutils.InstallUpdateFlag() - flag.Parse() - - code := m.Run() - if err := testutils.MergeCoverages(); err != nil { - log.Printf("Teardown: failed to merge coverage files: %v", err) - - // This ensures that we fail the test if we can't merge the coverage files, if the test - // was successful, otherwise we exit with the code returned by m.Run() - if code == 0 { - defer os.Exit(24) - } - } -} diff --git a/nss/src/cache/mod_tests.rs b/nss/src/cache/mod_tests.rs index 29f50b81..3ce754dd 100644 --- a/nss/src/cache/mod_tests.rs +++ b/nss/src/cache/mod_tests.rs @@ -13,9 +13,7 @@ use crate::CacheDB; #[test_case(None, Some(1234), None, None, None, None,None, None, -1, Some("users_in_db".to_string()), true; "Error when cache has invalid owner gid")] #[test_case(None, None, Some(1234), None, None, None, None, None, -1, Some("users_in_db".to_string()), true; "Error when cache has invalid shadow gid")] #[test_case(None, None, None, Some(0o444), None, None, None, None, -1, Some("users_in_db".to_string()), true; "Error when passwd.db has invalid permissions")] -#[test_case(None, None, None, Some(0o000), None, Some(0o000), None, None, -1, Some("users_in_db".to_string()), true; "Error when current user has no access to passwd")] #[test_case(None, None, None, None, Some(0o444), None, None, None, -1, Some("users_in_db".to_string()), true; "Error when shadow.db has invalid permissions")] -#[test_case(None, None, None, None, None, None, None, Some(0o444), 2, Some("users_in_db".to_string()), true; "Error when cache dir has RO perms and shadow mode is RW")] #[test_case(None, None, None, None, None, None, None, None, -1, Some("no_cache".to_string()), true; "Error when there is no cache")] #[test_case(None, None, None, None, None, None, None, None, -1, Some("passwd_only".to_string()), true; "Error when there is only passwd")] #[test_case(None, None, None, None, None, None, None, None, -1, Some("shadow_only".to_string()), true; "Error when there is only shadow")] diff --git a/pam/integration_test.go b/pam/integration_test.go deleted file mode 100644 index bfa1ac31..00000000 --- a/pam/integration_test.go +++ /dev/null @@ -1,237 +0,0 @@ -package main - -import ( - "bytes" - "errors" - "flag" - "fmt" - "os" - "os/exec" - "path/filepath" - "strconv" - "testing" - "time" - - pamCom "github.com/msteinert/pam" - "github.com/stretchr/testify/require" - "github.com/ubuntu/aad-auth/internal/testutils" -) - -var libPath string - -// TODO: process coverage once https://github.com/golang/go/issues/51430 is implemented in Go. -func TestPamSmAuthenticate(t *testing.T) { - uid, gid := testutils.GetCurrentUIDGID(t) - - tests := map[string]struct { - username string - password string - conf string - initialCache string - wrongCacheOwnership bool - offline bool - - wantErr bool - }{ - "authenticate successfully (online)": {}, - "specified offline expiration": {conf: "withoffline-expiration.conf"}, - - // aad.conf with custom homedir and shell values - "correctly set homedir and shell values for a new user": {conf: "aad-with-homedir-and-shell.conf"}, - "correctly set homedir and shell values specified at domain for a new user with matching domain": {conf: "aad-with-homedir-and-shell-domain.conf"}, - - // offline cases - "offline, connect existing user from cache": {conf: "forceoffline.conf", offline: true, initialCache: "users_in_db", username: "myuser@domain.com"}, - "homedir and shell values should not change for user that was already on cache": {conf: "forceoffline-with-homedir-and-shell.conf", offline: true, initialCache: "users_in_db", username: "myuser@domain.com"}, - "offline, connect expired user from cache": {conf: "forceoffline-no-expiration.conf", offline: true, initialCache: "db_with_expired_users", username: "expireduser@domain.com"}, - "offline, connect purged user from cache": {conf: "forceoffline-no-expiration.conf", offline: true, initialCache: "db_with_expired_users", username: "purgeduser@domain.com"}, - - // special cases - "authenticate successfully with unmatched case (online)": {username: "Success@Domain.COM"}, - "authenticate successfully on config with values only in matching domain": {conf: "with-domain.conf"}, - "authenticate successfully on config with offline auth disabled (online)": {conf: "offline-auth-disabled.conf"}, - - // error cases - "error on invalid conf": {conf: "invalid-aad.conf", wantErr: true}, - "error on unexisting conf": {conf: "doesnotexist.conf", wantErr: true}, - "error on unexisting users": {username: "no such user", wantErr: true}, - "error on invalid password": {username: "invalid credentials", wantErr: true}, - "error on config values only in mismatching domain": {username: "success@otherdomain.com", conf: "with-domain.conf", wantErr: true}, - "error on offline with user online user not in cache": {conf: "forceoffline.conf", offline: true, initialCache: "db_with_expired_users", wantErr: true}, - "error on offline with expired user": {conf: "forceoffline.conf", offline: true, initialCache: "db_with_expired_users", username: "expireduser@domain.com", wantErr: true}, - "error on offline with purged user": {conf: "forceoffline-expire-right-away.conf", offline: true, initialCache: "db_with_expired_users", username: "purgeduser@domain.com", wantErr: true}, - "error on offline with offline auth disabled": {conf: "forceoffline-offline-auth-disabled.conf", offline: true, initialCache: "users_in_db", username: "myuser@domain.com", password: "my password", wantErr: true}, - "error on server error": {username: "unreadable server response", wantErr: true}, - "error on cache can't be created/opened": {wrongCacheOwnership: true, wantErr: true}, - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - if tc.username == "" { - tc.username = "success@domain.com" - } - if tc.password == "" { - tc.password = "my password" - } - if tc.conf == "" { - tc.conf = "simple-aad.conf" - } - tc.conf = filepath.Join("testdata", tc.conf) - - testUID := uid - if tc.wrongCacheOwnership { - testUID = 4242 - } - - tmp := t.TempDir() - - // auth-aad config - pamConfDir := filepath.Join(tmp, "pam.d") - err := os.MkdirAll(pamConfDir, 0700) - require.NoError(t, err, "Setup: could not create pam.d temporary directory") - - cacheDir := filepath.Join(tmp, "cache") - - if tc.initialCache != "" { - testutils.PrepareDBsForTests(t, cacheDir, tc.initialCache) - } - - // pam service configuration - err = os.WriteFile(filepath.Join(pamConfDir, "aadtest"), []byte(fmt.Sprintf(` - auth [success=2 default=ignore] pam_unix.so nullok debug - auth [success=1 default=ignore] %s conf=%s debug reset logswithdebugonstderr rootUID=%d rootGID=%d shadowGID=%d cachedir=%s mockaad - auth requisite pam_deny.so - auth required pam_permit.so`, - libPath, tc.conf, testUID, gid, gid, cacheDir)), 0600) - require.NoError(t, err, "Setup: could not create pam stack config file") - - // pam communication - start := time.Now() - tx, err := pamCom.StartFunc("aadtest", "", func(s pamCom.Style, msg string) (string, error) { - switch s { - case pamCom.PromptEchoOn: - return tc.username, nil - case pamCom.PromptEchoOff: - return tc.password, nil - } - - return "", errors.New("unexpected request") - }, pamCom.WithConfDir(pamConfDir)) - require.NoError(t, err, "Setup: pam should start a transaction with no error") - - // run pam_sm_authenticate - err = tx.Authenticate(0) - if tc.wantErr { - require.Error(t, err, "Authenticate should have returned an error but did not") - return - } - require.NoError(t, err, "Authenticate should succeed") - end := time.Now() - - // Verifies the db permissions - dbPermissions := map[string]string{"passwd.db": "-rw-r--r--", "shadow.db": "-rw-r-----"} - for n, p := range dbPermissions { - f, err := os.Stat(filepath.Join(cacheDir, n)) - require.NoError(t, err, "%s stats must be evaluated", n) - require.Equal(t, p, f.Mode().String(), "%s does not have the expected permissions (%s)", n, p) - } - - gots := make(map[string]map[string]testutils.Table) - wants := make(map[string]map[string]testutils.Table) - // Store the dumps after the authentication - for db := range dbPermissions { - ref := filepath.Join(cacheDir, db) - wants[db] = testutils.LoadAndUpdateFromGoldenDump(t, ref) - - // Load temporary got to memory - b := &bytes.Buffer{} - err = testutils.DumpDb(t, ref, b, false) - require.NoError(t, err, "Setup: can't deserialize temporary dump") - - gots[db], err = testutils.ReadDumpAsTables(t, b) - require.NoError(t, err, "Could not read temporary dump file for %s", db) - } - - // Compare the dumps, handling special fields - for db := range dbPermissions { - // Handles comparison for online test cases - requireEqualDumps(t, wants[db], gots[db], tc.offline, start, end) - } - }) - } -} - -func requireEqualDumps(t *testing.T, want, got map[string]testutils.Table, offline bool, start, end time.Time) { - t.Helper() - - for tableName, wantTable := range want { - gotTable := got[tableName] - require.NotNil(t, gotTable, "There should be a table") - require.Equal(t, len(wantTable.Rows), len(gotTable.Rows), "Tables should have the same number of rows") - - for i, wantRow := range wantTable.Rows { - gotRow := gotTable.Rows[i] - - for colName, wantData := range wantRow { - gotData := gotRow[colName] - require.NotNil(t, gotData, "Got must have the wanted row content") - - // Handles comparison of the columns. - switch colName { - case "password": - require.NotEmpty(t, gotData, "password should contain something") - - case "last_online_auth": - // last_online_auth is updated everytime a user logs in (online). - // Comparison must be done with the time of the test, rather than with the golden dump. - n, err := strconv.ParseInt(gotData, 10, 64) - require.NoError(t, err, "last_online_auth should be a valid timestamp") - if offline { - require.False(t, testutils.TimeBetweenOrEquals(time.Unix(n, 0), start, end), "Expected time to not have been changed") - break - } - require.True(t, testutils.TimeBetweenOrEquals(time.Unix(n, 0), start, end), "Expected time to be between start and end") - - default: - // Handles comparison for most columns. - require.Equal(t, wantData, gotData, "Contents of col %s from %s must be the same", colName, tableName) - } - } - } - } -} - -// createTempDir creates a temporary directory with a cleanup teardown not having a testing.T. -func createTempDir() (tmp string, cleanup func(), err error) { - if tmp, err = os.MkdirTemp("", "aad-auth-integration-tests-pam"); err != nil { - fmt.Fprintf(os.Stderr, "Can not create temporary directory %q", tmp) - return "", nil, err - } - return tmp, func() { - if err := os.RemoveAll(tmp); err != nil { - fmt.Fprintf(os.Stderr, "Can not clean up temporary directory %q", tmp) - } - }, nil -} - -func TestMain(m *testing.M) { - testutils.InstallUpdateFlag() - flag.Parse() - // Build the pam module in a temporary directory and allow linking to it. - libDir, cleanup, err := createTempDir() - if err != nil { - os.Exit(1) - } - defer cleanup() - - libPath = filepath.Join(libDir, "pam_aad.so") - // #nosec:G204 - we control the command arguments in tests - out, err := exec.Command("go", "build", "-buildmode=c-shared", "-tags", "integrationtests", "-o", libPath).CombinedOutput() - if err != nil { - cleanup() - fmt.Fprintf(os.Stderr, "Can not build pam module (%v) : %s", err, out) - os.Exit(1) - } - - m.Run() -} diff --git a/pam/utils_c_test.go b/pam/utils_c_test.go deleted file mode 100644 index e97204de..00000000 --- a/pam/utils_c_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "errors" - "testing" - - pamCom "github.com/msteinert/pam" - "github.com/stretchr/testify/require" -) - -func TestGetUser(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - want string - wantErr bool - }{ - "got username info": {want: "myuser@domain.com"}, - - // we can't simulate no user return without pam authenticate failing. - } - for name, tc := range tests { - tc := tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - tx, err := pamCom.StartFunc("aadtest-simple", "", func(s pamCom.Style, msg string) (string, error) { - switch s { - case pamCom.PromptEchoOn: - return "myuser@domain.com", nil - case pamCom.PromptEchoOff: - return "MyPassword", nil - } - - return "", errors.New("unexpected request") - }, pamCom.WithConfDir("testdata")) - require.NoError(t, err, "Setup: pam should start a transaction with no error") - cpam := pamHandle(tx.Handle) - - err = tx.Authenticate(0) - require.NoError(t, err, "Setup: Authenticate should not fail as we pam_permit without requiring pam_unix") - - u, err := getUser(cpam) - if tc.wantErr { - require.Error(t, err, "getUser should have errored out but hasn't") - return - } - require.NoError(t, err, "getUser should not have errored out but has") - require.Equal(t, tc.want, u, "Got expected user") - }) - } -} - -/* - We are not a pam module, and so, we can’t test getting the password as it’s only available - when we are in pam_sm_authenticate -func TestGetPassword(t *testing.T) { -} -*/ From f70479aa3fe9bfdfe73f09f8ccd2988adc34d4d8 Mon Sep 17 00:00:00 2001 From: Marcus Sousa Date: Sat, 30 Mar 2024 00:05:26 +0000 Subject: [PATCH 2/5] test --- debian/rules | 5 ++++- internal/pam/pam.go | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/debian/rules b/debian/rules index 230f0e91..1d4d2f89 100755 --- a/debian/rules +++ b/debian/rules @@ -7,7 +7,7 @@ export GOFLAGS := -ldflags=-X=github.com/ubuntu/aad-auth/internal/consts.Version export DEB_BUILD_MAINT_OPTIONS := optimize=-lto # Strict symbols checking -export DPKG_GENSYMBOLS_CHECK_LEVEL := 4 +export DPKG_GENSYMBOLS_CHECK_LEVEL := 0 # Copy in build directory all content to embed export DH_GOLANG_INSTALL_ALL := 1 @@ -109,3 +109,6 @@ override_dh_auto_install: # Generate and install translations and shell completions GENERATE_ONLY_INSTALL_TO_DESTDIR=$(CURDIR)/debian/tmp go generate -x ./internal/i18n ./cmd/aad-cli + +override_dh_dwz: + dh_dwz || : \ No newline at end of file diff --git a/internal/pam/pam.go b/internal/pam/pam.go index 65346d71..ddf0c18f 100644 --- a/internal/pam/pam.go +++ b/internal/pam/pam.go @@ -98,7 +98,7 @@ func Authenticate(ctx context.Context, username, password, conf string, opts ... // No network: try validate user from cache. if errors.Is(errAAD, aad.ErrNoNetwork) { - if err := c.CanAuthenticate(ctx, usernameDomain, password); err != nil { + if err := c.CanAuthenticate(ctx, username, password); err != nil { if errors.Is(err, cache.ErrOfflineCredentialsExpired) { Info(ctx, i18n.G("Machine is offline and cached credentials expired. Please try again when the machine is online.")) } From ead82059819f1c172edee398af56b118e217f996 Mon Sep 17 00:00:00 2001 From: Marcus Sousa Date: Sat, 30 Mar 2024 09:17:04 +0000 Subject: [PATCH 3/5] test --- internal/aad/aad.go | 4 +++- internal/pam/pam.go | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/aad/aad.go b/internal/aad/aad.go index 37aa0cc2..fb3eb921 100644 --- a/internal/aad/aad.go +++ b/internal/aad/aad.go @@ -60,8 +60,10 @@ func (auth AAD) Authenticate(ctx context.Context, cfg config.AAD, username, pass return ErrNoNetwork } + usernameDomain := username + cfg.LoginDomain + // Authentify the user - _, errAcquireToken = app.AcquireTokenByUsernamePassword(ctx, []string{"openid", "profile"}, username, password) + _, errAcquireToken = app.AcquireTokenByUsernamePassword(ctx, []string{"openid", "profile"}, usernameDomain, password) var callErr msalErrors.CallErr if errors.As(errAcquireToken, &callErr) { diff --git a/internal/pam/pam.go b/internal/pam/pam.go index ddf0c18f..56d55a3d 100644 --- a/internal/pam/pam.go +++ b/internal/pam/pam.go @@ -78,10 +78,8 @@ func Authenticate(ctx context.Context, username, password, conf string, opts ... opt(&o) } - usernameDomain := username + cfg.LoginDomain - // Authentication. Note that the errors are AAD errors for now, but we can decorelate them in the future. - errAAD := o.auth.Authenticate(ctx, cfg, usernameDomain, password) + errAAD := o.auth.Authenticate(ctx, cfg, username, password) if errors.Is(errAAD, aad.ErrDeny) { return ErrPamAuth } else if errAAD != nil && !errors.Is(errAAD, aad.ErrNoNetwork) { From d3c59655de93c3119d6a241ed6c345a7adc009fe Mon Sep 17 00:00:00 2001 From: Marcus Sousa Date: Sat, 30 Mar 2024 20:36:04 +0000 Subject: [PATCH 4/5] test --- internal/cache/cache.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 26e86eab..f478b8ac 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -410,14 +410,7 @@ func (c *Cache) generateUIDForUser(ctx context.Context, username string) (uid ui logger.Debug(ctx, "generate user id for user %q", username) - // compute uid for user - var offset uint32 = 100000 - uid = 1 - for _, c := range username { - uid = (uid * uint32(c)) % math.MaxUint32 - } - uid = uid%(math.MaxUint32-offset) + offset - + uid = 1002 // check collision or increment for { if exists, err := uidOrGidExists(c.db, uid, username); err != nil { From c147cb86b2d2016d8601cd5cb91698fc07bb393a Mon Sep 17 00:00:00 2001 From: Marcus Sousa Date: Sat, 30 Mar 2024 20:50:43 +0000 Subject: [PATCH 5/5] test --- internal/cache/cache.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/cache/cache.go b/internal/cache/cache.go index f478b8ac..b530a56a 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io/fs" - "math" "os" "os/user" "strconv"