Skip to content

Commit 9262a39

Browse files
authored
Merge branch 'master' into interrupt
2 parents e71d971 + ccf97fb commit 9262a39

18 files changed

+467
-31
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,26 @@ beta releases are not included in this history.
1414

1515
[//]: # (begin_release_notes)
1616

17+
"1.49.0" (2023-10-17)
18+
=====================
19+
20+
Features
21+
--------
22+
23+
- :sparkles: `[environment]` Expose utilities for parsing and finding environment variables (#20231013160208)
24+
- :sparkles: `[collection]` Added `AllNotEmpty` and `AnyEmpty` to check the content of string slices (#20231016164922)
25+
- :sparkles: `[collection]` Added `FindInSlice` to extend `Find` search capabilities (#20231016173631)
26+
- :sparkles: `[platform]` Added a way to determine if a user has admin rights (i.e. root or superuser on Linux and administrator on Windows) (#20231016175953)
27+
- :sparkles: `[command]` Added a way to prepend commands (using `Prepend()`) to command wrappers in order to chain command wrappers easily (#20231016204910)
28+
- :sparkles: `[platform]` Added `WithPrivileges` so that commands are elevated with privileges depending on the permissions of the current user (#20231016234712)
29+
30+
31+
Bugfixes
32+
--------
33+
34+
- :bug: `[environment]` Fix environment variables parsing when an entry is incorrect (#20231013160247)
35+
36+
1737
"1.48.0" (2023-10-13)
1838
=====================
1939

changes/20231013160208.feature

Lines changed: 0 additions & 1 deletion
This file was deleted.

changes/20231013160247.bugfix

Lines changed: 0 additions & 1 deletion
This file was deleted.

utils/collection/search.go

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,78 @@
44
*/
55
package collection
66

7+
import "strings"
8+
79
// Find looks for an element in a slice. If found it will
810
// return its index and true; otherwise it will return -1 and false.
911
func Find(slice *[]string, val string) (int, bool) {
10-
for i, item := range *slice {
11-
if item == val {
12-
return i, true
12+
if slice == nil {
13+
return -1, false
14+
}
15+
return FindInSlice(true, *slice, val)
16+
}
17+
18+
// FindInSlice finds if any values val are present in the slice and if so returns the first index.
19+
// if strict, it check of an exact match; otherwise discard whitespaces and case.
20+
func FindInSlice(strict bool, slice []string, val ...string) (int, bool) {
21+
if len(val) == 0 || len(slice) == 0 {
22+
return -1, false
23+
}
24+
25+
inSlice := make(map[string]int, len(slice))
26+
for i := range slice {
27+
item := slice[i]
28+
if !strict {
29+
item = strings.ToLower(strings.TrimSpace(item))
30+
}
31+
if _, ok := inSlice[item]; !ok {
32+
inSlice[item] = i
33+
}
34+
}
35+
36+
for i := range val {
37+
item := val[i]
38+
if !strict {
39+
item = strings.ToLower(strings.TrimSpace(item))
40+
}
41+
if idx, ok := inSlice[item]; ok {
42+
return idx, true
1343
}
1444
}
45+
1546
return -1, false
1647
}
1748

49+
// Any returns true if there is at least one element of the slice which is true.
1850
func Any(slice []bool) bool {
19-
for _, v := range slice {
20-
if v {
51+
for i := range slice {
52+
if slice[i] {
2153
return true
2254
}
2355
}
2456
return false
2557
}
2658

59+
// AnyEmpty returns whether there is one entry in the slice which is empty.
60+
// If strict, then whitespaces are considered as empty strings
61+
func AnyEmpty(strict bool, slice []string) bool {
62+
_, found := FindInSlice(!strict, slice, "")
63+
return found
64+
}
65+
66+
// All returns true if all items of the slice are true.
2767
func All(slice []bool) bool {
28-
for _, v := range slice {
29-
if !v {
68+
for i := range slice {
69+
if !slice[i] {
3070
return false
3171
}
3272
}
3373
return true
3474
}
75+
76+
// AllNotEmpty returns whether all elements of the slice are not empty.
77+
// If strict, then whitespaces are considered as empty strings
78+
func AllNotEmpty(strict bool, slice []string) bool {
79+
_, found := FindInSlice(!strict, slice, "")
80+
return !found
81+
}

utils/collection/search_test.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ package collection
77
import (
88
"testing"
99

10+
"github.com/bxcodec/faker/v3"
1011
"github.com/stretchr/testify/assert"
1112
)
1213

1314
func TestFind(t *testing.T) {
14-
index, found := Find(&[]string{"A", "b", "c"}, "D")
15+
index, found := Find(nil, "D")
16+
assert.False(t, found)
17+
assert.Equal(t, -1, index)
18+
index, found = Find(&[]string{"A", "b", "c"}, "D")
1519
assert.False(t, found)
1620
assert.Equal(t, -1, index)
1721

@@ -20,6 +24,39 @@ func TestFind(t *testing.T) {
2024
assert.Equal(t, 2, index)
2125
}
2226

27+
func TestFindInSlice(t *testing.T) {
28+
index, found := FindInSlice(true, nil, "D")
29+
assert.False(t, found)
30+
assert.Equal(t, -1, index)
31+
index, found = FindInSlice(true, []string{"A", "b", "c"})
32+
assert.False(t, found)
33+
assert.Equal(t, -1, index)
34+
index, found = FindInSlice(true, []string{"A", "b", "c"}, "D")
35+
assert.False(t, found)
36+
assert.Equal(t, -1, index)
37+
index, found = FindInSlice(true, []string{"A", "b", "c"}, "D", "e", "f", "g", "H")
38+
assert.False(t, found)
39+
assert.Equal(t, -1, index)
40+
index, found = FindInSlice(false, []string{"A", "b", "c"}, "D")
41+
assert.False(t, found)
42+
assert.Equal(t, -1, index)
43+
index, found = FindInSlice(false, []string{"A", "b", "c"}, "D", "e", "f", "g", "H")
44+
assert.False(t, found)
45+
assert.Equal(t, -1, index)
46+
index, found = FindInSlice(true, []string{"A", "B", "b", "c"}, "b")
47+
assert.True(t, found)
48+
assert.Equal(t, 2, index)
49+
index, found = FindInSlice(false, []string{"A", "B", "b", "c"}, "b")
50+
assert.True(t, found)
51+
assert.Equal(t, 1, index)
52+
index, found = FindInSlice(true, []string{"A", "B", "b", "c"}, "b", "D", "e", "f", "g", "H")
53+
assert.True(t, found)
54+
assert.Equal(t, 2, index)
55+
index, found = FindInSlice(false, []string{"A", "B", "b", "c"}, "b", "D", "e", "f", "g", "H")
56+
assert.True(t, found)
57+
assert.Equal(t, 1, index)
58+
}
59+
2360
func TestAny(t *testing.T) {
2461
assert.True(t, Any([]bool{false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false}))
2562
assert.False(t, Any([]bool{false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}))
@@ -33,3 +70,23 @@ func TestAll(t *testing.T) {
3370
assert.True(t, All([]bool{true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true}))
3471
assert.False(t, All([]bool{true, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true}))
3572
}
73+
74+
func TestAnyEmpty(t *testing.T) {
75+
assert.True(t, AnyEmpty(false, []string{faker.Username(), faker.Name(), faker.Sentence(), ""}))
76+
assert.False(t, AnyEmpty(false, []string{faker.Username(), " ", faker.Name(), faker.Sentence()}))
77+
assert.True(t, AnyEmpty(true, []string{faker.Username(), " ", faker.Name(), faker.Sentence()}))
78+
assert.True(t, AnyEmpty(false, []string{"", faker.Name(), faker.Sentence()}))
79+
assert.True(t, AnyEmpty(false, []string{faker.Username(), "", faker.Name(), faker.Sentence()}))
80+
assert.True(t, AnyEmpty(false, []string{faker.Username(), "", faker.Name(), "", faker.Sentence()}))
81+
assert.False(t, AnyEmpty(false, []string{faker.Username(), faker.Name(), faker.Sentence()}))
82+
}
83+
84+
func TestAllNotEmpty(t *testing.T) {
85+
assert.False(t, AllNotEmpty(false, []string{faker.Username(), faker.Name(), faker.Sentence(), ""}))
86+
assert.False(t, AllNotEmpty(false, []string{"", faker.Name(), faker.Sentence()}))
87+
assert.False(t, AllNotEmpty(false, []string{faker.Username(), "", faker.Name(), faker.Sentence()}))
88+
assert.True(t, AllNotEmpty(false, []string{faker.Username(), " ", faker.Name(), faker.Sentence()}))
89+
assert.False(t, AllNotEmpty(true, []string{faker.Username(), " ", faker.Name(), faker.Sentence()}))
90+
assert.False(t, AllNotEmpty(false, []string{faker.Username(), "", faker.Name(), "", faker.Sentence()}))
91+
assert.True(t, AllNotEmpty(false, []string{faker.Username(), faker.Name(), faker.Sentence()}))
92+
}

utils/module.properties

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Version=1.48.0
1+
Version=1.49.0
22
MajorVersion=1
3-
MinorVersion=48
3+
MinorVersion=49
44
PatchVersion=0
5-
CommitHash=3aa6349c524af72e3eb4eb96321c162f071e69e5
5+
CommitHash=252a7ebceb348952f19c508ee71a4cb586e8edeb

utils/platform/cmd.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package platform
2+
3+
import "github.com/ARM-software/golang-utils/utils/subprocess/command"
4+
5+
// WithPrivileges redefines a command so that it is run with elevated privileges.
6+
// For instance, on Linux, if the current user has enough privileges, the command will be run as is.
7+
// Otherwise, `sudo` will be used if defined as the sudo (See `DefineSudoCommand`).
8+
// Similar scenario will happen on Windows, although the elevated command is defined using `DefineSudoCommand`.
9+
func WithPrivileges(cmd *command.CommandAsDifferentUser) (cmdWithPrivileges *command.CommandAsDifferentUser) {
10+
cmdWithPrivileges = cmd
11+
if cmdWithPrivileges == nil {
12+
cmdWithPrivileges = command.NewCommandAsDifferentUser()
13+
}
14+
hasPrivileges, err := IsCurrentUserAnAdmin()
15+
if err != nil {
16+
return
17+
}
18+
if !hasPrivileges {
19+
cmdWithPrivileges.Prepend(getRunCommandWithPrivileges())
20+
}
21+
return
22+
}

utils/platform/cmd_posix.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@ var (
1111
sudoCommand = command.Sudo()
1212
)
1313

14-
// DefineSudoCommand defines the command to run to be `root` or a user with enough privileges to manage accounts.
14+
// DefineSudoCommand defines the command to run to be `root` or a user with enough privileges (superuser).
1515
// e.g.
1616
// - args="sudo" to run commands as `root`
1717
// - args="su", "tom" if `tom` has enough privileges to run the command
18-
// - args="gosu", "tom" if `tom` has enough privileges to run the command in a container and `gosu` is installed
1918
func DefineSudoCommand(args ...string) {
2019
sudoCommand = command.NewCommandAsDifferentUser(args...)
2120
}
2221

23-
func defineCommandWithPrivileges(args ...string) (string, []string) {
24-
return sudoCommand.RedefineCommand(args...)
22+
func getRunCommandWithPrivileges() *command.CommandAsDifferentUser {
23+
return sudoCommand
2524
}

utils/platform/cmd_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package platform
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"testing"
7+
8+
"github.com/bxcodec/faker/v3"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/ARM-software/golang-utils/utils/subprocess/command"
13+
)
14+
15+
func TestWithPrivileges(t *testing.T) {
16+
admin, err := IsCurrentUserAnAdmin()
17+
require.NoError(t, err)
18+
name := faker.Username()
19+
cmd := WithPrivileges(command.Gosu(name)).RedefineInShellForm("test", "1", "2", "3")
20+
if admin {
21+
assert.Equal(t, fmt.Sprintf("gosu %v test 1 2 3", name), cmd)
22+
} else {
23+
assert.True(t, strings.Contains(cmd, fmt.Sprintf("gosu %v test 1 2 3", name)))
24+
assert.False(t, strings.HasPrefix(cmd, fmt.Sprintf("gosu %v test 1 2 3", name)))
25+
}
26+
cmd = WithPrivileges(nil).RedefineInShellForm("test", "1", "2", "3")
27+
if admin {
28+
assert.Equal(t, "test 1 2 3", cmd)
29+
} else {
30+
assert.True(t, strings.Contains(cmd, "test 1 2 3"))
31+
assert.False(t, strings.HasPrefix(cmd, "test 1 2 3"))
32+
}
33+
}

utils/platform/cmd_windows.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//go:build windows
2+
// +build windows
3+
4+
package platform
5+
6+
import "github.com/ARM-software/golang-utils/utils/subprocess/command"
7+
8+
var (
9+
// runAsAdmin describes the command to use to run command as Administrator
10+
// See https://ss64.com/nt/syntax-elevate.html
11+
// see https://lazyadmin.nl/it/runas-command/
12+
// see https://www.tenforums.com/general-support/111929-how-use-runas-without-password-prompt.html
13+
runAsAdmin = command.RunAs("Administrator")
14+
)
15+
16+
// DefineRunAsAdmin defines the command to run as Administrator.
17+
// e.g.
18+
// - args="runas", "/user:adrien" to run commands as `adrien`
19+
func DefineRunAsAdmin(args ...string) {
20+
runAsAdmin = command.NewCommandAsDifferentUser(args...)
21+
}
22+
23+
func getRunCommandWithPrivileges() *command.CommandAsDifferentUser {
24+
return runAsAdmin
25+
}

utils/platform/users.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,56 @@ func HasUser(username string) (found bool, err error) {
7373
return
7474
}
7575

76+
// IsCurrentUserAnAdmin states whether the current user is a superuser or not.
77+
func IsCurrentUserAnAdmin() (admin bool, err error) {
78+
cuser, err := user.Current()
79+
if err != nil {
80+
err = fmt.Errorf("%w: cannot fetch the current user: %v", commonerrors.ErrUnexpected, err.Error())
81+
return
82+
}
83+
admin, err = IsUserAdmin(cuser)
84+
return
85+
}
86+
87+
// IsUserAdmin states whether the user is a superuser or not. Similar to IsAdmin but may use more checks.
88+
func IsUserAdmin(user *user.User) (admin bool, err error) {
89+
if user == nil {
90+
err = fmt.Errorf("%w: missing user", commonerrors.ErrUndefined)
91+
return
92+
}
93+
admin, subErr := isUserAdmin(user)
94+
if subErr == nil {
95+
return
96+
}
97+
admin, err = IsAdmin(user.Username)
98+
err = ConvertUserGroupError(err)
99+
return
100+
}
101+
102+
// IsAdmin states whether the user is a superuser or not.
103+
func IsAdmin(username string) (admin bool, err error) {
104+
found, subErr := HasUser(username)
105+
if !found && subErr == nil {
106+
return
107+
}
108+
admin, err = isAdmin(username)
109+
err = ConvertUserGroupError(err)
110+
if err == nil {
111+
return
112+
}
113+
// Make more check if username is current user
114+
current, subErr := user.Current()
115+
if subErr != nil {
116+
return
117+
}
118+
if current.Username != username {
119+
return
120+
}
121+
admin, err = isCurrentAdmin()
122+
err = ConvertUserGroupError(err)
123+
return
124+
}
125+
76126
// HasGroup checks whether a group exists
77127
func HasGroup(groupName string) (found bool, err error) {
78128
group, err := user.LookupGroup(groupName)

0 commit comments

Comments
 (0)