Skip to content

Commit 07f7921

Browse files
authored
Add permission checks to all install and upgrade integration tests (#4761)
* Fix calling enroll from install. * Refactor installation checks. * Add detailed checks. * Improve permission validation. * Fix lint. * Fix windows lint. * More lint. * Adjustments for <=8.13. * Pass context into CheckSuccess. * Fix upgradetest to only check installation for specific versions. * Inverse logic.
1 parent 29d5108 commit 07f7921

10 files changed

+435
-185
lines changed

testing/installtest/checks.go

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package installtest
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"os"
11+
"path/filepath"
12+
"runtime"
13+
14+
atesting "github.com/elastic/elastic-agent/pkg/testing"
15+
"github.com/elastic/elastic-agent/pkg/testing/define"
16+
)
17+
18+
func DefaultTopPath() string {
19+
var defaultBasePath string
20+
switch runtime.GOOS {
21+
case "darwin":
22+
defaultBasePath = `/Library`
23+
case "linux":
24+
defaultBasePath = `/opt`
25+
case "windows":
26+
defaultBasePath = `C:\Program Files`
27+
}
28+
return filepath.Join(defaultBasePath, "Elastic", "Agent")
29+
}
30+
31+
func CheckSuccess(ctx context.Context, f *atesting.Fixture, topPath string, unprivileged bool) error {
32+
// Use default topPath if one not defined.
33+
if topPath == "" {
34+
topPath = DefaultTopPath()
35+
}
36+
37+
_, err := os.Stat(topPath)
38+
if err != nil {
39+
return fmt.Errorf("%s missing: %w", topPath, err)
40+
}
41+
42+
// Check that a few expected installed files are present
43+
installedBinPath := filepath.Join(topPath, exeOnWindows("elastic-agent"))
44+
installedDataPath := filepath.Join(topPath, "data")
45+
installMarkerPath := filepath.Join(topPath, ".installed")
46+
47+
_, err = os.Stat(installedBinPath)
48+
if err != nil {
49+
return fmt.Errorf("%s missing: %w", installedBinPath, err)
50+
}
51+
_, err = os.Stat(installedDataPath)
52+
if err != nil {
53+
return fmt.Errorf("%s missing: %w", installedDataPath, err)
54+
}
55+
_, err = os.Stat(installMarkerPath)
56+
if err != nil {
57+
return fmt.Errorf("%s missing: %w", installMarkerPath, err)
58+
}
59+
60+
// Specific checks depending on the platform.
61+
return checkPlatform(ctx, f, topPath, unprivileged)
62+
}
63+
64+
func exeOnWindows(filename string) string {
65+
if runtime.GOOS == define.Windows {
66+
return filename + ".exe"
67+
}
68+
return filename
69+
}

testing/installtest/checks_unix.go

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
//go:build !windows
6+
7+
package installtest
8+
9+
import (
10+
"context"
11+
"fmt"
12+
"os"
13+
"os/exec"
14+
"path/filepath"
15+
"syscall"
16+
"time"
17+
18+
"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
19+
"github.com/elastic/elastic-agent/internal/pkg/agent/install"
20+
atesting "github.com/elastic/elastic-agent/pkg/testing"
21+
)
22+
23+
func checkPlatform(ctx context.Context, _ *atesting.Fixture, topPath string, unprivileged bool) error {
24+
if unprivileged {
25+
// Check that the elastic-agent user/group exist.
26+
uid, err := install.FindUID(install.ElasticUsername)
27+
if err != nil {
28+
return fmt.Errorf("failed to find %s user: %w", install.ElasticUsername, err)
29+
}
30+
gid, err := install.FindGID(install.ElasticGroupName)
31+
if err != nil {
32+
return fmt.Errorf("failed to find %s group: %w", install.ElasticGroupName, err)
33+
}
34+
35+
// Ensure entire installation tree has the correct permissions.
36+
err = validateFileTree(topPath, uint32(uid), uint32(gid))
37+
if err != nil {
38+
// context already added
39+
return err
40+
}
41+
42+
// Check that the socket is created with the correct permissions.
43+
socketPath := filepath.Join(topPath, paths.ControlSocketName)
44+
err = waitForNoError(ctx, func(_ context.Context) error {
45+
_, err = os.Stat(socketPath)
46+
if err != nil {
47+
return fmt.Errorf("failed to stat socket path %s: %w", socketPath, err)
48+
}
49+
return nil
50+
}, 3*time.Minute, 1*time.Second)
51+
info, err := os.Stat(socketPath)
52+
if err != nil {
53+
return fmt.Errorf("failed to stat socket path %s: %w", socketPath, err)
54+
}
55+
fs, ok := info.Sys().(*syscall.Stat_t)
56+
if !ok {
57+
return fmt.Errorf("failed to convert info.Sys() into *syscall.Stat_t")
58+
}
59+
if fs.Uid != uint32(uid) {
60+
return fmt.Errorf("%s not owned by %s user", socketPath, install.ElasticUsername)
61+
}
62+
if fs.Gid != uint32(gid) {
63+
return fmt.Errorf("%s not owned by %s group", socketPath, install.ElasticGroupName)
64+
}
65+
66+
// Executing `elastic-agent status` as the `elastic-agent-user` user should work.
67+
var output []byte
68+
err = waitForNoError(ctx, func(_ context.Context) error {
69+
// #nosec G204 -- user cannot inject any parameters to this command
70+
cmd := exec.Command("sudo", "-u", install.ElasticUsername, "elastic-agent", "status")
71+
output, err = cmd.CombinedOutput()
72+
if err != nil {
73+
return fmt.Errorf("elastic-agent status failed: %w (output: %s)", err, output)
74+
}
75+
return nil
76+
}, 3*time.Minute, 1*time.Second)
77+
78+
// Executing `elastic-agent status` as the original user should fail, because that
79+
// user is not in the 'elastic-agent' group.
80+
originalUser := os.Getenv("SUDO_USER")
81+
if originalUser != "" {
82+
// #nosec G204 -- user cannot inject any parameters to this command
83+
cmd := exec.Command("sudo", "-u", originalUser, "elastic-agent", "status")
84+
output, err := cmd.CombinedOutput()
85+
if err == nil {
86+
return fmt.Errorf("sudo -u %s elastic-agent didn't fail: got output: %s", originalUser, output)
87+
}
88+
}
89+
} else {
90+
// Ensure entire installation tree has the correct permissions.
91+
err := validateFileTree(topPath, 0, 0)
92+
if err != nil {
93+
// context already added
94+
return err
95+
}
96+
}
97+
return nil
98+
}
99+
100+
func validateFileTree(dir string, uid uint32, gid uint32) error {
101+
return filepath.Walk(dir, func(file string, info os.FileInfo, err error) error {
102+
if err != nil {
103+
return fmt.Errorf("error traversing the file tree: %w", err)
104+
}
105+
if info.Mode().Type() == os.ModeSymlink {
106+
// symlink don't check permissions
107+
return nil
108+
}
109+
fs, ok := info.Sys().(*syscall.Stat_t)
110+
if !ok {
111+
return fmt.Errorf("failed to convert info.Sys() into *syscall.Stat_t")
112+
}
113+
if fs.Uid != uid {
114+
return fmt.Errorf("%s doesn't have correct uid: has %d (expected %d)", file, fs.Uid, uid)
115+
}
116+
if fs.Gid != gid {
117+
return fmt.Errorf("%s doesn't have correct gid: has %d (expected %d)", file, fs.Gid, gid)
118+
}
119+
if fs.Mode&0007 != 0 {
120+
return fmt.Errorf("%s has world access", file)
121+
}
122+
return nil
123+
})
124+
}
125+
126+
func waitForNoError(ctx context.Context, fun func(ctx context.Context) error, timeout time.Duration, interval time.Duration) error {
127+
ctx, cancel := context.WithTimeout(ctx, timeout)
128+
defer cancel()
129+
130+
t := time.NewTicker(interval)
131+
defer t.Stop()
132+
133+
var lastErr error
134+
for {
135+
select {
136+
case <-ctx.Done():
137+
if lastErr != nil {
138+
return lastErr
139+
}
140+
return ctx.Err()
141+
case <-t.C:
142+
err := fun(ctx)
143+
if err == nil {
144+
return nil
145+
}
146+
lastErr = err
147+
}
148+
}
149+
}

testing/installtest/checks_windows.go

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
//go:build windows
6+
7+
package installtest
8+
9+
import (
10+
"context"
11+
"fmt"
12+
"reflect"
13+
"syscall"
14+
"unsafe"
15+
16+
"golang.org/x/sys/windows"
17+
18+
"github.com/elastic/elastic-agent/internal/pkg/agent/install"
19+
atesting "github.com/elastic/elastic-agent/pkg/testing"
20+
)
21+
22+
const ACCESS_ALLOWED_ACE_TYPE = 0
23+
const ACCESS_DENIED_ACE_TYPE = 1
24+
25+
var (
26+
advapi32 = syscall.NewLazyDLL("advapi32.dll")
27+
28+
procGetAce = advapi32.NewProc("GetAce")
29+
)
30+
31+
type accessAllowedAce struct {
32+
AceType uint8
33+
AceFlags uint8
34+
AceSize uint16
35+
AccessMask uint32
36+
SidStart uint32
37+
}
38+
39+
func checkPlatform(ctx context.Context, f *atesting.Fixture, topPath string, unprivileged bool) error {
40+
secInfo, err := windows.GetNamedSecurityInfo(topPath, windows.SE_FILE_OBJECT, windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION)
41+
if err != nil {
42+
return fmt.Errorf("GetNamedSecurityInfo failed for %s: %w", topPath, err)
43+
}
44+
if !secInfo.IsValid() {
45+
return fmt.Errorf("GetNamedSecurityInfo result is not valid for %s: %w", topPath, err)
46+
}
47+
owner, _, err := secInfo.Owner()
48+
if err != nil {
49+
return fmt.Errorf("secInfo.Owner() failed for %s: %w", topPath, err)
50+
}
51+
sids, err := getAllowedSIDs(secInfo)
52+
if err != nil {
53+
return fmt.Errorf("failed to get allowed SID's for %s: %w", topPath, err)
54+
}
55+
if unprivileged {
56+
// Check that the elastic-agent user/group exist.
57+
uid, err := install.FindUID(install.ElasticUsername)
58+
if err != nil {
59+
return fmt.Errorf("failed to find %s user: %w", install.ElasticUsername, err)
60+
}
61+
uidSID, err := windows.StringToSid(uid)
62+
if err != nil {
63+
return fmt.Errorf("failed to convert string to windows.SID %s: %w", uid, err)
64+
}
65+
gid, err := install.FindGID(install.ElasticGroupName)
66+
if err != nil {
67+
return fmt.Errorf("failed to find %s group: %w", install.ElasticGroupName, err)
68+
}
69+
gidSID, err := windows.StringToSid(gid)
70+
if err != nil {
71+
return fmt.Errorf("failed to convert string to windows.SID %s: %w", uid, err)
72+
}
73+
if !owner.Equals(uidSID) {
74+
return fmt.Errorf("%s not owned by %s user", topPath, install.ElasticUsername)
75+
}
76+
if !hasSID(sids, uidSID) {
77+
return fmt.Errorf("path %s should have ACE for %s user", topPath, install.ElasticUsername)
78+
}
79+
if !hasSID(sids, gidSID) {
80+
return fmt.Errorf("path %s should have ACE for %s group", topPath, install.ElasticGroupName)
81+
}
82+
// administrators should have access as well
83+
if !hasWellKnownSID(sids, windows.WinBuiltinAdministratorsSid) {
84+
return fmt.Errorf("path %s should have ACE for Administrators", topPath)
85+
}
86+
// that is 3 unique SID's, it should not have anymore
87+
if len(sids) > 3 {
88+
return fmt.Errorf("DACL has more than allowed ACE for %s", topPath)
89+
}
90+
} else {
91+
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
92+
return fmt.Errorf("%s not owned by Administrators", topPath)
93+
}
94+
// that is 1 unique SID, it should not have anymore
95+
if len(sids) > 1 {
96+
return fmt.Errorf("DACL has more than allowed ACE for %s", topPath)
97+
}
98+
}
99+
return nil
100+
}
101+
102+
func hasSID(sids []*windows.SID, m *windows.SID) bool {
103+
for _, s := range sids {
104+
if s.Equals(m) {
105+
return true
106+
}
107+
}
108+
return false
109+
}
110+
111+
func appendSID(sids []*windows.SID, a *windows.SID) []*windows.SID {
112+
if hasSID(sids, a) {
113+
return sids
114+
}
115+
return append(sids, a)
116+
}
117+
118+
func hasWellKnownSID(sids []*windows.SID, m windows.WELL_KNOWN_SID_TYPE) bool {
119+
for _, s := range sids {
120+
if s.IsWellKnown(m) {
121+
return true
122+
}
123+
}
124+
return false
125+
}
126+
127+
func getAllowedSIDs(secInfo *windows.SECURITY_DESCRIPTOR) ([]*windows.SID, error) {
128+
dacl, _, err := secInfo.DACL()
129+
if err != nil {
130+
return nil, fmt.Errorf("secInfo.DACL() failed: %w", err)
131+
}
132+
if dacl == nil {
133+
return nil, fmt.Errorf("no DACL set")
134+
}
135+
136+
var sids []*windows.SID
137+
138+
// sadly the ACL information is not exported so reflect is needed to get the aceCount
139+
// it's always field #3 because it's defined by the Windows API (so no real need to worry about it changing)
140+
rs := reflect.ValueOf(dacl).Elem()
141+
aceCount := rs.Field(3).Uint()
142+
for i := uint64(0); i < aceCount; i++ {
143+
ace := &accessAllowedAce{}
144+
ret, _, _ := procGetAce.Call(uintptr(unsafe.Pointer(dacl)), uintptr(i), uintptr(unsafe.Pointer(&ace)))
145+
if ret == 0 {
146+
return nil, fmt.Errorf("while getting ACE: %w", windows.GetLastError())
147+
}
148+
if ace.AceType == ACCESS_DENIED_ACE_TYPE {
149+
// we never set denied ACE, something is wrong
150+
return nil, fmt.Errorf("denied ACE found (should not be set)")
151+
}
152+
if ace.AceType != ACCESS_ALLOWED_ACE_TYPE {
153+
// unknown ace type
154+
return nil, fmt.Errorf("unknown AceType: %d", ace.AceType)
155+
}
156+
aceSid := (*windows.SID)(unsafe.Pointer(&ace.SidStart))
157+
sids = appendSID(sids, aceSid)
158+
}
159+
return sids, nil
160+
}

0 commit comments

Comments
 (0)