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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions cmd/adsysd/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ type daemonConfig struct {
SystemUnitDir string `mapstructure:"systemunit_dir"`
GlobalTrustDir string `mapstructure:"global_trust_dir"`

AdBackend string `mapstructure:"ad_backend"`
SSSdConfig sss.Config `mapstructure:"sssd"`
WinbindConfig winbind.Config `mapstructure:"winbind"`
GpoListTimeout int `mapstructure:"gpo_list_timeout"`
AdBackend string `mapstructure:"ad_backend"`
CertificateEnrollment string `mapstructure:"certificate_enrollment"`
SSSdConfig sss.Config `mapstructure:"sssd"`
WinbindConfig winbind.Config `mapstructure:"winbind"`
GpoListTimeout int `mapstructure:"gpo_list_timeout"`

ServiceTimeout int `mapstructure:"service_timeout"`
}
Expand Down Expand Up @@ -124,6 +125,7 @@ func New() *App {
adsysservice.WithSystemUnitDir(a.config.SystemUnitDir),
adsysservice.WithGlobalTrustDir(a.config.GlobalTrustDir),
adsysservice.WithADBackend(a.config.AdBackend),
adsysservice.WithCertificateEnrollment(a.config.CertificateEnrollment),
adsysservice.WithSSSConfig(a.config.SSSdConfig),
adsysservice.WithWinbindConfig(a.config.WinbindConfig),
adsysservice.WithGpoListTimeout(time.Second*time.Duration(a.config.GpoListTimeout)),
Expand Down Expand Up @@ -172,6 +174,9 @@ func New() *App {
a.rootCmd.PersistentFlags().StringP("ad-backend", "", "sssd", gotext.Get("Active Directory authentication backend"))
err = a.viper.BindPFlag("ad_backend", a.rootCmd.PersistentFlags().Lookup("ad-backend"))
decorate.LogOnError(&err)
a.rootCmd.PersistentFlags().StringP("certificate-enrollment", "", consts.DefaultCertificateEnrollment, gotext.Get("Certificate enrollment method (ldap or cepces)"))
err = a.viper.BindPFlag("certificate_enrollment", a.rootCmd.PersistentFlags().Lookup("certificate-enrollment"))
decorate.LogOnError(&err)
a.rootCmd.PersistentFlags().StringP("sssd.config", "", consts.DefaultSSSConf, gotext.Get("SSSd config file path"))
err = a.viper.BindPFlag("sssd.config", a.rootCmd.PersistentFlags().Lookup("sssd.config"))
decorate.LogOnError(&err)
Expand Down
78 changes: 76 additions & 2 deletions cmd/adsysd/integration_tests/adsys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
"time"
Expand All @@ -33,6 +35,15 @@ var (
)

func TestMain(m *testing.M) {
// The integration tests run the daemon against a guest-only smbd with no
// real KDC, relying on libsmbclient falling back from kerberos to
// anonymous. Newer Samba (>= 4.23) dropped that implicit fallback, so we
// tell the daemon to skip kerberos entirely. This env var is only honored
// by the daemon for tests and is propagated to re-exec'd helper processes.
if err := os.Setenv("ADSYS_TESTS_WITHOUT_KERBEROS", "1"); err != nil {
log.Fatalf("Setup: can't disable kerberos for tests: %v", err)
}

if os.Getenv("ADSYS_SKIP_INTEGRATION_TESTS") != "" {
fmt.Println("Integration tests skipped as requested")
return
Expand Down Expand Up @@ -193,6 +204,7 @@ type confOptions struct {
adsysDir string
backend string
detectCachedTicket bool
certEnrollment string
}

func confWithAdsysDir(adsysDir string) confOption {
Expand Down Expand Up @@ -263,6 +275,13 @@ detect_cached_ticket: %[3]t
`, args.adsysDir, args.backend, args.detectCachedTicket))

testutils.WriteFile(t, confFile, confData, os.ModePerm)
if args.certEnrollment != "" {
f, err := os.OpenFile(confFile, os.O_APPEND|os.O_WRONLY, 0600)
require.NoError(t, err, "Setup: should open config to append certificate enrollment")
_, err = fmt.Fprintf(f, "\ncertificate_enrollment: %s\n", args.certEnrollment)
require.NoError(t, err, "Setup: should append certificate enrollment to config")
require.NoError(t, f.Close(), "Setup: should close config after appending")
}
require.NoError(t, os.MkdirAll(filepath.Join(args.adsysDir, "dconf"), 0750), "Setup: should create dconf dir")
// Don’t create empty dirs for sudo and polkit: todo: same for dconf?

Expand Down Expand Up @@ -402,6 +421,15 @@ func runDaemons() (teardown func()) {
answers[mode] = filepath.Join(dir, mode)
}

// The mock containers resolve the connecting user through the bind-mounted
// /etc/passwd and /etc/group. When the tests run as a user that is only
// known through NSS (e.g. an LDAP/SSSD user not present in the local files),
// the container's D-Bus daemon can't resolve the UID and resets the
// connection. Generate augmented databases that also contain the current
// user and group so the mocks work regardless of the NSS backend.
passwdFile := writeUserDatabase(dir, "passwd", "/etc/passwd")
groupFile := writeUserDatabase(dir, "group", "/etc/group")

var errsDocker error
var wg sync.WaitGroup
for answer, socketDir := range answers {
Expand All @@ -422,8 +450,8 @@ func runDaemons() (teardown func()) {
"run", "--rm", "--pid", "host",
"--name", containerName+answer,
"--volume", fmt.Sprintf("%s:%s:ro", adsysActionsDir, "/usr/share/polkit-1/actions.orig"),
"--volume", `/etc/group:/etc/group:ro`,
"--volume", `/etc/passwd:/etc/passwd:ro`,
"--volume", fmt.Sprintf("%s:/etc/group:ro", groupFile),
"--volume", fmt.Sprintf("%s:/etc/passwd:ro", passwdFile),
"--volume", fmt.Sprintf("%s:/dbus/", socketDir),
dockerSystemDaemonsImage,
answer,
Expand Down Expand Up @@ -474,6 +502,52 @@ func runDaemons() (teardown func()) {
}
}

// writeUserDatabase copies the host NSS database file (/etc/passwd or /etc/group)
// into dir and ensures the current user and group are present so that the mock
// containers can resolve the connecting UID even when it is only provided through
// NSS (e.g. an LDAP/SSSD user not listed in the local files). It returns the path
// to the generated file.
func writeUserDatabase(dir, name, src string) string {
content, err := os.ReadFile(src)
if err != nil {
log.Fatalf("Setup: can't read %s: %v", src, err)
}

// Resolve the current user (passwd) or group (group) entry through NSS and
// append it if it isn't already provided by the local file.
var id, entry string
switch name {
case "passwd":
id = strconv.Itoa(os.Getuid())
case "group":
id = strconv.Itoa(os.Getgid())
}

// #nosec G204: name and id are controlled by the test.
out, err := exec.Command("getent", name, id).Output()
if err == nil {
entry = strings.TrimSpace(string(out))
}
// Only append when getent returned a well-formed entry (field:...); a
// missing ':' means an unexpected format, so skip rather than slice blindly.
if colon := strings.Index(entry, ":"); entry != "" && colon != -1 {
field := entry[:colon+1]
if !bytes.Contains(content, []byte("\n"+field)) && !bytes.HasPrefix(content, []byte(field)) {
if len(content) > 0 && content[len(content)-1] != '\n' {
content = append(content, '\n')
}
content = append(content, []byte(entry+"\n")...)
}
}

dest := filepath.Join(dir, name)
//nolint:gosec // G306: mirrors the world-readable 0644 of /etc/passwd and /etc/group that nss_wrapper consumers expect
if err := os.WriteFile(dest, content, 0644); err != nil {
log.Fatalf("Setup: can't write %s database: %v", name, err)
}
return dest
}

// dbusAnswer will flip to which polkit and systemd mock to communicate to:
// - yes for polkit always authorizing our actions, with a harcoded startup time and next refresh unit time.
// - no for polkit always denying our actions, with a harcoded startup time and next refresh unit time.
Expand Down
92 changes: 77 additions & 15 deletions cmd/adsysd/integration_tests/adsysctl_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ func TestPolicyUpdate(t *testing.T) {
krb5MockBehavior string
purge bool
missingCertmonger bool
missingCepces bool
noExportKrb5cc bool
detectCachedTicket bool

Expand Down Expand Up @@ -634,7 +635,7 @@ func TestPolicyUpdate(t *testing.T) {
initState: "localhost-uptodate",
systemAnswer: "no_proxy_object",
},
"Does not error when certmonger or cepces is not available": {
"Does not error when certmonger is not available": {
args: []string{"-m"},
krb5ccname: "-",
krb5ccNamesState: []krb5ccNamesWithState{
Expand All @@ -649,6 +650,21 @@ func TestPolicyUpdate(t *testing.T) {
},
missingCertmonger: true,
},
"Does not error when cepces is not available": {
args: []string{"-m"},
krb5ccname: "-",
krb5ccNamesState: []krb5ccNamesWithState{
{
src: "ccache_EXAMPLE.COM",
machine: true,
},
},
initState: "localhost-uptodate",
addPaths: []string{
"lib/private", // make parent of private dir a file
},
missingCepces: true,
},

// Purge cases
"Purge current user policies": {
Expand Down Expand Up @@ -1044,14 +1060,23 @@ func TestPolicyUpdate(t *testing.T) {
}
t.Setenv("ADSYS_WBCLIENT_BEHAVIOR", tc.winbindMockBehavior)

// Create fake certmonger and cepces binaries for the certificate manager
// Create fake certmonger and cepces binaries for the certificate manager.
// Both are required for the cert-autoenroll script to perform enrollment;
// each can be omitted independently to exercise the graceful skip path.
binDir := t.TempDir()
if !tc.missingCertmonger {
binDir := t.TempDir()
for _, executable := range []string{"getcert", "cepces-submit"} {
// #nosec G306. We want this asset to be executable.
err := os.WriteFile(filepath.Join(binDir, executable), []byte("#!/bin/sh\necho $@\n"), 0755)
require.NoError(t, err, "Setup: could not create %q binary", executable)
}
// #nosec G306. We want this asset to be executable.
err := os.WriteFile(filepath.Join(binDir, "getcert"), []byte("#!/bin/sh\necho $@\n"), 0755)
require.NoError(t, err, "Setup: could not create getcert binary")
}
if !tc.missingCepces {
// #nosec G306. We want this asset to be executable.
err := os.WriteFile(filepath.Join(binDir, "cepces-submit"), []byte("#!/bin/sh\necho $@\n"), 0755)
require.NoError(t, err, "Setup: could not create cepces-submit binary")
}
if tc.missingCertmonger || tc.missingCepces {
isolatePolicyUpdatePath(t, binDir)
} else {
t.Setenv("PATH", binDir+":"+os.Getenv("PATH"))
}

Expand Down Expand Up @@ -1217,13 +1242,10 @@ func TestPolicyDebugScriptDump(t *testing.T) {

wantErr bool
}{
"Get adsys-gpolist script": {script: "adsys-gpolist", cmdName: "gpolist-script", path: "internal/ad", systemAnswer: "polkit_yes"},
"Get cert-autoenroll script": {script: "cert-autoenroll", cmdName: "cert-autoenroll-script", path: "internal/policies/certificate", systemAnswer: "polkit_yes"},
"adsys-gpolist is always authorized": {script: "adsys-gpolist", cmdName: "gpolist-script", path: "internal/ad", systemAnswer: "polkit_no"},
"cert-autoenroll is always authorized": {script: "cert-autoenroll", cmdName: "cert-autoenroll-script", path: "internal/policies/certificate", systemAnswer: "polkit_no"},
"Get adsys-gpolist script": {script: "adsys-gpolist", cmdName: "gpolist-script", path: "internal/ad", systemAnswer: "polkit_yes"},
"adsys-gpolist is always authorized": {script: "adsys-gpolist", cmdName: "gpolist-script", path: "internal/ad", systemAnswer: "polkit_no"},

"Error on daemon not responding for adsys-gpolist": {script: "adsys-gpolist", cmdName: "gpolist-script", path: "internal/ad", daemonNotStarted: true, wantErr: true},
"Error on daemon not responding for cert-autoenroll": {script: "cert-autoenroll", cmdName: "cert-autoenroll-script", path: "internal/policies/certificate", daemonNotStarted: true, wantErr: true},
"Error on daemon not responding for adsys-gpolist": {script: "adsys-gpolist", cmdName: "gpolist-script", path: "internal/ad", daemonNotStarted: true, wantErr: true},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
Expand Down Expand Up @@ -1437,17 +1459,28 @@ func modifyAndAddUsers(t *testing.T, newUsername string, users ...string) (passw
require.NoError(t, err, "Setup: can’t get group for current user")
group := groups[0]

var replaced bool
scanner := bufio.NewScanner(f)
for scanner.Scan() {
l := scanner.Text()
if strings.HasPrefix(l, fmt.Sprintf("%s:", u.Username)) {
l = fmt.Sprintf("%s%s", newUsername, strings.TrimPrefix(l, u.Username))
replaced = true
}
_, err = d.Write([]byte(l + "\n"))
require.NoError(t, err, "Setup: can’t write to passwd temp file")
}
require.NoError(t, scanner.Err(), "Setup: can't write temporary passwd file")

// When the current user is only known through NSS (e.g. an LDAP/SSSD user
// not present in /etc/passwd), the loop above can't find and rename its
// entry. Synthesize one from the NSS-resolved data so that the current
// UID resolves to newUsername inside the nss_wrapper database.
if !replaced {
_, err = fmt.Fprintf(d, "%s:x:%s:%s:%s:%s:/usr/bin/bash\n", newUsername, u.Uid, u.Gid, u.Name, u.HomeDir)
require.NoError(t, err, "Setup: can’t write current user to passwd temp file")
}

for i, u := range users {
_, err = fmt.Fprintf(d, "%s:x:%d:%s::/nonexistent:/usr/bin/false", u, i+23450, group)
require.NoError(t, err, "Setup: can’t write to passwd temp file")
Expand All @@ -1456,6 +1489,29 @@ func modifyAndAddUsers(t *testing.T, newUsername string, users ...string) (passw
return dest
}

func isolatePolicyUpdatePath(t *testing.T, binDir string) {
t.Helper()

for _, command := range []string{"mkdir", "dconf", "apparmor_parser", "python3"} {
exposeCommandOnPath(t, binDir, command)
}
if _, err := exec.LookPath("python3-coverage"); err == nil {
exposeCommandOnPath(t, binDir, "python3-coverage")
}

t.Setenv("PATH", binDir)
}

func exposeCommandOnPath(t *testing.T, dir, command string) {
t.Helper()

src, err := exec.LookPath(command)
require.NoErrorf(t, err, "Setup: could not find %s on PATH", command)

err = os.Symlink(src, filepath.Join(dir, command))
require.NoErrorf(t, err, "Setup: could not expose %s in isolated PATH", command)
}

// setupSubprocessForTest prepares a subprocess with a mock passwd file for running the tests.
// Returns false if we are already in the subprocess and should continue.
// Returns true if we prepare the subprocess and reexec ourself.
Expand Down Expand Up @@ -1512,6 +1568,12 @@ func setupSubprocessForTest(t *testing.T, currentUser string, otherUsers ...stri

passwd := modifyAndAddUsers(t, currentUser, otherUsers...)

// nss_wrapper resolves groups through NSS_WRAPPER_GROUP. When the current
// user's primary group is only known through NSS (e.g. an LDAP/SSSD group
// not present in /etc/group), copy the local file and append the
// NSS-resolved entry so that group lookups succeed inside the subprocess.
group := writeUserDatabase(t.TempDir(), "group", "/etc/group")

// Setup correct child environment, including LD_PRELOAD for nss mock
cmd.Env = append(os.Environ(),
"GO_WANT_HELPER_PROCESS=1",
Expand All @@ -1522,7 +1584,7 @@ func setupSubprocessForTest(t *testing.T, currentUser string, otherUsers ...stri
// override user and host database
fmt.Sprintf("LD_PRELOAD=libnss_wrapper.so:%s:%s", mockWinbindLibPath, mockKrb5LibPath),
fmt.Sprintf("NSS_WRAPPER_PASSWD=%s", passwd),
"NSS_WRAPPER_GROUP=/etc/group",
fmt.Sprintf("NSS_WRAPPER_GROUP=%s", group),
)
// dbus addresses to be reset in child
for _, mode := range dbusAnswerModes {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/usr/bin/baz {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/usr/bin/bar {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/usr/bin/foo {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
^adsystestuser@example.com {
/etc/environment r,
@{HOMEDIRS}/.xauth* w,
/usr/bin/{,b,d,rb}ash Ux,
/usr/bin/{c,k,tc}sh Ux,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[org/gnome/desktop/interface]
clock-format='24h'
clock-show-date=false
clock-show-weekday=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/org/gnome/desktop/interface/clock-format
/org/gnome/desktop/interface/clock-show-date
/org/gnome/desktop/interface/clock-show-weekday
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
user-db:user
system-db:gdm
system-db:machine
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
new content
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TDB file
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// This file is managed by adsys.
// Do not edit this file manually.
// Any changes will be overwritten.

polkit.addAdminRule(function(action, subject){
return ["unix-user:bob@example.com","unix-group:mygroup@example2.com"];
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
final machine script
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
script user logon
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
script user logoff
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
script machine shutdown
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
script machine startup
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
script user logon
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
subfolder other script
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
unreferenced data
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
unreferenced script
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
scripts/script-machine-startup
scripts/subfolder/other-script
Loading