Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pam/integration-tests): Add SSH authentication tests #583

Merged
merged 26 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f1d5791
pam/tools/pam-client: Remove leftover
3v1n0 Oct 9, 2024
42de3d1
pam-runner: Redirect ASAN and LSAN options to launched clients
3v1n0 Oct 9, 2024
8ba7d4a
pam/integration-tests/helpers: Use unique folder for each test artifacts
3v1n0 Oct 8, 2024
1077592
pam/integration-tests/vhs: Use const values for sleep definitions
3v1n0 Oct 8, 2024
4a2c3b3
pam/integration-tests: Make tapes commands to be controllable from go…
3v1n0 Oct 8, 2024
30d6346
pam/integration-tests: Use unique users for password reset
3v1n0 Oct 8, 2024
5c6b9bc
pam/integration-tests: Add helper for preparing gpasswd-generated files
3v1n0 Oct 8, 2024
196f24e
pam/integration-tests: Use the same authd instance when possible
3v1n0 Nov 8, 2024
d545797
pam/integration-tests/native: Check if sigint is actually working by …
3v1n0 Oct 10, 2024
b8b6c31
pam/test/service-utils: Add support for @include syntax
3v1n0 Sep 20, 2024
bfc6f90
pam/integration-tests: Generalize C module builder for wider scopes
3v1n0 Sep 23, 2024
ac84d80
examplebroker: Add user-needs-reset2
3v1n0 Oct 8, 2024
2f36e2f
pam/nativemodel: Use consistent layout for new password mode
3v1n0 Nov 8, 2024
dce36c8
pam/integration-tests: Add PAM tests using SSHd
3v1n0 Sep 26, 2024
88ef5b2
pam/integration-tests/ssh: Reuse native data tapes when possible
3v1n0 Oct 10, 2024
3eda8dc
pam/integration-tests/ssh: Disable coverage and asan for preloaded li…
3v1n0 Oct 9, 2024
2674647
pam/integration-tests/ssh: Add support for running sshd as daemon
3v1n0 Oct 9, 2024
b420f68
pam/integration-tests/ssh: Add test checking that user selections are…
3v1n0 Oct 9, 2024
23bce66
pam/utils: Check if we're in a session just once
3v1n0 Oct 9, 2024
cca9b3a
pam/integration-tests/ssh: Move sshd startup code to own function
3v1n0 Oct 10, 2024
570016a
pam/integration-tests: Add tests using a single SSH server for all th…
3v1n0 Oct 10, 2024
8c51be0
pam/integration-tests/native: Use PAM-preset user by default in tests
3v1n0 Oct 10, 2024
5a74862
pam/integration-tests/ssh: Reuse most of native tapes
3v1n0 Oct 10, 2024
439868d
pam/integration-tests: Remove unused tapes for mismatching user
3v1n0 Oct 10, 2024
a51f412
pam/integration-tests: Do not use utf-8 ellipses in broker messages
3v1n0 Oct 10, 2024
00f1a92
pam/integration-tests/ssh: Ignore SSH tests when running on unsupport…
3v1n0 Nov 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/qa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ env:
test_apt_deps: >-
cracklib-runtime
ffmpeg
openssh-client
openssh-server

# In Rust the grpc stubs are generated at build time
# so we always need to install the protobuf compilers
Expand Down
57 changes: 34 additions & 23 deletions examplebroker/broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,18 @@ type userInfoBroker struct {
var (
exampleUsersMu = sync.RWMutex{}
exampleUsers = map[string]userInfoBroker{
"user1": {Password: "goodpass"},
"user2": {Password: "goodpass"},
"user3": {Password: "goodpass"},
"user-mfa": {Password: "goodpass"},
"user-mfa-with-reset": {Password: "goodpass"},
"user-needs-reset": {Password: "goodpass"},
"user-can-reset": {Password: "goodpass"},
"user-can-reset2": {Password: "goodpass"},
"user-local-groups": {Password: "goodpass"},
"user-pre-check": {Password: "goodpass"},
"user-sudo": {Password: "goodpass"},
"user-mismatching-name": {Password: "goodpass"},
"user1": {Password: "goodpass"},
"user2": {Password: "goodpass"},
"user3": {Password: "goodpass"},
"user-mfa": {Password: "goodpass"},
"user-mfa-with-reset": {Password: "goodpass"},
"user-needs-reset": {Password: "goodpass"},
"user-needs-reset2": {Password: "goodpass"},
"user-can-reset": {Password: "goodpass"},
"user-can-reset2": {Password: "goodpass"},
"user-local-groups": {Password: "goodpass"},
"user-pre-check": {Password: "goodpass"},
"user-sudo": {Password: "goodpass"},
}
)

Expand Down Expand Up @@ -161,6 +161,8 @@ func (b *Broker) NewSession(ctx context.Context, username, lang, mode string) (s
case "user-mfa":
info.neededAuthSteps = 3
case "user-needs-reset":
fallthrough
case "user-needs-reset2":
info.neededAuthSteps = 2
info.pwdChange = mustReset
case "user-can-reset":
Expand Down Expand Up @@ -197,12 +199,24 @@ func (b *Broker) NewSession(ctx context.Context, username, lang, mode string) (s
info.pwdChange = mustReset
}

if _, ok := exampleUsers[username]; !ok && strings.HasPrefix(username, "user-mfa-with-reset-integration") {
exampleUsers[username] = userInfoBroker{Password: "goodpass"}
info.neededAuthSteps = 3
info.pwdChange = canReset
}

if _, ok := exampleUsers[username]; !ok && strings.HasPrefix(username, "user-needs-reset-integration") {
exampleUsers[username] = userInfoBroker{Password: "goodpass"}
info.neededAuthSteps = 2
info.pwdChange = mustReset
}

if _, ok := exampleUsers[username]; !ok && strings.HasPrefix(username, "user-can-reset-integration") {
exampleUsers[username] = userInfoBroker{Password: "goodpass"}
info.neededAuthSteps = 2
info.pwdChange = canReset
}

pubASN1, err := x509.MarshalPKIXPublicKey(&b.privateKey.PublicKey)
if err != nil {
return "", "", err
Expand Down Expand Up @@ -327,7 +341,7 @@ func getSupportedModes(sessionInfo sessionInfo, supportedUILayouts []map[string]
if layout["button"] == "optional" {
allModes["totp_with_button"] = map[string]string{
"selection_label": "Authentication code",
"phone": "+33",
"phone": "+33...",
"wantedCode": "temporary pass",
"ui": mapToJSON(map[string]string{
"type": "form",
Expand All @@ -339,7 +353,7 @@ func getSupportedModes(sessionInfo sessionInfo, supportedUILayouts []map[string]
} else {
allModes["totp"] = map[string]string{
"selection_label": "Authentication code",
"phone": "+33",
"phone": "+33...",
"wantedCode": "temporary pass",
"ui": mapToJSON(map[string]string{
"type": "form",
Expand All @@ -350,21 +364,21 @@ func getSupportedModes(sessionInfo sessionInfo, supportedUILayouts []map[string]
}

allModes["phoneack1"] = map[string]string{
"selection_label": "Use your phone +33",
"phone": "+33",
"selection_label": "Use your phone +33...",
"phone": "+33...",
"ui": mapToJSON(map[string]string{
"type": "form",
"label": "Unlock your phone +33 or accept request on web interface:",
"label": "Unlock your phone +33... or accept request on web interface:",
"wait": "true",
}),
}

allModes["phoneack2"] = map[string]string{
"selection_label": "Use your phone +1",
"phone": "+1",
"selection_label": "Use your phone +1...",
"phone": "+1...",
"ui": mapToJSON(map[string]string{
"type": "form",
"label": "Unlock your phone +1 or accept request on web interface",
"label": "Unlock your phone +1... or accept request on web interface",
"wait": "true",
}),
}
Expand Down Expand Up @@ -888,9 +902,6 @@ func userInfoFromName(name string) string {

case "user-sudo":
user.Groups = append(user.Groups, groupJSONInfo{Name: "sudo", UGID: ""}, groupJSONInfo{Name: "admin", UGID: ""})

case "user-mismatching-name":
user.Name = "mismatching-username"
}

// only used for tests, we can ignore the template execution error as the returned data will be failing.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ require (
google.golang.org/protobuf v1.34.2
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1
gorbe.io/go/osrelease v0.3.0
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,5 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorbe.io/go/osrelease v0.3.0 h1:RqVqqfYMbe8AkTrCovTzE+FXYNolUFrhP/ne44za/xA=
gorbe.io/go/osrelease v0.3.0/go.mod h1:kuAQu3QnfzxWIa2eppGpV3ZWAwa/7DP/m465/1I48oU=
38 changes: 32 additions & 6 deletions pam/integration-tests/cli_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main_test

import (
"fmt"
"os"
"os/exec"
"path/filepath"
Expand All @@ -21,13 +22,18 @@ func TestCLIAuthenticate(t *testing.T) {
clientPath := t.TempDir()
cliEnv := preparePamRunnerTest(t, clientPath)
const socketPathEnv = "AUTHD_TESTS_CLI_AUTHENTICATE_TESTS_SOCK"
tapeCommand := fmt.Sprintf("./pam_authd login socket=${%s}", socketPathEnv)

defaultGPasswdOutput, groupsFile := prepareGPasswdFiles(t)
defaultSocketPath := runAuthd(t, defaultGPasswdOutput, groupsFile, true)

tests := map[string]struct {
tape string
tapeSettings []tapeSetting

clientOptions clientOptions
currentUserNotRoot bool
wantLocalGroups bool
}{
"Authenticate user successfully": {
tape: "simple_auth",
Expand Down Expand Up @@ -96,7 +102,8 @@ func TestCLIAuthenticate(t *testing.T) {
tape: "switch_local_broker",
},
"Authenticate user and add it to local group": {
tape: "local_group",
tape: "local_group",
wantLocalGroups: true,
},
"Authenticate with warnings on unsupported arguments": {
tape: "simple_auth_with_unsupported_args",
Expand Down Expand Up @@ -148,11 +155,20 @@ func TestCLIAuthenticate(t *testing.T) {
filepath.Join(outDir, "pam_authd"))
require.NoError(t, err, "Setup: symlinking the pam client")

gpasswdOutput := filepath.Join(outDir, "gpasswd.output")
groupsFile := filepath.Join(testutils.TestFamilyPath(t), "gpasswd.group")
socketPath := runAuthd(t, gpasswdOutput, groupsFile, !tc.currentUserNotRoot)
socketPath := defaultSocketPath
gpasswdOutput := defaultGPasswdOutput
if tc.wantLocalGroups || tc.currentUserNotRoot {
// For the local groups tests we need to run authd again so that it has
// special environment that generates a fake gpasswd output for us to test.
// Similarly for the not-root tests authd has to run in a more restricted way.
// In the other cases this is not needed, so we can just use a shared authd.
var groupsFile string
gpasswdOutput, groupsFile = prepareGPasswdFiles(t)
socketPath = runAuthd(t, gpasswdOutput, groupsFile, !tc.currentUserNotRoot)
}

td := newTapeData(tc.tape, tc.tapeSettings...)
td.Command = tapeCommand
td.Env[socketPathEnv] = socketPath
td.AddClientOptions(t, tc.clientOptions)
td.RunVhs(t, "cli", outDir, cliEnv)
Expand All @@ -172,16 +188,22 @@ func TestCLIChangeAuthTok(t *testing.T) {
cliEnv := preparePamRunnerTest(t, outDir)

const socketPathEnv = "AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK"
const tapeBaseCommand = "./pam_authd %s socket=${%s}"
tapeCommand := fmt.Sprintf(tapeBaseCommand, "passwd", socketPathEnv)
defaultSocketPath := runAuthd(t, os.DevNull, os.DevNull, true)

tests := map[string]struct {
tape string
tapeSettings []tapeSetting
tape string
tapeSettings []tapeSetting
tapeVariables map[string]string

currentUserNotRoot bool
}{
"Change password successfully and authenticate with new one": {
tape: "passwd_simple",
tapeVariables: map[string]string{
"AUTHD_TEST_TAPE_LOGIN_COMMAND": fmt.Sprintf(tapeBaseCommand, "login", socketPathEnv),
},
},
"Change passwd after MFA auth": {
tape: "passwd_mfa",
Expand Down Expand Up @@ -224,10 +246,14 @@ func TestCLIChangeAuthTok(t *testing.T) {

socketPath := defaultSocketPath
if tc.currentUserNotRoot {
// For the not-root tests authd has to run in a more restricted way.
// In the other cases this is not needed, so we can just use a shared authd.
socketPath = runAuthd(t, os.DevNull, os.DevNull, false)
}

td := newTapeData(tc.tape, tc.tapeSettings...)
td.Command = tapeCommand
td.Variables = tc.tapeVariables
td.Env[socketPathEnv] = socketPath
td.AddClientOptions(t, clientOptions{})
td.RunVhs(t, "cli", outDir, cliEnv)
Expand Down
13 changes: 7 additions & 6 deletions pam/integration-tests/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestExecModule(t *testing.T) {
t.Fatal("can't test with this libpam version!")
}

libPath := buildExecModuleWithCFlags(t, []string{"-DAUTHD_TEST_EXEC_MODULE"})
libPath := buildExecModuleWithCFlags(t, []string{"-DAUTHD_TEST_EXEC_MODULE"}, false)
execClient := buildExecClient(t)

// We do multiple tests inside this test function not to have to re-compile
Expand Down Expand Up @@ -823,7 +823,7 @@ func TestExecModuleUnimplementedActions(t *testing.T) {
t.Fatal("can't test with this libpam version!")
}

libPath := buildExecModuleWithCFlags(t, nil)
libPath := buildExecModule(t)
execClient := buildExecClient(t)

tx := preparePamTransaction(t, libPath, execClient, nil, "an-user")
Expand All @@ -837,7 +837,7 @@ func getModuleArgs(t *testing.T, clientPath string, args []string) []string {

moduleArgs := []string{"--exec-debug"}
if env := testutils.CoverDirEnv(); env != "" {
moduleArgs = append(moduleArgs, "--exec-env", testutils.CoverDirEnv())
moduleArgs = append(moduleArgs, "--exec-env", env)
}

logFile := os.Stderr.Name()
Expand Down Expand Up @@ -925,16 +925,17 @@ func performAllPAMActions(t *testing.T, tx *pam.Transaction, flags pam.Flags, wa
func buildExecModule(t *testing.T) string {
t.Helper()

return buildExecModuleWithCFlags(t, nil)
return buildExecModuleWithCFlags(t, nil, false)
}

func buildExecModuleWithCFlags(t *testing.T, cFlags []string) string {
func buildExecModuleWithCFlags(t *testing.T, cFlags []string, forPreload bool) string {
t.Helper()

pkgConfigDeps := []string{"gio-2.0", "gio-unix-2.0"}
// t.Name() can be a subtest, so replace the directory slash to get a valid filename.
return buildCPAMModule(t, execModuleSources, pkgConfigDeps, cFlags,
"pam_authd_exec"+strings.ToLower(strings.ReplaceAll(t.Name(), "/", "_")))
"pam_authd_exec"+strings.ToLower(strings.ReplaceAll(t.Name(), "/", "_")),
forPreload)
}

func buildExecClient(t *testing.T) string {
Expand Down
2 changes: 1 addition & 1 deletion pam/integration-tests/gdm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ var testFidoDeviceUILayout = authd.UILayout{

var testPhoneAckUILayout = authd.UILayout{
Type: "form",
Label: ptrValue("Unlock your phone +33 or accept request on web interface:"),
Label: ptrValue("Unlock your phone +33... or accept request on web interface:"),
Content: ptrValue(""),
Wait: ptrValue("true"),
Button: ptrValue(""),
Expand Down
Loading
Loading