Skip to content

Commit

Permalink
add plugin for mapping user and groups inside container
Browse files Browse the repository at this point in the history
Plugin adds two flags to start:

- `--user-mapping-users` - a list of users, groups, and the uid and gid
  to replace them with inside the container
- `--user-mapping-owner` - boolean flag if the ISC owner user (such as
  `cacheusr` or `irisowner`) should be replaced with the uid and gid of
  the current user.

This allows files created by Cache/Ensemble/IRIS to be owned by the user
running the container. This is useful if you have the `homedir` or other
plugin mounting a volume from your current user. That way the ISC
instance doesn't write files you cannot access.
  • Loading branch information
b-dean committed Feb 26, 2024
1 parent 79446c7 commit 91cdeec
Show file tree
Hide file tree
Showing 4 changed files with 347 additions and 0 deletions.
2 changes: 2 additions & 0 deletions internal/plugins/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/ontariosystems/iscenv/v3/plugins/lifecycle/license-key"
"github.com/ontariosystems/iscenv/v3/plugins/lifecycle/service-bindings"
"github.com/ontariosystems/iscenv/v3/plugins/lifecycle/shm"
"github.com/ontariosystems/iscenv/v3/plugins/lifecycle/user-mapping"
"github.com/ontariosystems/iscenv/v3/plugins/versions/local"
"github.com/ontariosystems/iscenv/v3/plugins/versions/quay"
)
Expand Down Expand Up @@ -63,6 +64,7 @@ func init() {
addPlugin(iscenv.LifecyclerKey, new(cspplugin.Plugin))
addPlugin(iscenv.LifecyclerKey, new(servicebindingsplugin.Plugin))
addPlugin(iscenv.LifecyclerKey, new(shmplugin.Plugin))
addPlugin(iscenv.LifecyclerKey, new(usermappingplugin.Plugin))
}

func addPlugin(pluginType string, plugin InternalPlugin) {
Expand Down
182 changes: 182 additions & 0 deletions plugins/lifecycle/user-mapping/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
Copyright 2024 Finvi, Ontario Systems
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package usermappingplugin

import (
"fmt"
"os"
"os/user"
"strconv"
"strings"

"github.com/ontariosystems/iscenv/v3/iscenv"
"github.com/ontariosystems/isclib/v2"
log "github.com/sirupsen/logrus"
)

const (
pluginKey = "user-mapping"
mappingFlag = "users"
ownerFlag = "owner"
envMapping = "ISCENV_USER_MAPPING"
envOwner = "ISCENV_REMAP_OWNER"
)

var plog = log.WithField("plugin", pluginKey)

type Plugin struct{}

// Main serves as the main entry point for the plugin
func (p *Plugin) Main() {
iscenv.ServeLifecyclePlugin(p)
}

// Key returns the unique identifier for the plugin
func (*Plugin) Key() string {
return pluginKey
}

func (*Plugin) Flags() (iscenv.PluginFlags, error) {
fb := iscenv.NewPluginFlagsBuilder()
fb.AddFlag(mappingFlag, true, "", "Comma separated list of username:group:uid:gid to replace in instance")
fb.AddFlag(ownerFlag, true, true, "Should the ISC instance owner user be remapped to the current user")

return fb.Flags()
}

func (*Plugin) Environment(_ string, flags map[string]interface{}) ([]string, error) {
mapping, ok := flags[mappingFlag].(string)
if !ok {
return nil, fmt.Errorf("%s is not a string", mappingFlag)
}
env := []string{fmt.Sprintf("%s=%s", envMapping, mapping)}

remapOwner, ok := flags[ownerFlag].(bool)
if !ok {
return nil, fmt.Errorf("%s is not a bool", ownerFlag)
}

if remapOwner {
u, err := user.Current()
if err != nil {
return nil, err
}

env = append(env, fmt.Sprintf("%s=%s:%s", envOwner, u.Uid, u.Gid))
}

return env, nil
}

func (*Plugin) Copies(_ string, _ map[string]interface{}) ([]string, error) {
return nil, nil
}

func (*Plugin) Volumes(_ string, _ map[string]interface{}) ([]string, error) { return nil, nil }

func (*Plugin) Ports(_ string, _ map[string]interface{}) ([]string, error) {
return nil, nil
}

func (*Plugin) AfterStart(_ *iscenv.ISCInstance) error {
return nil
}

func (*Plugin) AfterStop(_ *iscenv.ISCInstance) error {
return nil
}

func (*Plugin) BeforeRemove(_ *iscenv.ISCInstance) error {
return nil
}

func (p *Plugin) BeforeInstance(state *isclib.Instance) error {
mappingEnv := os.Getenv(envMapping)
replacements := []userInfo{}
if mappingEnv != "" {
mappings := strings.Split(mappingEnv, ",")
for _, m := range mappings {
parts := strings.Split(m, ":")
if len(parts) != 4 {
return fmt.Errorf("user mapping has wrong number of parts. mapping: '%s'", m)
}

uid, err := strconv.Atoi(parts[2])
if err != nil {
return err
}

gid, err := strconv.Atoi(parts[3])
if err != nil {
return err
}

replacements = append(replacements, userInfo{
user: parts[0],
group: parts[1],
uid: uid,
gid: gid,
})
}
}

remapOwnerEnv := os.Getenv(envOwner)
if remapOwnerEnv != "" {
parts := strings.Split(remapOwnerEnv, ":")
if len(parts) != 2 {
return fmt.Errorf("%s has invalid value", envOwner)
}

hostUid, err := strconv.Atoi(parts[0])
if err != nil {
return err
}

hostGid, err := strconv.Atoi(parts[1])
if err != nil {
return err
}

ownerUser, ownerGroup, err := state.DetermineOwner()
if err != nil {
return err
}

replacements = append(replacements, userInfo{
user: ownerUser,
group: ownerGroup,
uid: hostUid,
gid: hostGid,
})
}

for _, ui := range replacements {
if err := replaceUser(ui); err != nil {
return err
}
}

return nil
}

func (*Plugin) WithInstance(_ *isclib.Instance) error {
return nil
}

func (*Plugin) AfterInstance(_ *isclib.Instance) error {
return nil
}
93 changes: 93 additions & 0 deletions plugins/lifecycle/user-mapping/replaceuser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
Copyright 2024 Finvi, Ontario Systems
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package usermappingplugin

import (
"os/exec"
"os/user"
"strconv"

log "github.com/sirupsen/logrus"
)

type userInfo struct {
user string
group string
uid int
gid int
}

func replaceUser(ui userInfo) error {
l := plog.WithFields(log.Fields{
"user": ui.user,
"group": ui.group,
})

u, err := user.Lookup(ui.user)
if err != nil {
return err
}
oldUID, err := strconv.Atoi(u.Uid)
if err != nil {
return err
}

g, err := user.LookupGroup(ui.group)
if err != nil {
return err
}
oldGID, err := strconv.Atoi(g.Gid)
if err != nil {
return err
}

l = l.WithFields(log.Fields{
"oldUID": oldUID,
"oldGID": oldGID,
"newUID": strconv.Itoa(ui.uid),
"newGID": strconv.Itoa(ui.gid),
})

if ui.uid == 0 {
l.Warn("Refusing to switch ISC manager UID to 0 (root)")
return nil
}

if ui.gid == 0 {
l.Warn("Refusing to switch ISC manager GID to 0 (root)")
return nil
}

if out, err := exec.Command("usermod", "-o", "-u", strconv.Itoa(ui.uid), ui.user).CombinedOutput(); err != nil {
l.WithField("output", out).WithError(err).Error("Failed to execute usermod")
return err
}

if out, err := exec.Command("groupmod", "-o", "-g", strconv.Itoa(ui.gid), ui.group).CombinedOutput(); err != nil {
l.WithField("output", out).WithError(err).Error("Failed to execute groupmod")
return err
}
l.Info("Replaced user and group ids")

l.Info("Searching file system for files owned by old IDs and changing ownership")
if err := swapOwnersOnDevice("/", oldUID, oldGID, ui.uid, ui.gid); err != nil {
return nil
}

l.Info("Replaced file system ownership")
return nil
}
70 changes: 70 additions & 0 deletions plugins/lifecycle/user-mapping/swapownersondevice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
Copyright 2024 Finvi, Ontario Systems
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package usermappingplugin

import (
"os"
"path/filepath"
"syscall"
)

func swapOwnersOnDevice(root string, oldUID, oldGID, newUID, newGID int) error {
// Nothing to do, yay
if oldUID == newUID && oldGID == newGID {
return nil
}

info, err := os.Stat(root)
if err != nil {
return err
}

stat := info.Sys().(*syscall.Stat_t)
rootDev := stat.Dev

return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
// if there was an error walking just abort the whole process
if err != nil {
return err
}

stat := info.Sys().(*syscall.Stat_t)
// We're not on the same mount point anymore so skip this directory
if stat.Dev != rootDev {
return filepath.SkipDir
}

// This is a bit cumbersome, but it's to avoid having to do multiple Chown calls
uid := int(stat.Uid)
gid := int(stat.Gid)

// These aren't the IDs you're looking for
if uid != oldUID && gid != oldGID {
return nil
}

if uid == oldUID {
uid = newUID
}

if gid == oldGID {
gid = newGID
}

return os.Chown(path, uid, gid)
})
}

0 comments on commit 91cdeec

Please sign in to comment.