Skip to content

Commit

Permalink
make sure to run instance commands as manager user
Browse files Browse the repository at this point in the history
If we have a non-root instance and are running as root, we need to
switch to the other user before we can do things like `iris list`,
`iris start`, etc
  • Loading branch information
b-dean committed Feb 23, 2024
1 parent cb5e3ce commit 643e92e
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 35 deletions.
115 changes: 96 additions & 19 deletions instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ const (

var (
// ErrLoadFailed is an error signifying that the loading of the source code failed
ErrLoadFailed = errors.New("load did not appear to finish successfully")
getQlist = qlist
ErrLoadFailed = errors.New("load did not appear to finish successfully")
getQlist = qlist
parameterReader = fileParameterReader
)

// An Instance represents an instance of Caché/Ensemble/Iris on the current system.
Expand Down Expand Up @@ -91,7 +92,29 @@ type Instance struct {
// Update will query the the underlying instance and update the Instance fields with its current state.
// It returns any error encountered.
func (i *Instance) Update() error {
q, err := getQlist(i.Name)
procAttr, err := i.managerSysProc()
if err != nil {
return err
}

// if we didn't get a manager proc, try to update without it to find the manager
if procAttr == nil {
q, err := getQlist(i.Name, nil)
if err != nil {
return err
}

if err := i.UpdateFromQList(q); err != nil {
return err
}

procAttr, err = i.managerSysProc()
if err != nil {
return err
}
}

q, err := getQlist(i.Name, procAttr)
if err != nil {
return err
}
Expand Down Expand Up @@ -228,6 +251,32 @@ func (i *Instance) DetermineManager() (string, string, error) {
return i.getUserAndGroupFromParameters("Manager", managerUserKey, managerGroupKey)
}

// managerSysProc is used to run instance management commands as a different user (if the current user isn't the manager)
func (i *Instance) managerSysProc() (*syscall.SysProcAttr, error) {
// can't find manager if we don't have a directory
if i.Directory == "" {
return nil, nil
}

mgr, _, err := i.DetermineManager()
if err != nil {
return nil, err
}

uid, gid, err := lookupUser(mgr)
if err != nil {
return nil, err
}

log.WithFields(log.Fields{"user": mgr, "uid": uid, "gid": gid}).Debug("instance manager sysproc")
return &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
},
}, nil
}

// DetermineOwner will determine the owner of an instance by reader the parameters file associate with this instance.
// The owner is the user which owns the files from the installers and as who most Caché processes will be running.
// It returns the owner and owner group as strings and any error encountered.
Expand Down Expand Up @@ -323,7 +372,14 @@ func (i *Instance) LicenseKeyFilePath() string {
func (i *Instance) Start() error {
// TODO: Think about a nozstu flag if there's a reason
if i.Status.Down() {
if output, err := exec.Command(i.controlPath(), "start", i.Name, "quietly").CombinedOutput(); err != nil {
cmd := exec.Command(i.controlPath(), "start", i.Name, "quietly")
procAttr, err := i.managerSysProc()
if err != nil {
return err
}

cmd.SysProcAttr = procAttr
if output, err := cmd.CombinedOutput(); err != nil {
log.WithError(err).WithFields(log.Fields{"output": string(output), "instance": i.Name}).Debug("Error start quietly")
return fmt.Errorf("error starting instance, error: %w", err)
}
Expand Down Expand Up @@ -351,7 +407,13 @@ func (i *Instance) Stop() error {
args = append(args, "bypass")
}
args = append(args, "quietly")
if output, err := exec.Command(i.controlPath(), args...).CombinedOutput(); err != nil {
cmd := exec.Command(i.controlPath(), args...)
procAttr, err := i.managerSysProc()
if err != nil {
return err
}
cmd.SysProcAttr = procAttr
if output, err := cmd.CombinedOutput(); err != nil {
ilog.WithError(err).WithFields(log.Fields{"output": string(output), "args": args}).Debug("Error stopping")
return fmt.Errorf("error stopping instance, error: %w", err)
}
Expand Down Expand Up @@ -401,22 +463,12 @@ func (i *Instance) ExecuteAsUser(execUser string) error {
return err
}

u, err := user.Lookup(execUser)
if err != nil {
return err
}

uid, err := strconv.ParseUint(u.Uid, 10, 32)
uid, gid, err := lookupUser(execUser)
if err != nil {
return err
}

gid, err := strconv.ParseUint(u.Gid, 10, 32)
if err != nil {
return err
}

log.WithFields(log.Fields{"user": execUser, "uid": u.Uid, "gid": u.Gid}).Debug("Configured to execute as alternate user")
log.WithFields(log.Fields{"user": execUser, "uid": uid, "gid": gid}).Debug("Configured to execute as alternate user")
i.executionSysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uint32(uid),
Expand All @@ -426,6 +478,23 @@ func (i *Instance) ExecuteAsUser(execUser string) error {
return nil
}

func lookupUser(execUser string) (uid, gid uint64, err error) {
var u *user.User
u, err = user.Lookup(execUser)
if err != nil {
return
}

uid, err = strconv.ParseUint(u.Uid, 10, 32)
if err != nil {
return
}

gid, err = strconv.ParseUint(u.Gid, 10, 32)

return
}

// ImportSource will import the source specified using a glob pattern into Caché with the provided qualifiers.
// sourcePathGlob only allows a subset of glob patterns. It must be in the format /p/a/t/h/**/*.xml
//
Expand Down Expand Up @@ -562,8 +631,7 @@ func (i *Instance) ExecuteString(namespace string, code string) (string, error)
// ReadParametersISC will read the current instances parameters ISC file into a simple data structure.
// It returns the ParametersISC data structure and any error encountered.
func (i *Instance) ReadParametersISC() (ParametersISC, error) {
pfp := filepath.Join(i.Directory, iscParametersFile)
f, err := os.Open(pfp)
f, err := parameterReader(i.Directory, iscParametersFile)
if err != nil {
return nil, err
}
Expand All @@ -572,6 +640,15 @@ func (i *Instance) ReadParametersISC() (ParametersISC, error) {
return LoadParametersISC(f)
}

func fileParameterReader(directory string, file string) (io.ReadCloser, error) {
pfp := filepath.Join(directory, file)
f, err := os.Open(pfp)
if err != nil {
return nil, err
}
return f, nil
}

// WaitForReady waits indefinitely for an instance to be up and ready for use
func (i *Instance) WaitForReady(ctx context.Context) error {
return i.WaitForReadyWithInterval(ctx, 100*time.Millisecond)
Expand Down
31 changes: 27 additions & 4 deletions instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ limitations under the License.
package isclib

import (
"bytes"
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"fmt"
"io"
"os/user"
"syscall"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Instance", func() {
Expand All @@ -41,6 +46,24 @@ var _ = Describe("Instance", func() {
origCSessionCommand string
origIrisSessionCommand string
)

BeforeEach(func() {
// make just enough of a parameters.isc to be able to find the manager user
parameterReader = func(directory string, file string) (io.ReadCloser, error) {
u, err := user.Current()
if err != nil {
return nil, err
}
g, err := user.LookupGroupId(u.Gid)
if err != nil {
return nil, err
}

parametersContent := fmt.Sprintf("security_settings.manager_user: %s\nsecurity_settings.manager_group: %s", u.Username, g.Name)
return io.NopCloser(bytes.NewBufferString(parametersContent)), nil
}
})

Describe("InstanceFromQList", func() {
Context("Invalid qlist", func() {
BeforeEach(func() {
Expand Down Expand Up @@ -396,11 +419,11 @@ var _ = Describe("Instance", func() {
Context("Does come up", func() {
BeforeEach(func() {
timeout = 500 * time.Millisecond
getQlist = func(instanceName string) (string, error) {
getQlist = func(instanceName string, _ *syscall.SysProcAttr) (string, error) {
return legacyqlist, nil
}
time.AfterFunc(timeout/2, func() {
getQlist = func(instanceName string) (string, error) {
getQlist = func(instanceName string, _ *syscall.SysProcAttr) (string, error) {
return durableqlist, nil
}
})
Expand Down
19 changes: 14 additions & 5 deletions isclib.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func SetExecuteTemporaryDirectory(path string) {
// LoadInstances returns a listing of all Caché/Ensemble instances on this system.
// It returns the list of instances and any error encountered.
func LoadInstances() (Instances, error) {
qs, err := qlist("")
qs, err := qlist("", nil)
if err != nil {
return nil, err
}
Expand All @@ -194,14 +194,15 @@ func LoadInstances() (Instances, error) {
}

// LoadInstance retrieves a single instance by name.
// The instance name is case insensitive.
// The instance name is case-insensitive.
// It returns the instance and any error encountered.
func LoadInstance(name string) (*Instance, error) {
q, err := qlist(name)
if err != nil {
i := &Instance{Name: name}
if err := i.Update(); err != nil {
return nil, err
}
return InstanceFromQList(q)

return i, nil
}

// InstanceFromQList will parse the output of a qlist into an Instance struct.
Expand All @@ -213,5 +214,13 @@ func InstanceFromQList(qlist string) (*Instance, error) {
return nil, err
}

// if the status is unknown, we may be running as a different user,
// do the full update (running qlist again as the correct user)
if i.Status == InstanceStatusUnknown {
if err := i.Update(); err != nil {
return nil, err
}
}

return i, nil
}
16 changes: 10 additions & 6 deletions parametersisc.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,11 @@ func LoadParametersISC(r io.Reader) (ParametersISC, error) {

// Values will, given a set of identifiers making up a parameter key, return the values at that key
// Identifiers can be...
// the full key (group.name)
// the group, name as two separate parameters
// A single parameter representing the name of a parameter in the "" group
//
// - the full key (group.name)
// - the group, name as two separate parameters
// - A single parameter representing the name of a parameter in the "" group
//
// It returns the values at that key or an empty slice if it does not exist
func (pi ParametersISC) Values(identifiers ...string) []string {
var group, name string
Expand Down Expand Up @@ -136,9 +138,11 @@ func (pi ParametersISC) Values(identifiers ...string) []string {

// Value will, given a set of identifiers making up a parameter key, return the single value at that key
// Identifiers can be...
// the full key (group.name)
// the group, name as two separate parameters
// A single parameter representing the name of a parameter in the "" group
//
// - the full key (group.name)
// - the group, name as two separate parameters
// - A single parameter representing the name of a parameter in the "" group
//
// It returns the value if a single value exists for the key or "" if it does not
func (pi ParametersISC) Value(identifiers ...string) string {
values := pi.Values(identifiers...)
Expand Down
5 changes: 4 additions & 1 deletion qlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ import (
"fmt"
"os/exec"
"strings"
"syscall"

log "github.com/sirupsen/logrus"
)

// qlist returns the results of executing qlist for the specified instance.
// If instanceName is "", it will return the results of an argumentless qlist (which contains all instances)
// It returns a string containing the combined standard input and output of the qlist command and any error which occurred.
func qlist(instanceName string) (string, error) {
// If procAttr is not nil, it uses it to switch to run qlist as a different user
func qlist(instanceName string, procAttr *syscall.SysProcAttr) (string, error) {
// Example qlist output...
// DOCKER^/ensemble/instances/docker/^2015.2.2.805.0.16216^down, last used Fri May 13 18:12:33 2016^cache.cpf^56772^57772^62972^^
// DOCKER^/ensemble/instances/docker^2018.1.1.643.0^running, since Mon Jul 23 14:42:09 2018^iris.cpf^1972^57772^62972^ok^IRIS^^^/ensemble/instances/docker
Expand All @@ -48,6 +50,7 @@ func qlist(instanceName string) (string, error) {
return qlist, nil
}

cmd.SysProcAttr = procAttr
out, err := cmd.CombinedOutput()
if err != nil {
log.WithError(err).WithFields(log.Fields{"output": string(out), "command": cmd.Path, "args": cmd.Args}).Debug("Error running qlist")
Expand Down

0 comments on commit 643e92e

Please sign in to comment.