Merge pull request #168 from Random-Liu/add-run-as-user

Add RunAsUser support
This commit is contained in:
Lantao Liu 2017-08-25 13:45:47 -07:00 committed by GitHub
commit 980e8e8007
16 changed files with 817 additions and 82 deletions

View File

@ -21,7 +21,7 @@ source $(dirname "${BASH_SOURCE[0]}")/test-utils.sh
# FOCUS focuses the test to run.
FOCUS=${FOCUS:-}
# SKIP skips the test to skip.
SKIP=${SKIP:-"RunAsUserName"}
SKIP=${SKIP:-""}
REPORT_DIR=${REPORT_DIR:-"/tmp/test-cri"}
if [[ -z "${GOPATH}" ]]; then

View File

@ -140,8 +140,16 @@ func (c *criContainerdService) CreateContainer(ctx context.Context, r *runtime.C
containerMetadataLabel: string(metaBytes),
}
specOpts := containerd.WithSpec(spec)
// Set container username. This could only be done by containerd, because it needs
// access to the container rootfs. Pass user name to containerd, and let it overwrite
// the spec for us.
if username := config.GetLinux().GetSecurityContext().GetRunAsUsername(); username != "" {
specOpts = containerd.WithSpec(spec, containerd.WithUsername(username))
}
opts = append(opts,
containerd.WithSpec(spec),
specOpts,
containerd.WithRuntime(defaultRuntime),
containerd.WithContainerLabels(labels))
var cntr containerd.Container
@ -185,7 +193,7 @@ func (c *criContainerdService) CreateContainer(ctx context.Context, r *runtime.C
func (c *criContainerdService) generateContainerSpec(id string, sandboxPid uint32, config *runtime.ContainerConfig,
sandboxConfig *runtime.PodSandboxConfig, imageConfig *imagespec.ImageConfig, extraMounts []*runtime.Mount) (*runtimespec.Spec, error) {
// Creates a spec Generator with the default spec.
spec, err := containerd.GenerateSpec()
spec, err := containerd.GenerateSpec(context.Background(), nil, nil)
if err != nil {
return nil, err
}
@ -225,6 +233,9 @@ func (c *criContainerdService) generateContainerSpec(id string, sandboxPid uint3
addOCIBindMounts(&g, append(extraMounts, config.GetMounts()...))
if securityContext.GetPrivileged() {
if !sandboxConfig.GetLinux().GetSecurityContext().GetPrivileged() {
return nil, fmt.Errorf("no privileged container allowed in sandbox")
}
if err := setOCIPrivileged(&g, config); err != nil {
return nil, err
}
@ -238,14 +249,15 @@ func (c *criContainerdService) generateContainerSpec(id string, sandboxPid uint3
securityContext.GetCapabilities(), err)
}
// TODO(random-liu): [P1] Set selinux options.
// TODO(random-liu): [P2] Add apparmor and seccomp.
// TODO: Figure out whether we should set no new privilege for sandbox container by default
g.SetProcessNoNewPrivileges(securityContext.GetNoNewPrivs())
}
// TODO: Figure out whether we should set no new privilege for sandbox container by default
g.SetProcessNoNewPrivileges(securityContext.GetNoNewPrivs())
// TODO(random-liu): [P1] Set selinux options.
g.SetRootReadonly(securityContext.GetReadonlyRootfs())
setOCILinuxResource(&g, config.GetLinux().GetResources())
@ -258,9 +270,9 @@ func (c *criContainerdService) generateContainerSpec(id string, sandboxPid uint3
// Set namespaces, share namespace with sandbox container.
setOCINamespaces(&g, securityContext.GetNamespaceOptions(), sandboxPid)
// TODO(random-liu): [P1] Set username.
runAsUser := securityContext.GetRunAsUser()
if runAsUser != nil {
// TODO(random-liu): We should also set gid. Use containerd#1425 instead.
g.SetProcessUID(uint32(runAsUser.GetValue()))
}

View File

@ -91,6 +91,7 @@ func getCreateContainerTestData() (*runtime.ContainerConfig, *runtime.PodSandbox
},
SupplementalGroups: []int64{1111, 2222},
NoNewPrivs: true,
RunAsUser: &runtime.Int64Value{Value: 255},
},
},
}
@ -143,6 +144,9 @@ func getCreateContainerTestData() (*runtime.ContainerConfig, *runtime.PodSandbox
assert.NotContains(t, spec.Process.Capabilities.Permitted, "CAP_CHOWN")
assert.NotContains(t, spec.Process.Capabilities.Ambient, "CAP_CHOWN")
t.Logf("Check uid")
assert.EqualValues(t, spec.Process.User.UID, 255)
t.Logf("Check supplemental groups")
assert.Contains(t, spec.Process.User.AdditionalGids, uint32(1111))
assert.Contains(t, spec.Process.User.AdditionalGids, uint32(2222))

View File

@ -207,7 +207,7 @@ func (c *criContainerdService) generateSandboxContainerSpec(id string, config *r
imageConfig *imagespec.ImageConfig, nsPath string) (*runtimespec.Spec, error) {
// Creates a spec Generator with the default spec.
// TODO(random-liu): [P1] Compare the default settings with docker and containerd default.
spec, err := containerd.GenerateSpec()
spec, err := containerd.GenerateSpec(context.Background(), nil, nil)
if err != nil {
return nil, err
}
@ -250,7 +250,8 @@ func (c *criContainerdService) generateSandboxContainerSpec(id string, config *r
// TODO(random-liu): [P2] Set default cgroup path if cgroup parent is not specified.
// Set namespace options.
nsOptions := config.GetLinux().GetSecurityContext().GetNamespaceOptions()
securityContext := config.GetLinux().GetSecurityContext()
nsOptions := securityContext.GetNamespaceOptions()
if nsOptions.GetHostNetwork() {
g.RemoveLinuxNamespace(string(runtimespec.NetworkNamespace)) // nolint: errcheck
} else {
@ -267,11 +268,16 @@ func (c *criContainerdService) generateSandboxContainerSpec(id string, config *r
// TODO(random-liu): [P1] Apply SeLinux options.
// TODO(random-liu): [P1] Set user.
runAsUser := securityContext.GetRunAsUser()
if runAsUser != nil {
// TODO(random-liu): We should also set gid. Use containerd#1425 instead.
g.SetProcessUID(uint32(runAsUser.GetValue()))
}
// TODO(random-liu): [P1] Set supplemental group.
// TODO(random-liu): [P1] Set privileged.
supplementalGroups := securityContext.GetSupplementalGroups()
for _, group := range supplementalGroups {
g.AddProcessAdditionalGid(uint32(group))
}
// Add sysctls
sysctls := config.GetLinux().GetSysctls()

View File

@ -128,6 +128,20 @@ func TestGenerateSandboxContainerSpec(t *testing.T) {
},
expectErr: true,
},
"should set user correctly": {
configChange: func(c *runtime.PodSandboxConfig) {
c.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{
RunAsUser: &runtime.Int64Value{Value: 255},
SupplementalGroups: []int64{1111, 2222},
}
},
specCheck: func(t *testing.T, spec *runtimespec.Spec) {
require.NotNil(t, spec.Process)
assert.EqualValues(t, spec.Process.User.UID, 255)
assert.Contains(t, spec.Process.User.AdditionalGids, uint32(1111))
assert.Contains(t, spec.Process.User.AdditionalGids, uint32(2222))
},
},
} {
t.Logf("TestCase %q", desc)
c := newTestCRIContainerdService()

View File

@ -1,6 +1,6 @@
github.com/blang/semver v3.1.0
github.com/boltdb/bolt v1.3.0-58-ge9cf4fa
github.com/containerd/containerd f05281743e5ac9ad11c6e19a72be7a903eab79f5
github.com/containerd/containerd a6ce1ef2a140d79856a8647e1d1ae5ac9ab581eb
github.com/containerd/fifo fbfb6a11ec671efbe94ad1c12c2e98773f19e1e6
github.com/containernetworking/cni v0.6.0
github.com/containernetworking/plugins v0.6.0

View File

@ -60,25 +60,25 @@ image, err := client.Pull(context, "docker.io/library/redis:latest")
err := client.Push(context, "docker.io/library/redis:latest", image.Target())
```
### OCI Runtime Specification
containerd fully supports the OCI runtime specification for running containers. We have built in functions to help you generate runtime specifications based on images as well as custom parameters.
```go
spec, err := containerd.GenerateSpec(containerd.WithImageConfig(context, image))
```
### Containers
In containerd, a container is a metadata object. Resources such as an OCI runtime specification, image, root filesystem, and other metadata can be attached to a container.
```go
redis, err := client.NewContainer(context, "redis-master",
containerd.WithSpec(spec),
)
redis, err := client.NewContainer(context, "redis-master")
defer redis.Delete(context)
```
### OCI Runtime Specification
containerd fully supports the OCI runtime specification for running containers. We have built in functions to help you generate runtime specifications based on images as well as custom parameters.
You can specify options when creating a container about how to modify the specification.
```go
redis, err := client.NewContainer(context, "redis-master", containerd.WithNewSpec(containerd.WithImageConfig(image)))
```
## Root Filesystems
containerd allows you to use overlay or snapshot filesystems with your containers. It comes with builtin support for overlayfs and btrfs.
@ -89,16 +89,17 @@ image, err := client.Pull(context, "docker.io/library/redis:latest", containerd.
// allocate a new RW root filesystem for a container based on the image
redis, err := client.NewContainer(context, "redis-master",
containerd.WithSpec(spec),
containerd.WithNewSnapshot("redis-rootfs", image),
containerd.WithNewSpec(containerd.WithImageConfig(image)),
)
// use a readonly filesystem with multiple containers
for i := 0; i < 10; i++ {
id := fmt.Sprintf("id-%s", i)
container, err := client.NewContainer(ctx, id,
containerd.WithSpec(spec),
containerd.WithNewSnapshotView(id, image),
containerd.WithNewSpec(containerd.WithImageConfig(image)),
)
}
```

View File

@ -2,11 +2,16 @@
package containerd
import specs "github.com/opencontainers/runtime-spec/specs-go"
import (
"context"
"github.com/containerd/containerd/containers"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// WithApparmor sets the provided apparmor profile to the spec
func WithApparmorProfile(profile string) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.ApparmorProfile = profile
return nil
}

View File

@ -1,16 +1,21 @@
package containerd
import specs "github.com/opencontainers/runtime-spec/specs-go"
import (
"context"
"github.com/containerd/containerd/containers"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// GenerateSpec will generate a default spec from the provided image
// for use as a containerd container
func GenerateSpec(opts ...SpecOpts) (*specs.Spec, error) {
func GenerateSpec(ctx context.Context, client *Client, c *containers.Container, opts ...SpecOpts) (*specs.Spec, error) {
s, err := createDefaultSpec()
if err != nil {
return nil, err
}
for _, o := range opts {
if err := o(s); err != nil {
if err := o(ctx, client, c, s); err != nil {
return nil, err
}
}

View File

@ -1,13 +1,19 @@
package containerd
import specs "github.com/opencontainers/runtime-spec/specs-go"
import (
"context"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/typeurl"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// SpecOpts sets spec specific information to a newly generated OCI spec
type SpecOpts func(s *specs.Spec) error
type SpecOpts func(context.Context, *Client, *containers.Container, *specs.Spec) error
// WithProcessArgs replaces the args on the generated spec
func WithProcessArgs(args ...string) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.Args = args
return nil
}
@ -15,8 +21,46 @@ func WithProcessArgs(args ...string) SpecOpts {
// WithHostname sets the container's hostname
func WithHostname(name string) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Hostname = name
return nil
}
}
// WithNewSpec generates a new spec for a new container
func WithNewSpec(opts ...SpecOpts) NewContainerOpts {
return func(ctx context.Context, client *Client, c *containers.Container) error {
s, err := createDefaultSpec()
if err != nil {
return err
}
for _, o := range opts {
if err := o(ctx, client, c, s); err != nil {
return err
}
}
any, err := typeurl.MarshalAny(s)
if err != nil {
return err
}
c.Spec = any
return nil
}
}
// WithSpec sets the provided spec on the container
func WithSpec(s *specs.Spec, opts ...SpecOpts) NewContainerOpts {
return func(ctx context.Context, client *Client, c *containers.Container) error {
for _, o := range opts {
if err := o(ctx, client, c, s); err != nil {
return err
}
}
any, err := typeurl.MarshalAny(s)
if err != nil {
return err
}
c.Spec = any
return nil
}
}

View File

@ -6,23 +6,28 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"golang.org/x/sys/unix"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/typeurl"
"github.com/opencontainers/image-spec/identity"
"github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runc/libcontainer/user"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
)
// WithTTY sets the information on the spec as well as the environment variables for
// using a TTY
func WithTTY(s *specs.Spec) error {
func WithTTY(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.Terminal = true
s.Process.Env = append(s.Process.Env, "TERM=xterm")
return nil
@ -30,7 +35,7 @@ func WithTTY(s *specs.Spec) error {
// WithHostNamespace allows a task to run inside the host's linux namespace
func WithHostNamespace(ns specs.LinuxNamespaceType) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
for i, n := range s.Linux.Namespaces {
if n.Type == ns {
s.Linux.Namespaces = append(s.Linux.Namespaces[:i], s.Linux.Namespaces[i+1:]...)
@ -44,7 +49,7 @@ func WithHostNamespace(ns specs.LinuxNamespaceType) SpecOpts {
// WithLinuxNamespace uses the passed in namespace for the spec. If a namespace of the same type already exists in the
// spec, the existing namespace is replaced by the one provided.
func WithLinuxNamespace(ns specs.LinuxNamespace) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
for i, n := range s.Linux.Namespaces {
if n.Type == ns.Type {
before := s.Linux.Namespaces[:i]
@ -60,11 +65,11 @@ func WithLinuxNamespace(ns specs.LinuxNamespace) SpecOpts {
}
// WithImageConfig configures the spec to from the configuration of an Image
func WithImageConfig(ctx context.Context, i Image) SpecOpts {
return func(s *specs.Spec) error {
func WithImageConfig(i Image) SpecOpts {
return func(ctx context.Context, client *Client, c *containers.Container, s *specs.Spec) error {
var (
image = i.(*image)
store = image.client.ContentStore()
store = client.ContentStore()
)
ic, err := image.i.Config(ctx, store)
if err != nil {
@ -100,6 +105,10 @@ func WithImageConfig(ctx context.Context, i Image) SpecOpts {
case 1:
v, err := strconv.ParseUint(parts[0], 0, 10)
if err != nil {
// if we cannot parse as a uint they try to see if it is a username
if err := WithUsername(config.User)(ctx, client, c, s); err != nil {
return err
}
return err
}
uid, gid = uint32(v), uint32(v)
@ -129,7 +138,7 @@ func WithImageConfig(ctx context.Context, i Image) SpecOpts {
// WithRootFSPath specifies unmanaged rootfs path.
func WithRootFSPath(path string, readonly bool) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Root = &specs.Root{
Path: path,
Readonly: readonly,
@ -139,18 +148,6 @@ func WithRootFSPath(path string, readonly bool) SpecOpts {
}
}
// WithSpec sets the provided spec for a new container
func WithSpec(spec *specs.Spec) NewContainerOpts {
return func(ctx context.Context, client *Client, c *containers.Container) error {
any, err := typeurl.MarshalAny(spec)
if err != nil {
return err
}
c.Spec = any
return nil
}
}
// WithResources sets the provided resources on the spec for task updates
func WithResources(resources *specs.LinuxResources) UpdateTaskOpts {
return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error {
@ -160,13 +157,13 @@ func WithResources(resources *specs.LinuxResources) UpdateTaskOpts {
}
// WithNoNewPrivileges sets no_new_privileges on the process for the container
func WithNoNewPrivileges(s *specs.Spec) error {
func WithNoNewPrivileges(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.NoNewPrivileges = true
return nil
}
// WithHostHostsFile bind-mounts the host's /etc/hosts into the container as readonly
func WithHostHostsFile(s *specs.Spec) error {
func WithHostHostsFile(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Mounts = append(s.Mounts, specs.Mount{
Destination: "/etc/hosts",
Type: "bind",
@ -177,7 +174,7 @@ func WithHostHostsFile(s *specs.Spec) error {
}
// WithHostResolvconf bind-mounts the host's /etc/resolv.conf into the container as readonly
func WithHostResolvconf(s *specs.Spec) error {
func WithHostResolvconf(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Mounts = append(s.Mounts, specs.Mount{
Destination: "/etc/resolv.conf",
Type: "bind",
@ -188,7 +185,7 @@ func WithHostResolvconf(s *specs.Spec) error {
}
// WithHostLocaltime bind-mounts the host's /etc/localtime into the container as readonly
func WithHostLocaltime(s *specs.Spec) error {
func WithHostLocaltime(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Mounts = append(s.Mounts, specs.Mount{
Destination: "/etc/localtime",
Type: "bind",
@ -201,7 +198,7 @@ func WithHostLocaltime(s *specs.Spec) error {
// WithUserNamespace sets the uid and gid mappings for the task
// this can be called multiple times to add more mappings to the generated spec
func WithUserNamespace(container, host, size uint32) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
var hasUserns bool
for _, ns := range s.Linux.Namespaces {
if ns.Type == specs.UserNamespace {
@ -271,7 +268,7 @@ func WithRemappedSnapshot(id string, i Image, uid, gid uint32) NewContainerOpts
// WithCgroup sets the container's cgroup path
func WithCgroup(path string) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Linux.CgroupsPath = path
return nil
}
@ -279,13 +276,69 @@ func WithCgroup(path string) SpecOpts {
// WithNamespacedCgroup uses the namespace set on the context to create a
// root directory for containers in the cgroup with the id as the subcgroup
func WithNamespacedCgroup(ctx context.Context, id string) SpecOpts {
return func(s *specs.Spec) error {
func WithNamespacedCgroup() SpecOpts {
return func(ctx context.Context, _ *Client, c *containers.Container, s *specs.Spec) error {
namespace, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return err
}
s.Linux.CgroupsPath = filepath.Join("/", namespace, id)
s.Linux.CgroupsPath = filepath.Join("/", namespace, c.ID)
return nil
}
}
// WithUserIDs allows the UID and GID for the Process to be set
func WithUserIDs(uid, gid uint32) SpecOpts {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.User.UID = uid
s.Process.User.GID = gid
return nil
}
}
// WithUsername sets the correct UID and GID for the container
// based on the the image's /etc/passwd contents.
// id is the snapshot id that is used
func WithUsername(username string) SpecOpts {
return func(ctx context.Context, client *Client, c *containers.Container, s *specs.Spec) error {
if c.Snapshotter == "" {
return errors.Errorf("no snapshotter set for container")
}
if c.RootFS == "" {
return errors.Errorf("rootfs not created for container")
}
snapshotter := client.SnapshotService(c.Snapshotter)
mounts, err := snapshotter.Mounts(ctx, c.RootFS)
if err != nil {
return err
}
root, err := ioutil.TempDir("", "ctd-username")
if err != nil {
return err
}
defer os.RemoveAll(root)
for _, m := range mounts {
if err := m.Mount(root); err != nil {
return err
}
}
defer unix.Unmount(root, 0)
f, err := os.Open(filepath.Join(root, "/etc/passwd"))
if err != nil {
return err
}
defer f.Close()
users, err := user.ParsePasswdFilter(f, func(u user.User) bool {
return u.Name == username
})
if err != nil {
return err
}
if len(users) == 0 {
return errors.Errorf("no users found for %s", username)
}
u := users[0]
s.Process.User.UID, s.Process.User.GID = uint32(u.Uid), uint32(u.Gid)
return nil
}
}

View File

@ -10,16 +10,15 @@ import (
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/typeurl"
"github.com/opencontainers/image-spec/specs-go/v1"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
func WithImageConfig(ctx context.Context, i Image) SpecOpts {
return func(s *specs.Spec) error {
func WithImageConfig(i Image) SpecOpts {
return func(ctx context.Context, client *Client, _ *containers.Container, s *specs.Spec) error {
var (
image = i.(*image)
store = image.client.ContentStore()
store = client.ContentStore()
)
ic, err := image.i.Config(ctx, store)
if err != nil {
@ -52,7 +51,7 @@ func WithImageConfig(ctx context.Context, i Image) SpecOpts {
}
func WithTTY(width, height int) SpecOpts {
return func(s *specs.Spec) error {
return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error {
s.Process.Terminal = true
s.Process.ConsoleSize.Width = uint(width)
s.Process.ConsoleSize.Height = uint(height)
@ -60,17 +59,6 @@ func WithTTY(width, height int) SpecOpts {
}
}
func WithSpec(spec *specs.Spec) NewContainerOpts {
return func(ctx context.Context, client *Client, c *containers.Container) error {
any, err := typeurl.MarshalAny(spec)
if err != nil {
return err
}
c.Spec = any
return nil
}
}
func WithResources(resources *specs.WindowsResources) UpdateTaskOpts {
return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error {
r.Resources = resources

View File

@ -0,0 +1,111 @@
package user
import (
"errors"
"golang.org/x/sys/unix"
)
var (
// The current operating system does not provide the required data for user lookups.
ErrUnsupported = errors.New("user lookup: operating system does not provide passwd-formatted data")
// No matching entries found in file.
ErrNoPasswdEntries = errors.New("no matching entries in passwd file")
ErrNoGroupEntries = errors.New("no matching entries in group file")
)
func lookupUser(filter func(u User) bool) (User, error) {
// Get operating system-specific passwd reader-closer.
passwd, err := GetPasswd()
if err != nil {
return User{}, err
}
defer passwd.Close()
// Get the users.
users, err := ParsePasswdFilter(passwd, filter)
if err != nil {
return User{}, err
}
// No user entries found.
if len(users) == 0 {
return User{}, ErrNoPasswdEntries
}
// Assume the first entry is the "correct" one.
return users[0], nil
}
// CurrentUser looks up the current user by their user id in /etc/passwd. If the
// user cannot be found (or there is no /etc/passwd file on the filesystem),
// then CurrentUser returns an error.
func CurrentUser() (User, error) {
return LookupUid(unix.Getuid())
}
// LookupUser looks up a user by their username in /etc/passwd. If the user
// cannot be found (or there is no /etc/passwd file on the filesystem), then
// LookupUser returns an error.
func LookupUser(username string) (User, error) {
return lookupUser(func(u User) bool {
return u.Name == username
})
}
// LookupUid looks up a user by their user id in /etc/passwd. If the user cannot
// be found (or there is no /etc/passwd file on the filesystem), then LookupId
// returns an error.
func LookupUid(uid int) (User, error) {
return lookupUser(func(u User) bool {
return u.Uid == uid
})
}
func lookupGroup(filter func(g Group) bool) (Group, error) {
// Get operating system-specific group reader-closer.
group, err := GetGroup()
if err != nil {
return Group{}, err
}
defer group.Close()
// Get the users.
groups, err := ParseGroupFilter(group, filter)
if err != nil {
return Group{}, err
}
// No user entries found.
if len(groups) == 0 {
return Group{}, ErrNoGroupEntries
}
// Assume the first entry is the "correct" one.
return groups[0], nil
}
// CurrentGroup looks up the current user's group by their primary group id's
// entry in /etc/passwd. If the group cannot be found (or there is no
// /etc/group file on the filesystem), then CurrentGroup returns an error.
func CurrentGroup() (Group, error) {
return LookupGid(unix.Getgid())
}
// LookupGroup looks up a group by its name in /etc/group. If the group cannot
// be found (or there is no /etc/group file on the filesystem), then LookupGroup
// returns an error.
func LookupGroup(groupname string) (Group, error) {
return lookupGroup(func(g Group) bool {
return g.Name == groupname
})
}
// LookupGid looks up a group by its group id in /etc/group. If the group cannot
// be found (or there is no /etc/group file on the filesystem), then LookupGid
// returns an error.
func LookupGid(gid int) (Group, error) {
return lookupGroup(func(g Group) bool {
return g.Gid == gid
})
}

View File

@ -0,0 +1,30 @@
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
package user
import (
"io"
"os"
)
// Unix-specific path to the passwd and group formatted files.
const (
unixPasswdPath = "/etc/passwd"
unixGroupPath = "/etc/group"
)
func GetPasswdPath() (string, error) {
return unixPasswdPath, nil
}
func GetPasswd() (io.ReadCloser, error) {
return os.Open(unixPasswdPath)
}
func GetGroupPath() (string, error) {
return unixGroupPath, nil
}
func GetGroup() (io.ReadCloser, error) {
return os.Open(unixGroupPath)
}

View File

@ -0,0 +1,21 @@
// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris
package user
import "io"
func GetPasswdPath() (string, error) {
return "", ErrUnsupported
}
func GetPasswd() (io.ReadCloser, error) {
return nil, ErrUnsupported
}
func GetGroupPath() (string, error) {
return "", ErrUnsupported
}
func GetGroup() (io.ReadCloser, error) {
return nil, ErrUnsupported
}

View File

@ -0,0 +1,441 @@
package user
import (
"bufio"
"fmt"
"io"
"os"
"strconv"
"strings"
)
const (
minId = 0
maxId = 1<<31 - 1 //for 32-bit systems compatibility
)
var (
ErrRange = fmt.Errorf("uids and gids must be in range %d-%d", minId, maxId)
)
type User struct {
Name string
Pass string
Uid int
Gid int
Gecos string
Home string
Shell string
}
type Group struct {
Name string
Pass string
Gid int
List []string
}
func parseLine(line string, v ...interface{}) {
if line == "" {
return
}
parts := strings.Split(line, ":")
for i, p := range parts {
// Ignore cases where we don't have enough fields to populate the arguments.
// Some configuration files like to misbehave.
if len(v) <= i {
break
}
// Use the type of the argument to figure out how to parse it, scanf() style.
// This is legit.
switch e := v[i].(type) {
case *string:
*e = p
case *int:
// "numbers", with conversion errors ignored because of some misbehaving configuration files.
*e, _ = strconv.Atoi(p)
case *[]string:
// Comma-separated lists.
if p != "" {
*e = strings.Split(p, ",")
} else {
*e = []string{}
}
default:
// Someone goof'd when writing code using this function. Scream so they can hear us.
panic(fmt.Sprintf("parseLine only accepts {*string, *int, *[]string} as arguments! %#v is not a pointer!", e))
}
}
}
func ParsePasswdFile(path string) ([]User, error) {
passwd, err := os.Open(path)
if err != nil {
return nil, err
}
defer passwd.Close()
return ParsePasswd(passwd)
}
func ParsePasswd(passwd io.Reader) ([]User, error) {
return ParsePasswdFilter(passwd, nil)
}
func ParsePasswdFileFilter(path string, filter func(User) bool) ([]User, error) {
passwd, err := os.Open(path)
if err != nil {
return nil, err
}
defer passwd.Close()
return ParsePasswdFilter(passwd, filter)
}
func ParsePasswdFilter(r io.Reader, filter func(User) bool) ([]User, error) {
if r == nil {
return nil, fmt.Errorf("nil source for passwd-formatted data")
}
var (
s = bufio.NewScanner(r)
out = []User{}
)
for s.Scan() {
if err := s.Err(); err != nil {
return nil, err
}
line := strings.TrimSpace(s.Text())
if line == "" {
continue
}
// see: man 5 passwd
// name:password:UID:GID:GECOS:directory:shell
// Name:Pass:Uid:Gid:Gecos:Home:Shell
// root:x:0:0:root:/root:/bin/bash
// adm:x:3:4:adm:/var/adm:/bin/false
p := User{}
parseLine(line, &p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell)
if filter == nil || filter(p) {
out = append(out, p)
}
}
return out, nil
}
func ParseGroupFile(path string) ([]Group, error) {
group, err := os.Open(path)
if err != nil {
return nil, err
}
defer group.Close()
return ParseGroup(group)
}
func ParseGroup(group io.Reader) ([]Group, error) {
return ParseGroupFilter(group, nil)
}
func ParseGroupFileFilter(path string, filter func(Group) bool) ([]Group, error) {
group, err := os.Open(path)
if err != nil {
return nil, err
}
defer group.Close()
return ParseGroupFilter(group, filter)
}
func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) {
if r == nil {
return nil, fmt.Errorf("nil source for group-formatted data")
}
var (
s = bufio.NewScanner(r)
out = []Group{}
)
for s.Scan() {
if err := s.Err(); err != nil {
return nil, err
}
text := s.Text()
if text == "" {
continue
}
// see: man 5 group
// group_name:password:GID:user_list
// Name:Pass:Gid:List
// root:x:0:root
// adm:x:4:root,adm,daemon
p := Group{}
parseLine(text, &p.Name, &p.Pass, &p.Gid, &p.List)
if filter == nil || filter(p) {
out = append(out, p)
}
}
return out, nil
}
type ExecUser struct {
Uid int
Gid int
Sgids []int
Home string
}
// GetExecUserPath is a wrapper for GetExecUser. It reads data from each of the
// given file paths and uses that data as the arguments to GetExecUser. If the
// files cannot be opened for any reason, the error is ignored and a nil
// io.Reader is passed instead.
func GetExecUserPath(userSpec string, defaults *ExecUser, passwdPath, groupPath string) (*ExecUser, error) {
var passwd, group io.Reader
if passwdFile, err := os.Open(passwdPath); err == nil {
passwd = passwdFile
defer passwdFile.Close()
}
if groupFile, err := os.Open(groupPath); err == nil {
group = groupFile
defer groupFile.Close()
}
return GetExecUser(userSpec, defaults, passwd, group)
}
// GetExecUser parses a user specification string (using the passwd and group
// readers as sources for /etc/passwd and /etc/group data, respectively). In
// the case of blank fields or missing data from the sources, the values in
// defaults is used.
//
// GetExecUser will return an error if a user or group literal could not be
// found in any entry in passwd and group respectively.
//
// Examples of valid user specifications are:
// * ""
// * "user"
// * "uid"
// * "user:group"
// * "uid:gid
// * "user:gid"
// * "uid:group"
//
// It should be noted that if you specify a numeric user or group id, they will
// not be evaluated as usernames (only the metadata will be filled). So attempting
// to parse a user with user.Name = "1337" will produce the user with a UID of
// 1337.
func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) (*ExecUser, error) {
if defaults == nil {
defaults = new(ExecUser)
}
// Copy over defaults.
user := &ExecUser{
Uid: defaults.Uid,
Gid: defaults.Gid,
Sgids: defaults.Sgids,
Home: defaults.Home,
}
// Sgids slice *cannot* be nil.
if user.Sgids == nil {
user.Sgids = []int{}
}
// Allow for userArg to have either "user" syntax, or optionally "user:group" syntax
var userArg, groupArg string
parseLine(userSpec, &userArg, &groupArg)
// Convert userArg and groupArg to be numeric, so we don't have to execute
// Atoi *twice* for each iteration over lines.
uidArg, uidErr := strconv.Atoi(userArg)
gidArg, gidErr := strconv.Atoi(groupArg)
// Find the matching user.
users, err := ParsePasswdFilter(passwd, func(u User) bool {
if userArg == "" {
// Default to current state of the user.
return u.Uid == user.Uid
}
if uidErr == nil {
// If the userArg is numeric, always treat it as a UID.
return uidArg == u.Uid
}
return u.Name == userArg
})
// If we can't find the user, we have to bail.
if err != nil && passwd != nil {
if userArg == "" {
userArg = strconv.Itoa(user.Uid)
}
return nil, fmt.Errorf("unable to find user %s: %v", userArg, err)
}
var matchedUserName string
if len(users) > 0 {
// First match wins, even if there's more than one matching entry.
matchedUserName = users[0].Name
user.Uid = users[0].Uid
user.Gid = users[0].Gid
user.Home = users[0].Home
} else if userArg != "" {
// If we can't find a user with the given username, the only other valid
// option is if it's a numeric username with no associated entry in passwd.
if uidErr != nil {
// Not numeric.
return nil, fmt.Errorf("unable to find user %s: %v", userArg, ErrNoPasswdEntries)
}
user.Uid = uidArg
// Must be inside valid uid range.
if user.Uid < minId || user.Uid > maxId {
return nil, ErrRange
}
// Okay, so it's numeric. We can just roll with this.
}
// On to the groups. If we matched a username, we need to do this because of
// the supplementary group IDs.
if groupArg != "" || matchedUserName != "" {
groups, err := ParseGroupFilter(group, func(g Group) bool {
// If the group argument isn't explicit, we'll just search for it.
if groupArg == "" {
// Check if user is a member of this group.
for _, u := range g.List {
if u == matchedUserName {
return true
}
}
return false
}
if gidErr == nil {
// If the groupArg is numeric, always treat it as a GID.
return gidArg == g.Gid
}
return g.Name == groupArg
})
if err != nil && group != nil {
return nil, fmt.Errorf("unable to find groups for spec %v: %v", matchedUserName, err)
}
// Only start modifying user.Gid if it is in explicit form.
if groupArg != "" {
if len(groups) > 0 {
// First match wins, even if there's more than one matching entry.
user.Gid = groups[0].Gid
} else {
// If we can't find a group with the given name, the only other valid
// option is if it's a numeric group name with no associated entry in group.
if gidErr != nil {
// Not numeric.
return nil, fmt.Errorf("unable to find group %s: %v", groupArg, ErrNoGroupEntries)
}
user.Gid = gidArg
// Must be inside valid gid range.
if user.Gid < minId || user.Gid > maxId {
return nil, ErrRange
}
// Okay, so it's numeric. We can just roll with this.
}
} else if len(groups) > 0 && uidErr != nil {
// Supplementary group ids only make sense if in the implicit form for non-numeric users.
user.Sgids = make([]int, len(groups))
for i, group := range groups {
user.Sgids[i] = group.Gid
}
}
}
return user, nil
}
// GetAdditionalGroups looks up a list of groups by name or group id
// against the given /etc/group formatted data. If a group name cannot
// be found, an error will be returned. If a group id cannot be found,
// or the given group data is nil, the id will be returned as-is
// provided it is in the legal range.
func GetAdditionalGroups(additionalGroups []string, group io.Reader) ([]int, error) {
var groups = []Group{}
if group != nil {
var err error
groups, err = ParseGroupFilter(group, func(g Group) bool {
for _, ag := range additionalGroups {
if g.Name == ag || strconv.Itoa(g.Gid) == ag {
return true
}
}
return false
})
if err != nil {
return nil, fmt.Errorf("Unable to find additional groups %v: %v", additionalGroups, err)
}
}
gidMap := make(map[int]struct{})
for _, ag := range additionalGroups {
var found bool
for _, g := range groups {
// if we found a matched group either by name or gid, take the
// first matched as correct
if g.Name == ag || strconv.Itoa(g.Gid) == ag {
if _, ok := gidMap[g.Gid]; !ok {
gidMap[g.Gid] = struct{}{}
found = true
break
}
}
}
// we asked for a group but didn't find it. let's check to see
// if we wanted a numeric group
if !found {
gid, err := strconv.Atoi(ag)
if err != nil {
return nil, fmt.Errorf("Unable to find group %s", ag)
}
// Ensure gid is inside gid range.
if gid < minId || gid > maxId {
return nil, ErrRange
}
gidMap[gid] = struct{}{}
}
}
gids := []int{}
for gid := range gidMap {
gids = append(gids, gid)
}
return gids, nil
}
// GetAdditionalGroupsPath is a wrapper around GetAdditionalGroups
// that opens the groupPath given and gives it as an argument to
// GetAdditionalGroups.
func GetAdditionalGroupsPath(additionalGroups []string, groupPath string) ([]int, error) {
var group io.Reader
if groupFile, err := os.Open(groupPath); err == nil {
group = groupFile
defer groupFile.Close()
}
return GetAdditionalGroups(additionalGroups, group)
}