Skip to content

Commit

Permalink
DAOS-15849 control: Add client uid map to agent config
Browse files Browse the repository at this point in the history
Allow daos_agent to optionally handle unresolvable client
uids via custom mapping. In deployments where the agent
may not have access to the same user namespace as client
applications (e.g. in containerized deployments), the
client_user_map can provide a fallback mechanism for
resolving the client uids to known usernames for the
purpose of applying ACL permissions tests.

Example agent config:

transport_config:
  client_user_map:
    default:
      user: nobody
      group: nobody
    1000:
      user: joe
      group: blow

Features: control
Required-githooks: true
Change-Id: I72905ccc5ddee27fc2101aa4358a14e352c86253
Signed-off-by: Michael MacDonald <mjmac@google.com>
  • Loading branch information
mjmac committed May 15, 2024
1 parent b81d4f6 commit d283800
Show file tree
Hide file tree
Showing 11 changed files with 658 additions and 373 deletions.
61 changes: 46 additions & 15 deletions src/control/cmd/daos_agent/security_rpc.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// (C) Copyright 2018-2022 Intel Corporation.
// (C) Copyright 2018-2024 Intel Corporation.
//
// SPDX-License-Identifier: BSD-2-Clause-Patent
//
Expand All @@ -9,6 +9,9 @@ package main
import (
"context"
"net"
"os/user"

"github.com/pkg/errors"

"github.com/daos-stack/daos/src/control/drpc"
"github.com/daos-stack/daos/src/control/lib/daos"
Expand All @@ -17,21 +20,25 @@ import (
"github.com/daos-stack/daos/src/control/security/auth"
)

// SecurityModule is the security drpc module struct
type SecurityModule struct {
log logging.Logger
ext auth.UserExt
config *security.TransportConfig
}
type (
credSignerFn func(*auth.CredentialRequest) (*auth.Credential, error)

// SecurityModule is the security drpc module struct
SecurityModule struct {
log logging.Logger
signCredential credSignerFn

config *security.TransportConfig
}
)

// NewSecurityModule creates a new module with the given initialized TransportConfig
func NewSecurityModule(log logging.Logger, tc *security.TransportConfig) *SecurityModule {
mod := SecurityModule{
log: log,
config: tc,
return &SecurityModule{
log: log,
signCredential: auth.GetSignedCredential,
config: tc,
}
mod.ext = &auth.External{}
return &mod
}

// HandleCall is the handler for calls to the SecurityModule
Expand All @@ -46,6 +53,10 @@ func (m *SecurityModule) HandleCall(_ context.Context, session *drpc.Session, me
// getCredentials generates a signed user credential based on the data attached to
// the Unix Domain Socket.
func (m *SecurityModule) getCredential(session *drpc.Session) ([]byte, error) {
if session == nil {
return nil, drpc.NewFailureWithMessage("session is nil")
}

uConn, ok := session.Conn.(*net.UnixConn)
if !ok {
return nil, drpc.NewFailureWithMessage("connection is not a unix socket")
Expand All @@ -64,10 +75,30 @@ func (m *SecurityModule) getCredential(session *drpc.Session) ([]byte, error) {
return m.credRespWithStatus(daos.BadCert)
}

cred, err := auth.AuthSysRequestFromCreds(m.ext, info, signingKey)
req := auth.NewCredentialRequest(info, signingKey)
cred, err := m.signCredential(req)
if err != nil {
m.log.Errorf("%s: failed to get AuthSys struct: %s", info, err)
return m.credRespWithStatus(daos.MiscError)
if err := func() error {
if !errors.Is(err, user.UnknownUserIdError(info.Uid())) {
return err
}

mu := m.config.ClientUserMap.Lookup(info.Uid())
if mu == nil {
return user.UnknownUserIdError(info.Uid())
}

req.WithUserAndGroup(mu.User, mu.Group, mu.Groups...)
cred, err = m.signCredential(req)
if err != nil {
return err
}

return nil
}(); err != nil {
m.log.Errorf("%s: failed to get user credential: %s", info, err)
return m.credRespWithStatus(daos.MiscError)
}
}

m.log.Tracef("%s: successfully signed credential", info)
Expand Down
136 changes: 131 additions & 5 deletions src/control/cmd/daos_agent/security_rpc_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// (C) Copyright 2019-2022 Intel Corporation.
// (C) Copyright 2019-2024 Intel Corporation.
//
// SPDX-License-Identifier: BSD-2-Clause-Patent
//
Expand All @@ -9,8 +9,11 @@ package main
import (
"errors"
"net"
"os/user"
"testing"

"github.com/google/go-cmp/cmp"
"golang.org/x/sys/unix"
"google.golang.org/protobuf/proto"

"github.com/daos-stack/daos/src/control/common/test"
Expand Down Expand Up @@ -74,6 +77,7 @@ func setupTestUnixConn(t *testing.T) (*net.UnixConn, func()) {
}

func getClientConn(t *testing.T, path string) drpc.DomainSocketClient {
t.Helper()
client := drpc.NewClientConnection(path)
if err := client.Connect(test.Context(t)); err != nil {
t.Fatalf("Failed to connect: %v", err)
Expand All @@ -82,6 +86,8 @@ func getClientConn(t *testing.T, path string) drpc.DomainSocketClient {
}

func expectCredResp(t *testing.T, respBytes []byte, expStatus int32, expCred bool) {
t.Helper()

if respBytes == nil {
t.Error("Expected non-nil response")
}
Expand All @@ -105,7 +111,6 @@ func TestAgentSecurityModule_RequestCreds_OK(t *testing.T) {
defer cleanup()

mod := NewSecurityModule(log, defaultTestTransportConfig())
mod.ext = auth.NewMockExtWithUser("agent-test", 0, 0)
respBytes, err := callRequestCreds(mod, t, log, conn)

if err != nil {
Expand Down Expand Up @@ -176,9 +181,8 @@ func TestAgentSecurityModule_RequestCreds_BadUid(t *testing.T) {
defer cleanup()

mod := NewSecurityModule(log, defaultTestTransportConfig())
mod.ext = &auth.MockExt{
LookupUserIDErr: errors.New("LookupUserID"),
LookupGroupIDErr: errors.New("LookupGroupID"),
mod.signCredential = func(_ *auth.CredentialRequest) (*auth.Credential, error) {
return nil, errors.New("LookupUserID")
}
respBytes, err := callRequestCreds(mod, t, log, conn)

Expand All @@ -188,3 +192,125 @@ func TestAgentSecurityModule_RequestCreds_BadUid(t *testing.T) {

expectCredResp(t, respBytes, int32(daos.MiscError), false)
}

func TestAgent_SecurityRPC_getCredential(t *testing.T) {
type response struct {
cred *auth.Credential
err error
}
testCred := &auth.Credential{
Token: &auth.Token{Flavor: auth.Flavor_AUTH_SYS, Data: []byte("test-token")},
Origin: "test-origin",
}
miscErrBytes, err := proto.Marshal(
&auth.GetCredResp{
Status: int32(daos.MiscError),
},
)
if err != nil {
t.Fatalf("Couldn't marshal misc error: %v", err)
}
successBytes, err := proto.Marshal(
&auth.GetCredResp{
Status: 0,
Cred: testCred,
},
)
if err != nil {
t.Fatalf("Couldn't marshal success: %v", err)
}

for name, tc := range map[string]struct {
transportCfg *security.TransportConfig
responses []response
expBytes []byte
expErr error
}{
"lookup miss": {
transportCfg: defaultTestTransportConfig(),
responses: []response{
{
cred: nil,
err: user.UnknownUserIdError(unix.Getuid()),
},
},
expBytes: miscErrBytes,
},
"lookup OK": {
transportCfg: func() *security.TransportConfig {
cfg := defaultTestTransportConfig()
cfg.ClientUserMap = security.ClientUserMap{
uint32(unix.Getuid()): &security.MappedClientUser{
User: "test-user",
},
}
return cfg
}(),
responses: []response{
{
cred: nil,
err: user.UnknownUserIdError(unix.Getuid()),
},
{
cred: testCred,
err: nil,
},
},
expBytes: successBytes,
},
"lookup OK, but retried request fails": {
transportCfg: func() *security.TransportConfig {
cfg := defaultTestTransportConfig()
cfg.ClientUserMap = security.ClientUserMap{
uint32(unix.Getuid()): &security.MappedClientUser{
User: "test-user",
},
}
return cfg
}(),
responses: []response{
{
cred: nil,
err: user.UnknownUserIdError(unix.Getuid()),
},
{
cred: nil,
err: errors.New("oops"),
},
},
expBytes: miscErrBytes,
},
} {
t.Run(name, func(t *testing.T) {
log, buf := logging.NewTestLogger(t.Name())
defer test.ShowBufferOnFailure(t, buf)

// Set up a real unix socket so we can make a real connection
conn, cleanup := setupTestUnixConn(t)
defer cleanup()

mod := NewSecurityModule(log, tc.transportCfg)
mod.signCredential = func() func(req *auth.CredentialRequest) (*auth.Credential, error) {
var idx int
return func(req *auth.CredentialRequest) (*auth.Credential, error) {
defer func() {
if idx < len(tc.responses)-1 {
idx++
}
}()
t.Logf("returning response %d: %+v", idx, tc.responses[idx])
return tc.responses[idx].cred, tc.responses[idx].err
}
}()

respBytes, gotErr := callRequestCreds(mod, t, log, conn)
test.CmpErr(t, tc.expErr, gotErr)
if tc.expErr != nil {
return
}
if diff := cmp.Diff(tc.expBytes, respBytes); diff != "" {
t.Errorf("unexpected response (-want +got):\n%s", diff)
}
})
}
}
21 changes: 7 additions & 14 deletions src/control/lib/control/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"encoding/json"
"fmt"
"math"
"os/user"
"sort"
"strings"
"time"
Expand All @@ -28,7 +29,6 @@ import (
"github.com/daos-stack/daos/src/control/fault/code"
"github.com/daos-stack/daos/src/control/lib/daos"
"github.com/daos-stack/daos/src/control/lib/ranklist"
"github.com/daos-stack/daos/src/control/security/auth"
"github.com/daos-stack/daos/src/control/server/storage"
"github.com/daos-stack/daos/src/control/system"
)
Expand All @@ -50,22 +50,19 @@ func checkUUID(uuidStr string) error {

// formatNameGroup converts system names to principals, If user or group is not
// provided, the effective user and/or effective group will be used.
func formatNameGroup(ext auth.UserExt, usr string, grp string) (string, string, error) {
func formatNameGroup(usr string, grp string) (string, string, error) {
if usr == "" || grp == "" {
eUsr, err := ext.Current()
eUsr, err := user.Current()
if err != nil {
return "", "", err
}

if usr == "" {
usr = eUsr.Username()
usr = eUsr.Username
}
if grp == "" {
gid, err := eUsr.Gid()
if err != nil {
return "", "", err
}
eGrp, err := ext.LookupGroupID(gid)
gid := eUsr.Gid
eGrp, err := user.LookupGroupId(gid)
if err != nil {
return "", "", err
}
Expand Down Expand Up @@ -202,7 +199,6 @@ type (
// PoolCreateReq contains the parameters for a pool create request.
PoolCreateReq struct {
poolRequest
userExt auth.UserExt
User string
UserGroup string
ACL *AccessControlList `json:"-"`
Expand Down Expand Up @@ -287,11 +283,8 @@ func poolCreateReqChkSizes(log debugLogger, getMaxPoolSz maxPoolSizeGetter, req
}

func poolCreateGenPBReq(ctx context.Context, rpcClient UnaryInvoker, in *PoolCreateReq) (out *mgmtpb.PoolCreateReq, err error) {
if in.userExt == nil {
in.userExt = &auth.External{}
}
// ensure pool ownership is set up correctly
in.User, in.UserGroup, err = formatNameGroup(in.userExt, in.User, in.UserGroup)
in.User, in.UserGroup, err = formatNameGroup(in.User, in.UserGroup)
if err != nil {
return
}
Expand Down
5 changes: 0 additions & 5 deletions src/control/lib/control/pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (
"github.com/daos-stack/daos/src/control/lib/daos"
"github.com/daos-stack/daos/src/control/lib/ranklist"
"github.com/daos-stack/daos/src/control/logging"
"github.com/daos-stack/daos/src/control/security/auth"
"github.com/daos-stack/daos/src/control/server/storage"
"github.com/daos-stack/daos/src/control/system"
)
Expand Down Expand Up @@ -511,7 +510,6 @@ func TestControl_poolCreateReqChkSizes(t *testing.T) {
}

func TestControl_PoolCreate(t *testing.T) {
mockExt := auth.NewMockExtWithUser("poolTest", 0, 0)
mockTierRatios := []float64{0.06, 0.94}
mockTierBytes := []uint64{humanize.GiByte * 6, humanize.GiByte * 94}
validReq := &PoolCreateReq{
Expand Down Expand Up @@ -704,9 +702,6 @@ func TestControl_PoolCreate(t *testing.T) {
ctx := test.Context(t)
mi := NewMockInvoker(log, mic)

if tc.req.userExt == nil {
tc.req.userExt = mockExt
}
gotResp, gotErr := PoolCreate(ctx, mi, tc.req)
test.CmpErr(t, tc.expErr, gotErr)
if tc.expErr != nil {
Expand Down
Loading

0 comments on commit d283800

Please sign in to comment.