-
Notifications
You must be signed in to change notification settings - Fork 583
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
many: do not use nss when looking up for users/groups from snapd snap #13776
many: do not use nss when looking up for users/groups from snapd snap #13776
Conversation
e52d6a4
to
7b51d5b
Compare
osutil/user/user_from_snap.go
Outdated
} | ||
return nil, err | ||
} | ||
line = append(line, chunk...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since we're doing what bufio.Scanner would do, but less efficiently, we should use scanner here (and in lookupExtraUser)
osutil/user/user_from_snap.go
Outdated
} | ||
} | ||
|
||
if len(line) == 0 || line[0] == '#' { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bytes.TrimSpace() before?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not think this is allowed.
osutil/user/user_from_snap.go
Outdated
components := strings.SplitN(string(line), ":", 4) | ||
if len(components) != 4 { | ||
continue | ||
} | ||
|
||
if components[index] != expectedValue { | ||
continue | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this could probably be in a callback, eg func(line []byte) error
, so that you can share the code reading the file between group/user since processing is identical for the both, eg.
var extrausersGroup = "/var/lib/extrausers/group"
func lookupExtraGroup(group string) (gr *Group, err error) {
gb := []byte(group)
err = processPasswdFile(extrausersGroup, func(line []byte) error {
components := strings.SplitN(line, []byte(":"), 4)
if bytes.Equal(components[0], gb) {
gr = &Group{
Name: string(components[0]),
Gid: string(components[2]),
}
// we're done
return io.EOF
}
return nil
})
if err != nil {
return nil, err
}
return gr, nil
}
7b51d5b
to
3bf27fb
Compare
3bf27fb
to
5713649
Compare
This branch depends on #13517 to be properly tested. |
4a74f31
to
2ef7a9f
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅
❗ Your organization needs to install the Codecov GitHub app to enable full functionality. Additional details and impacted files@@ Coverage Diff @@
## master #13776 +/- ##
==========================================
+ Coverage 78.73% 78.74% +0.01%
==========================================
Files 1055 1061 +6
Lines 138275 139197 +922
==========================================
+ Hits 108866 109609 +743
- Misses 22588 22736 +148
- Partials 6821 6852 +31
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
44bf5f1
to
ecc0756
Compare
@@ -27,11 +27,11 @@ import ( | |||
"errors" | |||
"fmt" | |||
"net/url" | |||
"os/user" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw. perhaps we could prevent from os/user
being imported by our code, there's a depguard linter we could configure in golangci-lint: https://golangci-lint.run/usage/linters/#depguard
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have not done this one...
osutil/user/user.go
Outdated
package user | ||
|
||
import ( | ||
origUser "os/user" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
osuser ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"orig" for "original"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick, I would also prefer osuser
for simplicity.
osutil/user/getent.go
Outdated
}, nil | ||
} | ||
|
||
func lookupUserFromGetent(index int, expectedValue string) (*User, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm I'm slightly uneasy with the caller needing to know the index here, what if we had:
type matcher func() (index int, value string)
func lookupUserFromGetent(match matcher) (*User, error) {
index, value := match()
...
}
// and relevant matchers
func userMatch(name string) matcher {
return func() (int, string) { return 0, name }
}
func uidMatch(uid int) (int string) matcher {
return func() (int, string) { return 2, strconv.Itoa(uid) }
}
func groupMatch (name string) matcher {
return func() (int, string) { return 0, name }
}
and the the actual call:
u, err := lookupUserFromGetent(userMatch("foo"))
// or
u, err := lookupUserFromGetent(uidMatch(os.Geteuid()))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure that sounds fine. I did not want to over engineer it because we are not really going to even add more stuff in there. But this does not seem to be a complex design. I will try it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will push a fix. I have made 2 different matcher types. Even though they do the same, in theory they do not make sense when used to the wrong function.
build-aux/snap/snapcraft.yaml
Outdated
@@ -305,6 +302,7 @@ parts: | |||
esac | |||
;; | |||
esac | |||
TAGS+=(snap osusergo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
github.com/godbus/dbus seems to call user.Current() a couple of times in some fallback paths:
vendor/github.com/godbus/dbus/conn_other.go
80: if currentUser, err := user.Current(); err != nil {
vendor/github.com/godbus/dbus/homedir_dynamic.go
10: u, err := user.Current()
which could be incorrect with osusergo
. Perhaps we should still pack libnss-extrausers to handle this edge case but make sure our code is using the osutil wrapper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the cases where HOME
environment variable is not defined? I wonder what weird cases exist without it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those cases should work. One is for the home dir, and if HOME defined it should work. Which should be 99.99% cases.
The other case is to get the uid, which should work correct in osusergo. It is just os.Getuid
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marking as requested changes since the numeric / digit code looks wrong.
Separately, I really wish Go had a way to do this globally without the developer being really careful not to import the other package. I wonder what else might be using os/user internally in Go? Do we know?
osutil/user/getent.go
Outdated
cmd.Stderr = &errBuf | ||
|
||
if err := cmd.Run(); err != nil { | ||
return nil, fmt.Errorf("getent returned an error: %q", errBuf.Bytes()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think we should dispaly the error code here?
Alternatively if we don't capture stderr we could just return fmt.Errorf("... : %w", err)
here:
type ExitError struct {
*os.ProcessState
// Stderr holds a subset of the standard error output from the
// Cmd.Output method if standard error was not otherwise being
// collected.
//
// If the error output is long, Stderr may contain only a prefix
// and suffix of the output, with the middle replaced with
// text about the number of omitted bytes.
//
// Stderr is provided for debugging, for inclusion in error messages.
// Users with other needs should redirect Cmd.Stderr as needed.
Stderr []byte
}
osutil/user/getent.go
Outdated
|
||
func isNumeric(value string) bool { | ||
for _, c := range value { | ||
return unicode.IsDigit(c) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we want to use this. This will return true for roman numerals, fractions and other weird stuff: https://cs.opensource.google/go/go/+/refs/tags/go1.23.1:src/unicode/digit.go
Separately this is a misnomer. Unicode has deifnitions for digits and number that are different. There's also unicode.IsNumber
which we are not calling here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's a big issue if we simply check that c >= '0' && c <= '9'
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what I have changed to.
osutil/user/user.go
Outdated
package user | ||
|
||
import ( | ||
origUser "os/user" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick, I would also prefer osuser
for simplicity.
d4c8037
to
1bab881
Compare
We might need this in coming release. Added milestone 2.66 to track it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with small nitpicks on pass-by-value and use of errors.As
.golangci.yml
Outdated
- "!**/osutil/user/*.go" | ||
deny: | ||
- pkg: "os/user" | ||
desc: "Please use osutil/user instead" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you link to a spec number or this PR perhaps?
osutil/user/getent.go
Outdated
|
||
outBuf, err := cmd.Output() | ||
if err != nil { | ||
exitError, isExitError := err.(*exec.ExitError) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be errors.As technically.
osutil/user/getent.go
Outdated
exitError, isExitError := err.(*exec.ExitError) | ||
if isExitError { | ||
return nil, fmt.Errorf("getent returned an error: %q", exitError.Stderr) | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can drop the else and unindent the return
osutil/user/getent.go
Outdated
value string | ||
} | ||
|
||
func (m *groupnameMatcher) index() int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick, this can be passed by value
osutil/user/getent.go
Outdated
return 0 | ||
} | ||
|
||
func (m *groupnameMatcher) expectedValue() string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same
osutil/user/getent.go
Outdated
value string | ||
} | ||
|
||
func (m *usernameMatcher) index() int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By value
osutil/user/getent.go
Outdated
} | ||
|
||
func userMatchUsername(username string) userMatcher { | ||
return &usernameMatcher{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By value
osutil/user/getent.go
Outdated
value int | ||
} | ||
|
||
func (m *uidMatcher) index() int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same
osutil/user/getent.go
Outdated
} | ||
|
||
func userMatchUid(uid int) userMatcher { | ||
return &uidMatcher{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My value
osutil/user/user.go
Outdated
) | ||
|
||
type User = osuser.User | ||
type Group = osuser.Group |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: type ( ... ) perhaps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
# include-go-root: false | ||
# packages: | ||
# - github.com/davecgh/go-spew/spew | ||
depguard: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for this
osutil/user/getent.go
Outdated
exitError, isExitError := err.(*exec.ExitError) | ||
if isExitError { | ||
return nil, fmt.Errorf("getent returned an error: %q", exitError.Stderr) | ||
} else { | ||
return nil, fmt.Errorf("getent could not be executed: %w", err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exitError, isExitError := err.(*exec.ExitError) | |
if isExitError { | |
return nil, fmt.Errorf("getent returned an error: %q", exitError.Stderr) | |
} else { | |
return nil, fmt.Errorf("getent could not be executed: %w", err) | |
} | |
var exitError *exec.ExitError | |
if errors.As(err, &exitError) { | |
return nil, fmt.Errorf("getent returned an error: %q", exitError.Stderr) | |
} else { | |
return nil, fmt.Errorf("getent could not be executed: %w", err) | |
} |
osutil/user/user_from_snap.go
Outdated
@@ -0,0 +1,74 @@ | |||
// -*- Mode: Go; indent-tabs-mode: t -*- | |||
//go:build snap |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm not sure about the build tag, maybe @pedronis has an opinion about it
I would try something more specific, eg snapdusergo
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have renamed it.
45a215d
to
7bd1ffb
Compare
…apd snap When snapd runs as a snap, it has its own runtime. This may not have NSS plugins needed for the host. For example to get users from AD/LDAP/Kerberos, or systemd-homed, or custom user databses. In general we can use tag `osusergo` to make go not to use the local configuration (i.e. `/etc/nsswitch.conf`), however, even if it is fine for most databases, we really need users and groups to be resolved with the host configuration. To be able to load correctly plugins, we expect the host system to provide `getent`. And we query `passwd` and `group` databases through this command. In the future we should connect the systemd-userdb if it is running and use `getent` only as fallback.
7bd1ffb
to
22f3403
Compare
Failures:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's a bit unclear what happens with unit tests here given the new tags? also do we need a spread test that shows the changes to be relevant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why from_snap in the name of this one? also this code doesn't seem to be tested?
// The component at `index` will need to match `expectedValue`. | ||
// If `isKey`, then `expectedValue` will also be passed as parameter | ||
// to getent along `database`. `numComponents` should be 4 for groups | ||
// and 7 for users. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be a bit cleaner to use a map database -> #fields and return an internal error if database is not in the map
@@ -0,0 +1,60 @@ | |||
// -*- Mode: Go; indent-tabs-mode: t -*- | |||
//go:build !snapdusergo |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need a new unit tests variant in GH, or changes to the current ones? cc @bboozzoo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func isKey(index int, expectedValue string) bool { | ||
numeric := isNumeric(expectedValue) | ||
return (index == 0 && !numeric) || (index == 2 && numeric) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
couldn't isKey also be part of the interfaces below?
// If `isKey`, then `expectedValue` will also be passed as parameter | ||
// to getent along `database`. `numComponents` should be 4 for groups | ||
// and 7 for users. | ||
func lookupFromGetent(database string, index int, expectedValue string, isKey bool, numComponents int) ([]string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it seems this could an interface instead:
type entMatcher interface {
index() int
expectedValue() string
isKey() bool
}
} | ||
|
||
if components == nil { | ||
return nil, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test don't seem to hit here
@valentindavid will do followup PRs for improvements related to latest comments (after merged). |
Depends on #13370When snapd runs as a snap, it has its own runtime. This may not have NSS plugins needed for the host. For example to get users from AD/LDAP/Kerberos, or systemd-homed, or custom user databses. In general we can use tag
osusergo
to make go not to use the local configuration (i.e./etc/nsswitch.conf
), however, even if it is fine for most databases, we really need users and groups to be resolved with the host configuration.To be able to load correctly plugins, we expect the host system to provide
getent
. And we querypasswd
andgroup
databases through this command.In the future we should connect the systemd-userdb if it is running and use
getent
only as fallback.