Repository: chef/automate (go)

Issue:
Automate deploy fails when the linux hab user comes from external auth instead of local accounts
<!-- /!\ Please ensure that you are NOT disclosing any customer information without their consent /!\ -->

## Describe the bug

Automate is unable to complete the deployment step if `hab` the hab user is coming from an external auth like LDAP in Linux instead of as a local account in `/etc/passwd`.

Initially, we saw errors from `useradd` complaining that the user already completed and a workaround was added #4078 to get past this.  

After the workaround, we now see the following error when running `CHEF_AUTOMATE_SKIP_HAB_USER=true chef-automate deploy`

```
Jul 20 17:57:26 HOSTNAME.com hab[30310]: deployment-service.default(O): failed to start server: failed to initialize secret store: user lookup: user: unknown user hab
```

The underlying problem appears to be caused by GoLang not using libc to query for user and group UID/GIDs and instead only pulling them from `/etc/passwd` and `/etc/group`. This will cause the deploy to fail as it can't find the `hab` user and attempts to create it via `useradd`. Which will then fail as it already exists in LDAP, also any other code that attempts to fetch the `hab` UID/GID` will have problems as it won't get a valid ID.

## To Reproduce
Steps to reproduce the behavior:

1. configure Linux to use external LDAP user accounts (via /etc/nsswitch.conf)
2. create a `hab` user in LDAP
3. Attempt to install Automate `chef-automate deploy`, latest current version will throw an error 
    
    ```
    Creating Habitat user and group
    Error: setup failed: creating hab user failed: "useradd: user 'hab' already exists\n": exit status 9  
    DeployError: Unable to install, configure and start the
    ```

4. If using a version from the dev channel `CHEF_AUTOMATE_SKIP_HAB_USER=true chef-automate deploy` to skip the `useradd` error and get the error about the `secret store` and `hab`.

    ```
    Jul 20 17:57:26 HOSTNAME.com hab[30310]: deployment-service.default(O): failed to start server: failed to initialize secret store: user lookup: user: unknown user hab
    ```

## Expected behavior
Automate will complete the deployment successfully.

## Screenshots
If applicable, add screenshots to help explain your problem.

## Versions (please complete the following information):
- OS: RHEL 7
- Browser: N/A
- Automate Build Number: latest current and dev builds

## Additional context
Add any other context about the problem here.


Pull request description:
Wrap os/user to fall back to getent
Partial fix for #4105

If the hab user and groups and not local to the system, automate is unable to start. A good portion of the problem is that we use golang's `os/user` library to get UIDs and GIDs, which just looks at `/etc/passwd` and `/etc/group` when compiled without CGO. The bulk of this PR works around that. Instead of using `os/user`, we create out own user library which first tries `os/user`, the falls back to shelling out to `getent`. We can't shell out to any getent though, we need to find a system one as its going to know how to deal with the systems `/etc/nsswitch.conf`.

Unfortunately, this does not fix everything. The habitat supervisor links against a glibc that isn't going to lookup the nss modules (`libnss_*`) on the system. That means just because your system tools say a user/group exist, doesn't mean that hab will agree. To get around this, for now, we need to lay down a systemd unit overlay:
```
cat > /etc/systemd/system/chef-automate.service.d/custom.conf <<EOF
[Service]
Environment=LD_PRELOAD=/lib64/libnss_ldap.so
EOF
```
Doing this also makes all the places we depend on `core/coreutils` work. I think our main use-case would be `chown`.
Alas, we use busybox in a bunch of places to get chown, the `LD_PRELOAD` wont help.

Things that will happen in another PR:
- Stop using busybox and perhaps create a `chown` that can benefit from our `getent` hack in golang. Its also unclear we need to be using chown in a lot of places that we use it. automate-elasticsearch and automate-es-gateway currently complain, but automate-postgresql, automate-deployment, automate-cs-nginx, automate-ui, and automate-build-api-proxy need to be audited.
- We need changes to the supervisor so it will work without `LD_PRELOAD`


Patch:
diff --git a/.bldr.toml b/.bldr.toml
index 5a34adf99..1d4b453d5 100644
--- a/.bldr.toml
+++ b/.bldr.toml
@@ -34,6 +34,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/github.com/apache/thrift/*",
   "vendor/github.com/codahale/hdrhistogram/*",
@@ -124,6 +125,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/uuid4/*",
   "lib/version/*",
   "vendor/github.com/apache/thrift/*",
@@ -191,6 +193,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/uuid4/*",
   "lib/validate/*",
   "lib/version/*",
@@ -403,6 +406,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/github.com/apache/thrift/*",
   "vendor/github.com/beorn7/perks/*",
@@ -507,6 +511,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/cloud.google.com/go/compute/*",
   "vendor/contrib.go.opencensus.io/exporter/ocagent/*",
@@ -626,6 +631,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/github.com/BurntSushi/toml/*",
   "vendor/github.com/apache/thrift/*",
@@ -754,6 +760,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/uuid4/*",
   "lib/version/*",
   "vendor/cloud.google.com/go/compute/*",
@@ -940,6 +947,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/uuid4/*",
   "lib/version/*",
   "vendor/cloud.google.com/go/compute/*",
@@ -1111,6 +1119,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/github.com/apache/thrift/*",
   "vendor/github.com/codahale/hdrhistogram/*",
@@ -1250,6 +1259,7 @@ paths = [
   "lib/timef/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/github.com/apache/thrift/*",
   "vendor/github.com/beorn7/perks/*",
@@ -1366,6 +1376,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/cloud.google.com/go/compute/*",
   "vendor/contrib.go.opencensus.io/exporter/ocagent/*",
@@ -1481,6 +1492,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/github.com/apache/thrift/*",
   "vendor/github.com/buger/jsonparser/*",
@@ -1544,6 +1556,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/github.com/apache/thrift/*",
   "vendor/github.com/codahale/hdrhistogram/*",
@@ -1659,6 +1672,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/github.com/apache/thrift/*",
   "vendor/github.com/codahale/hdrhistogram/*",
@@ -1724,6 +1738,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/github.com/alexedwards/scs/*",
   "vendor/github.com/apache/thrift/*",
@@ -1795,6 +1810,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/github.com/apache/thrift/*",
   "vendor/github.com/codahale/hdrhistogram/*",
@@ -1904,6 +1920,7 @@ paths = [
   "components/automate-cs-nginx/*",
   "lib/io/*",
   "lib/secrets/*",
+  "lib/user/*",
   "vendor/github.com/pkg/errors/*",
   "vendor/github.com/sirupsen/logrus/*",
   "vendor/go.uber.org/atomic/*",
@@ -1954,6 +1971,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/github.com/apache/thrift/*",
   "vendor/github.com/codahale/hdrhistogram/*",
@@ -2079,6 +2097,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/github.com/apache/thrift/*",
   "vendor/github.com/codahale/hdrhistogram/*",
@@ -2139,6 +2158,7 @@ paths = [
   "lib/stringutils/*",
   "lib/tls/*",
   "lib/tracing/*",
+  "lib/user/*",
   "lib/version/*",
   "vendor/github.com/apache/thrift/*",
   "vendor/github.com/codahale/hdrhistogram/*",
diff --git a/.expeditor/verify_private.pipeline.yml b/.expeditor/verify_private.pipeline.yml
index d2a0db6c4..ce5baf13f 100644
--- a/.expeditor/verify_private.pipeline.yml
+++ b/.expeditor/verify_private.pipeline.yml
@@ -883,3 +883,18 @@ steps:
       executor:
         linux:
           privileged: true
+
+  - label: "ldap hab user"
+    command:
+      - integration/run_test integration/tests/ldap_hab_user.sh
+    timeout_in_minutes: 30
+    expeditor:
+      secrets:
+        A2_LICENSE:
+          path: secret/a2/license
+          field: license
+      executor:
+        linux:
+          privileged: true
+
+
diff --git a/components/automate-cli/cmd/chef-automate/backup.go b/components/automate-cli/cmd/chef-automate/backup.go
index 777f061d7..23d8111ba 100644
--- a/components/automate-cli/cmd/chef-automate/backup.go
+++ b/components/automate-cli/cmd/chef-automate/backup.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
-	"os/user"
 	"path"
 	"path/filepath"
 	"sort"
@@ -26,6 +25,7 @@ import (
 	"github.com/chef/automate/components/automate-deployment/pkg/preflight"
 	"github.com/chef/automate/components/automate-deployment/pkg/target"
 	"github.com/chef/automate/lib/io/fileutils"
+	"github.com/chef/automate/lib/user"
 )
 
 const restoreRecovery = `Check the logs (journalctl -u chef-automate) for errors related
diff --git a/components/automate-deployment/pkg/backup/operations.go b/components/automate-deployment/pkg/backup/operations.go
index c2bc26f2a..b68d8c5da 100644
--- a/components/automate-deployment/pkg/backup/operations.go
+++ b/components/automate-deployment/pkg/backup/operations.go
@@ -7,7 +7,6 @@ import (
 	"io"
 	"os"
 	"os/exec"
-	"os/user"
 	"path"
 	"path/filepath"
 	"strconv"
@@ -23,6 +22,7 @@ import (
 	"github.com/chef/automate/components/automate-deployment/pkg/manifest"
 	"github.com/chef/automate/lib/platform/command"
 	"github.com/chef/automate/lib/platform/pg"
+	"github.com/chef/automate/lib/user"
 )
 
 const (
diff --git a/components/automate-deployment/pkg/bootstrapbundle/bundle_creator.go b/components/automate-deployment/pkg/bootstrapbundle/bundle_creator.go
index 8fdf0a7a9..8bf6b9af8 100644
--- a/components/automate-deployment/pkg/bootstrapbundle/bundle_creator.go
+++ b/components/automate-deployment/pkg/bootstrapbundle/bundle_creator.go
@@ -4,15 +4,16 @@ import (
 	"archive/tar"
 	"io"
 	"os"
-	"os/user"
 	"path"
 	"strconv"
 	"strings"
 
+	"github.com/pkg/errors"
+
 	"github.com/chef/automate/lib/io/fileutils"
 	"github.com/chef/automate/lib/product"
 	"github.com/chef/automate/lib/stringutils"
-	"github.com/pkg/errors"
+	"github.com/chef/automate/lib/user"
 )
 
 const magicHeader = "ABB-1\n\n"
diff --git a/components/automate-deployment/pkg/client/deployer.go b/components/automate-deployment/pkg/client/deployer.go
index 47be0011e..eb76663b5 100644
--- a/components/automate-deployment/pkg/client/deployer.go
+++ b/components/automate-deployment/pkg/client/deployer.go
@@ -13,7 +13,6 @@ import (
 	"math/big"
 	"os"
 	"os/exec"
-	"os/user"
 	"path"
 	"path/filepath"
 	"strconv"
@@ -39,6 +38,7 @@ import (
 	"github.com/chef/automate/lib/platform/command"
 	"github.com/chef/automate/lib/platform/pg"
 	"github.com/chef/automate/lib/platform/sys"
+	"github.com/chef/automate/lib/user"
 )
 
 // deployTimeout is used as the timeout for the deployers initial
diff --git a/components/automate-deployment/pkg/preflight/checks.go b/components/automate-deployment/pkg/preflight/checks.go
index 81b166c44..a5ebcf4f4 100644
--- a/components/automate-deployment/pkg/preflight/checks.go
+++ b/components/automate-deployment/pkg/preflight/checks.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"fmt"
 	"os"
-	"os/user"
 	"reflect"
 	"strings"
 
@@ -17,6 +16,7 @@ import (
 	"github.com/chef/automate/components/automate-deployment/pkg/services"
 	"github.com/chef/automate/components/automate-grpc/protoc-gen-a2-config/api/a2conf"
 	"github.com/chef/automate/lib/proc"
+	"github.com/chef/automate/lib/user"
 )
 
 func RootUserRequiredCheck() Check {
diff --git a/components/automate-deployment/pkg/target/local_target.go b/components/automate-deployment/pkg/target/local_target.go
index 090f527aa..842e0e784 100644
--- a/components/automate-deployment/pkg/target/local_target.go
+++ b/components/automate-deployment/pkg/target/local_target.go
@@ -13,7 +13,6 @@ import (
 	"net"
 	"net/http"
 	"os"
-	"os/user"
 	"path"
 	"path/filepath"
 	"strings"
@@ -35,6 +34,7 @@ import (
 	"github.com/chef/automate/lib/io/fileutils"
 	"github.com/chef/automate/lib/platform/command"
 	"github.com/chef/automate/lib/platform/sys"
+	"github.com/chef/automate/lib/user"
 )
 
 // LocalTarget struct
diff --git a/components/automate-platform-tools/cmd/secrets-helper/secrets-helper.go b/components/automate-platform-tools/cmd/secrets-helper/secrets-helper.go
index 8a549023f..ab4f7c7d7 100644
--- a/components/automate-platform-tools/cmd/secrets-helper/secrets-helper.go
+++ b/components/automate-platform-tools/cmd/secrets-helper/secrets-helper.go
@@ -27,7 +27,6 @@ import (
 	"io/ioutil"
 	"os"
 	"os/exec"
-	"os/user"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -38,6 +37,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/chef/automate/lib/secrets"
+	"github.com/chef/automate/lib/user"
 )
 
 //
diff --git a/components/compliance-service/cmd/inspec_runner/inspec_runner.go b/components/compliance-service/cmd/inspec_runner/inspec_runner.go
index f9482c603..bfda36d84 100644
--- a/components/compliance-service/cmd/inspec_runner/inspec_runner.go
+++ b/components/compliance-service/cmd/inspec_runner/inspec_runner.go
@@ -6,12 +6,12 @@ import (
 	"fmt"
 	"log"
 	"os"
-	"os/user"
 	"path"
 	"strconv"
 	"syscall"
 
 	"github.com/chef/automate/components/compliance-service/cmd/inspec_runner/platform"
+	"github.com/chef/automate/lib/user"
 )
 
 // Set at build time via linker flags.
diff --git a/lib/io/fileutils/access_linux.go b/lib/io/fileutils/access_linux.go
index 161f8755b..3a3ca9452 100644
--- a/lib/io/fileutils/access_linux.go
+++ b/lib/io/fileutils/access_linux.go
@@ -8,11 +8,11 @@ package fileutils
 
 import (
 	"os"
-	"os/user"
 	"path/filepath"
 	"strconv"
 	"syscall"
 
+	"github.com/chef/automate/lib/user"
 	"github.com/pkg/errors"
 )
 
diff --git a/lib/platform/command/command.go b/lib/platform/command/command.go
index 2b8eadba0..80f104594 100644
--- a/lib/platform/command/command.go
+++ b/lib/platform/command/command.go
@@ -11,7 +11,6 @@ import (
 	"io"
 	"os"
 	"os/exec"
-	"os/user"
 	"strconv"
 	"syscall"
 	"time"
@@ -20,6 +19,7 @@ import (
 	"github.com/sirupsen/logrus"
 
 	"github.com/chef/automate/lib/platform/sys"
+	"github.com/chef/automate/lib/user"
 )
 
 type WaitFunc func() error
diff --git a/lib/secrets/secrets.go b/lib/secrets/secrets.go
index 625b30c07..c0b13ee52 100644
--- a/lib/secrets/secrets.go
+++ b/lib/secrets/secrets.go
@@ -11,7 +11,6 @@ import (
 	"encoding/hex"
 	"io/ioutil"
 	"os"
-	"os/user"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -19,6 +18,7 @@ import (
 	"github.com/pkg/errors"
 
 	"github.com/chef/automate/lib/io/fileutils"
+	"github.com/chef/automate/lib/user"
 )
 
 // Commonly used secrets names. When accessing this library from
diff --git a/lib/user/lookup.go b/lib/user/lookup.go
new file mode 100644
index 000000000..d15ff102a
--- /dev/null
+++ b/lib/user/lookup.go
@@ -0,0 +1,237 @@
+package user
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	gouser "os/user"
+	"path/filepath"
+	"strconv"
+	"strings"
+)
+
+type UnknownGroupIdError = gouser.UnknownGroupIdError
+type UnknownGroupError = gouser.UnknownGroupError
+type UnknownUserError = gouser.UnknownUserError
+type UnknownUserIdError = gouser.UnknownUserIdError
+
+type User = gouser.User
+type Group = gouser.Group
+
+var DefaultLookupProvider LookupProvider = &chainLookupProvider{
+	providers: []LookupProvider{
+		goLookupProvider{},
+		newGetentLookupProvider(),
+	},
+}
+
+type LookupProvider interface {
+	Lookup(username string) (*User, error)
+	LookupId(uid string) (*User, error)
+	LookupGroup(groupname string) (*Group, error)
+	LookupGroupId(gid string) (*Group, error)
+}
+
+func Lookup(username string) (*User, error) {
+	return DefaultLookupProvider.Lookup(username)
+}
+
+func LookupId(uid string) (*User, error) {
+	return DefaultLookupProvider.LookupId(uid)
+}
+
+func LookupGroup(groupname string) (*Group, error) {
+	return DefaultLookupProvider.LookupGroup(groupname)
+}
+
+func LookupGroupId(gid string) (*Group, error) {
+	return DefaultLookupProvider.LookupGroupId(gid)
+}
+
+type chainLookupProvider struct {
+	providers []LookupProvider
+}
+
+func (c *chainLookupProvider) Lookup(username string) (*User, error) {
+	var lastErr error
+	for _, provider := range c.providers {
+		u, err := provider.Lookup(username)
+		if err == nil {
+			return u, nil
+		}
+		lastErr = err
+	}
+	return nil, lastErr
+}
+
+func (c *chainLookupProvider) LookupId(uid string) (*User, error) {
+	var lastErr error
+	for _, provider := range c.providers {
+		u, err := provider.LookupId(uid)
+		if err == nil {
+			return u, nil
+		}
+		lastErr = err
+	}
+	return nil, lastErr
+}
+
+func (c *chainLookupProvider) LookupGroup(groupname string) (*Group, error) {
+	var lastErr error
+	for _, provider := range c.providers {
+		g, err := provider.LookupGroup(groupname)
+		if err == nil {
+			return g, nil
+		}
+		lastErr = err
+	}
+	return nil, lastErr
+}
+
+func (c *chainLookupProvider) LookupGroupId(gid string) (*Group, error) {
+	var lastErr error
+	for _, provider := range c.providers {
+		g, err := provider.LookupGroupId(gid)
+		if err == nil {
+			return g, nil
+		}
+		lastErr = err
+	}
+	return nil, lastErr
+}
+
+type goLookupProvider struct {
+}
+
+func (goLookupProvider) Lookup(username string) (*User, error) {
+	return gouser.Lookup(username)
+}
+
+func (goLookupProvider) LookupId(uid string) (*User, error) {
+	return gouser.LookupId(uid)
+}
+
+func (goLookupProvider) LookupGroup(groupname string) (*Group, error) {
+	return gouser.LookupGroup(groupname)
+}
+
+func (goLookupProvider) LookupGroupId(gid string) (*gouser.Group, error) {
+	return gouser.LookupGroupId(gid)
+}
+
+type getentLookupProvider struct {
+	getentPath string
+}
+
+func newGetentLookupProvider() getentLookupProvider {
+	getentPath := "getent"
+	if p := os.Getenv("CHEF_AUTOMATE_GETENT_PATH"); p != "" {
+		getentPath = p
+	} else {
+		// Find a system getent
+		for _, d := range []string{"/bin", "/usr/bin"} {
+			p := filepath.Join(d, "getent")
+			d, err := os.Stat(p)
+			if err != nil {
+				continue
+			}
+			if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
+				getentPath = p
+				break
+			}
+		}
+	}
+
+	return getentLookupProvider{
+		getentPath: getentPath,
+	}
+}
+
+func (g getentLookupProvider) Lookup(username string) (*User, error) {
+	parts, err := g.getent("passwd", username, 7)
+	if err != nil {
+		var exitError *exec.ExitError
+		if errors.As(err, &exitError) {
+			if exitError.ExitCode() == 2 {
+				return nil, UnknownUserError(username)
+			}
+		}
+		return nil, err
+	}
+	return &User{
+		Username: parts[0],
+		Uid:      parts[2],
+		Gid:      parts[3],
+		HomeDir:  parts[5],
+	}, nil
+}
+
+func (g getentLookupProvider) LookupId(uid string) (*User, error) {
+	parts, err := g.getent("passwd", uid, 7)
+	if err != nil {
+		var exitError *exec.ExitError
+		if errors.As(err, &exitError) {
+			if exitError.ExitCode() == 2 {
+				uidNum, _ := strconv.Atoi(uid) // nolint: errcheck
+				return nil, UnknownUserIdError(uidNum)
+			}
+		}
+		return nil, err
+	}
+	return &User{
+		Username: parts[0],
+		Uid:      parts[2],
+		Gid:      parts[3],
+		HomeDir:  parts[5],
+	}, nil
+}
+
+func (g getentLookupProvider) LookupGroup(groupname string) (*Group, error) {
+	parts, err := g.getent("group", groupname, 4)
+	if err != nil {
+		var exitError *exec.ExitError
+		if errors.As(err, &exitError) {
+			if exitError.ExitCode() == 2 {
+				return nil, UnknownGroupError(groupname)
+			}
+		}
+		return nil, err
+	}
+
+	return &Group{
+		Gid:  parts[2],
+		Name: parts[0],
+	}, nil
+}
+
+func (g getentLookupProvider) LookupGroupId(gid string) (*Group, error) {
+	parts, err := g.getent("group", gid, 4)
+	if err != nil {
+		var exitError *exec.ExitError
+		if errors.As(err, &exitError) {
+			if exitError.ExitCode() == 2 {
+				return nil, UnknownGroupIdError(gid)
+			}
+		}
+		return nil, err
+	}
+
+	return &Group{
+		Gid:  parts[2],
+		Name: parts[0],
+	}, nil
+}
+
+func (g getentLookupProvider) getent(database string, name string, numParts int) ([]string, error) {
+	cmd := exec.Command(g.getentPath, database, name)
+	output, err := cmd.Output()
+	if err != nil {
+		return nil, err
+	}
+	parts := strings.Split(string(output), ":")
+	if len(parts) < numParts {
+		return nil, fmt.Errorf("'getent %s %s' invalid output: %s", database, name, output)
+	}
+	return parts, nil
+}


Test patch:
diff --git a/components/automate-deployment/pkg/bootstrapbundle/bundle_creator_test.go b/components/automate-deployment/pkg/bootstrapbundle/bundle_creator_test.go
index 680982adb..837e936bf 100644
--- a/components/automate-deployment/pkg/bootstrapbundle/bundle_creator_test.go
+++ b/components/automate-deployment/pkg/bootstrapbundle/bundle_creator_test.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"io/ioutil"
 	"os"
-	"os/user"
 	"path"
 	"path/filepath"
 	"strconv"
@@ -12,6 +11,7 @@ import (
 	"testing"
 
 	"github.com/chef/automate/lib/product"
+	"github.com/chef/automate/lib/user"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
diff --git a/components/automate-deployment/pkg/preflight/preflight_test.go b/components/automate-deployment/pkg/preflight/preflight_test.go
index 941d5197f..d77f54eb0 100644
--- a/components/automate-deployment/pkg/preflight/preflight_test.go
+++ b/components/automate-deployment/pkg/preflight/preflight_test.go
@@ -3,7 +3,6 @@ package preflight_test
 import (
 	"fmt"
 	"os"
-	"os/user"
 	"strings"
 	"testing"
 
@@ -13,6 +12,7 @@ import (
 
 	"github.com/chef/automate/components/automate-deployment/pkg/preflight"
 	"github.com/chef/automate/lib/proc"
+	"github.com/chef/automate/lib/user"
 )
 
 type fileOrError struct {
diff --git a/components/automate-deployment/pkg/preflight/test_probe.go b/components/automate-deployment/pkg/preflight/test_probe.go
index c5e4647b1..a7641856e 100644
--- a/components/automate-deployment/pkg/preflight/test_probe.go
+++ b/components/automate-deployment/pkg/preflight/test_probe.go
@@ -7,7 +7,6 @@ import (
 	"net/http"
 	"os"
 	"os/exec"
-	"os/user"
 	"strings"
 	"time"
 
@@ -15,6 +14,7 @@ import (
 
 	"github.com/chef/automate/lib/io/fileutils"
 	"github.com/chef/automate/lib/platform/sys"
+	"github.com/chef/automate/lib/user"
 )
 
 type Reporter interface {
diff --git a/components/automate-deployment/pkg/target/local_target_test.go b/components/automate-deployment/pkg/target/local_target_test.go
index ef139bb63..ba0ec07b4 100644
--- a/components/automate-deployment/pkg/target/local_target_test.go
+++ b/components/automate-deployment/pkg/target/local_target_test.go
@@ -11,7 +11,6 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"os"
-	"os/user"
 	"path/filepath"
 	"syscall"
 	"testing"
@@ -29,6 +28,7 @@ import (
 	"github.com/chef/automate/components/automate-deployment/pkg/manifest"
 	"github.com/chef/automate/components/automate-deployment/pkg/manifest/parser"
 	"github.com/chef/automate/lib/platform/command"
+	"github.com/chef/automate/lib/user"
 )
 
 type mockUserLookupProvider struct {
diff --git a/components/automate-platform-tools/cmd/secrets-helper/secrets_helper_test.go b/components/automate-platform-tools/cmd/secrets-helper/secrets_helper_test.go
index aca3f57c8..6fb9a27b0 100644
--- a/components/automate-platform-tools/cmd/secrets-helper/secrets_helper_test.go
+++ b/components/automate-platform-tools/cmd/secrets-helper/secrets_helper_test.go
@@ -6,7 +6,6 @@ import (
 	"io/ioutil"
 	"os"
 	"os/exec"
-	"os/user"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -17,6 +16,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/chef/automate/lib/io/fileutils"
+	"github.com/chef/automate/lib/user"
 )
 
 type testSecretsHelper struct {
diff --git a/integration/tests/ldap_hab_user.sh b/integration/tests/ldap_hab_user.sh
new file mode 100644
index 000000000..3288f1893
--- /dev/null
+++ b/integration/tests/ldap_hab_user.sh
@@ -0,0 +1,85 @@
+#!/bin/bash
+
+#shellcheck disable=SC2034
+test_name="ldap_hab_user"
+test_backup_restore=true
+
+do_setup() {
+    previous_umask=$(umask)
+    umask 022
+    yum -y install openldap compat-openldap openldap-clients openldap-servers nslcd nss-pam-ldapd authconfig
+    systemctl start slapd
+
+    ldapmodify -Y EXTERNAL  -H 'ldapi:///' <<EOF
+dn: olcDatabase={2}hdb,cn=config
+changetype: modify
+replace: olcSuffix
+olcSuffix: dc=example,dc=local
+
+dn: olcDatabase={2}hdb,cn=config
+changetype: modify
+replace: olcRootDN
+olcRootDN: cn=ldapadm,dc=example,dc=local
+
+dn: olcDatabase={2}hdb,cn=config
+changetype: modify
+replace: olcRootPW
+# password is password
+olcRootPW: {SSHA}OiFN/myARrDNIDNkVLmDbqnO5Oh2hlR8
+EOF
+
+    ldapadd -Y EXTERNAL -H 'ldapi:///' -f /etc/openldap/schema/cosine.ldif
+    ldapadd -Y EXTERNAL -H 'ldapi:///' -f /etc/openldap/schema/nis.ldif 
+    ldapadd -Y EXTERNAL -H 'ldapi:///' -f /etc/openldap/schema/inetorgperson.ldif
+
+    ldapadd -x -w password -D "cn=ldapadm,dc=example,dc=local" <<EOF
+dn: dc=example,dc=local
+dc: example
+o: Example Organization
+objectClass: dcObject
+objectClass: organization
+
+dn: ou=users,dc=example,dc=local
+ou: users
+objectClass: top
+objectClass: organizationalUnit
+
+dn: ou=groups,dc=example,dc=local
+ou: groups
+objectClass: top
+objectClass: organizationalUnit
+
+dn: cn=hab,ou=groups,dc=example,dc=local
+objectClass: posixGroup
+objectClass: top
+cn: hab
+gidNumber: 21000
+
+dn: uid=hab,ou=users,dc=example,dc=local
+objectClass: top
+objectClass: account
+objectClass: posixAccount
+objectClass: shadowAccount
+cn: hab
+uid: hab
+uidNumber: 11000
+gidNumber: 21000
+homeDirectory: /hab
+loginShell: /sbin/nologin
+EOF
+
+    authconfig --enableldap --enableldapauth --ldapserver=127.0.0.1 --ldapbasedn="dc=example,dc=local" --enablemkhomedir --update
+    systemctl restart nslcd
+
+    mkdir /etc/systemd/system/chef-automate.service.d
+    cat > /etc/systemd/system/chef-automate.service.d/custom.conf <<EOF
+[Service]
+Environment=LD_PRELOAD=/lib64/libnss_ldap.so
+EOF
+
+    umask "$previous_umask"
+}
+
+do_test_restore() {
+    do_test_restore_default || return 1
+}
diff --git a/lib/platform/pg/exporter_integration_test.go b/lib/platform/pg/exporter_integration_test.go
index 492c88e01..a8b58d93c 100644
--- a/lib/platform/pg/exporter_integration_test.go
+++ b/lib/platform/pg/exporter_integration_test.go
@@ -7,7 +7,6 @@ import (
 	"net"
 	"os"
 	"os/exec"
-	"os/user"
 	"path"
 	"strconv"
 	"testing"
@@ -19,6 +18,7 @@ import (
 
 	"github.com/chef/automate/lib/platform/command"
 	"github.com/chef/automate/lib/platform/pg"
+	"github.com/chef/automate/lib/user"
 )
 
 var testConnInfo *pg.A1ConnInfo
diff --git a/lib/user/lookup_test.go b/lib/user/lookup_test.go
new file mode 100644
index 000000000..6fa8d664a
--- /dev/null
+++ b/lib/user/lookup_test.go
@@ -0,0 +1,140 @@
+package user
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/suite"
+)
+
+type LookupTestSuite struct {
+	suite.Suite
+	p LookupProvider
+}
+
+func (suite *LookupTestSuite) TestLookupUser() {
+	suite.Run("user is not found", func() {
+		_, err := suite.p.Lookup("thisuserdoesnotexist")
+		suite.Require().Error(err)
+		suite.Require().True(errors.Is(err, UnknownUserError("thisuserdoesnotexist")))
+	})
+
+	suite.Run("user exists", func() {
+		u, err := suite.p.Lookup("root")
+		suite.Require().NoError(err)
+		suite.Require().Equal("root", u.Username)
+		suite.Require().Equal("0", u.Uid)
+		suite.Require().Equal("0", u.Gid)
+		suite.Require().Contains(u.HomeDir, "/root")
+	})
+}
+
+func (suite *LookupTestSuite) TestLookupUserId() {
+	suite.Run("user is not found", func() {
+		_, err := suite.p.LookupId("65535") // wikipedia says this value is avoided for reasons
+		suite.Require().Error(err)
+		suite.Require().True(errors.Is(err, UnknownUserIdError(65535)))
+	})
+
+	suite.Run("user exists", func() {
+		u, err := suite.p.Lookup("root")
+		suite.Require().NoError(err)
+		suite.Require().Equal("root", u.Username)
+		suite.Require().Equal("0", u.Uid)
+		suite.Require().Equal("0", u.Gid)
+		suite.Require().Contains(u.HomeDir, "/root")
+	})
+}
+
+func (suite *LookupTestSuite) TestLookupGroup() {
+	suite.Run("group is not found", func() {
+		_, err := suite.p.LookupGroup("thisgroupdoesnotexist")
+		suite.Require().Error(err)
+		suite.Require().True(errors.Is(err, UnknownGroupError("thisgroupdoesnotexist")))
+	})
+
+	suite.Run("group exists", func() {
+		g, err := suite.p.LookupGroup("root")
+		suite.Require().NoError(err)
+		suite.Require().Equal("root", g.Name)
+		suite.Require().Equal("0", g.Gid)
+	})
+}
+
+func (suite *LookupTestSuite) TestLookupGroupId() {
+	suite.Run("group is not found", func() {
+		_, err := suite.p.LookupGroupId("4294967295")
+		suite.Require().Error(err)
+		suite.Require().True(errors.Is(err, UnknownGroupIdError("4294967295")))
+	})
+
+	suite.Run("group exists", func() {
+		g, err := suite.p.LookupGroupId("0")
+		suite.Require().NoError(err)
+		suite.Require().Equal("root", g.Name)
+		suite.Require().Equal("0", g.Gid)
+	})
+}
+
+func TestGetent(t *testing.T) {
+	s := &LookupTestSuite{
+		p: newGetentLookupProvider(),
+	}
+	suite.Run(t, s)
+}
+
+func TestGouser(t *testing.T) {
+	s := &LookupTestSuite{
+		p: goLookupProvider{},
+	}
+	suite.Run(t, s)
+}
+
+type funcLookupProvider struct {
+	LookupFunc        func(username string) (*User, error)
+	LookupIdFunc      func(uid string) (*User, error)
+	LookupGroupFunc   func(groupname string) (*Group, error)
+	LookupGroupIdFunc func(gid string) (*Group, error)
+}
+
+func (p funcLookupProvider) Lookup(username string) (*User, error) {
+	return p.LookupFunc(username)
+}
+
+func (p funcLookupProvider) LookupId(uid string) (*User, error) {
+	return p.LookupIdFunc(uid)
+}
+
+func (p funcLookupProvider) LookupGroup(groupname string) (*Group, error) {
+	return p.LookupGroupFunc(groupname)
+}
+
+func (p funcLookupProvider) LookupGroupId(gid string) (*Group, error) {
+	return p.LookupGroupIdFunc(gid)
+}
+
+func TestChainFallback(t *testing.T) {
+	fallback := funcLookupProvider{
+		LookupFunc: func(username string) (*User, error) {
+			return nil, errors.New("fails")
+		},
+		LookupIdFunc: func(uid string) (*User, error) {
+			return nil, errors.New("fails")
+		},
+		LookupGroupFunc: func(groupname string) (*Group, error) {
+			return nil, errors.New("fails")
+		},
+		LookupGroupIdFunc: func(gid string) (*Group, error) {
+			return nil, errors.New("fails")
+		},
+	}
+	s := &LookupTestSuite{
+		p: &chainLookupProvider{
+			providers: []LookupProvider{
+				fallback,
+				&goLookupProvider{},
+			},
+		},
+	}
+	suite.Run(t, s)
+}