Merge lp:~thumper/juju-core/juju-ssh-dir-as-user into lp:~go-bot/juju-core/trunk

Proposed by Tim Penhey
Status: Merged
Approved by: Tim Penhey
Approved revision: no longer in the source branch.
Merged at revision: 2212
Proposed branch: lp:~thumper/juju-core/juju-ssh-dir-as-user
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 254 lines (+155/-8)
4 files modified
cmd/juju/main_test.go (+1/-7)
utils/ssh/clientkeys.go (+6/-1)
utils/sudo.go (+57/-0)
utils/sudo_test.go (+91/-0)
To merge this branch: bzr merge lp:~thumper/juju-core/juju-ssh-dir-as-user
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+201876@code.launchpad.net

Commit message

Fix the creation of ~/.juju/ssh to be user owned

If the user's first interaction is with bootstrapping the
local provider with sudo, we want to make sure that the
directory and content is owned by the user.

Drive by fix for the cmd/juju MainSuite. I had a root owned
~/.juju/ssh directory, and the test was trying to read it and
failing, so I isolated the test by having it use the FakeHomeSuite
which overrides $HOME (and other things).

Many other utilities written to help with dealing with
creating directories and changing ownership of files to the
user if running under sudo.

https://codereview.appspot.com/52950043/

Description of the change

Fix the creation of ~/.juju/ssh to be user owned

If the user's first interaction is with bootstrapping the
local provider with sudo, we want to make sure that the
directory and content is owned by the user.

Drive by fix for the cmd/juju MainSuite. I had a root owned
~/.juju/ssh directory, and the test was trying to read it and
failing, so I isolated the test by having it use the FakeHomeSuite
which overrides $HOME (and other things).

Many other utilities written to help with dealing with
creating directories and changing ownership of files to the
user if running under sudo.

https://codereview.appspot.com/52950043/

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

Reviewers: mp+201876_code.launchpad.net,

Message:
Please take a look.

Description:
Fix the creation of ~/.juju/ssh to be user owned

If the user's first interaction is with bootstrapping the
local provider with sudo, we want to make sure that the
directory and content is owned by the user.

Drive by fix for the cmd/juju MainSuite. I had a root owned
~/.juju/ssh directory, and the test was trying to read it and
failing, so I isolated the test by having it use the FakeHomeSuite
which overrides $HOME (and other things).

Many other utilities written to help with dealing with
creating directories and changing ownership of files to the
user if running under sudo.

https://code.launchpad.net/~thumper/juju-core/juju-ssh-dir-as-user/+merge/201876

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/52950043/

Affected files (+194, -8 lines):
   A [revision details]
   M cmd/juju/main_test.go
   M utils/file.go
   M utils/file_test.go
   M utils/ssh/clientkeys.go
   M utils/sudo.go
   M utils/sudo_test.go

Revision history for this message
Andrew Wilkins (axwalk) wrote :

https://codereview.appspot.com/52950043/diff/1/utils/file.go
File utils/file.go (right):

https://codereview.appspot.com/52950043/diff/1/utils/file.go#newcode104
utils/file.go:104: func IsDirectory(path string) bool {
In light of my comments on MkDirAllForUser, I think this should just be
ditched. Some callers will care about symlinks, some won't.

https://codereview.appspot.com/52950043/diff/1/utils/file.go#newcode106
utils/file.go:106: if os.IsNotExist(err) {
This is redundant, given the below check.

https://codereview.appspot.com/52950043/diff/1/utils/sudo.go
File utils/sudo.go (right):

https://codereview.appspot.com/52950043/diff/1/utils/sudo.go#newcode63
utils/sudo.go:63: if IsDirectory(dir) {
How about symlinks?

I think, rather than IsDirectory, use os.Lstat and os.IsNotExist. If
there's a file in the path, then the function can just fail when we
attempt to mkdir.

IOW, something like:

_, err := os.Lstat(dir)
if os.IsNotExist(err) {
     if err := MkdirAllForUser(filepath.Dir(dir), perm); err != nil {
         return err
     }
     err = MkdirForUser(dir, perm)
}
if err != nil {
     return err
}
return ChownToUser(dir)

https://codereview.appspot.com/52950043/diff/1/utils/sudo.go#newcode79
utils/sudo.go:79: os.RemoveAll(topMostDir)
This scares me. Can we please not remove things on failure? What if
chown $HOME is a symlink, and chmod fails? Then we try to RemoveAll
$HOME.

https://codereview.appspot.com/52950043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

On 2014/01/16 03:09:31, axw wrote:
> https://codereview.appspot.com/52950043/diff/1/utils/file.go
> File utils/file.go (right):

https://codereview.appspot.com/52950043/diff/1/utils/file.go#newcode104
> utils/file.go:104: func IsDirectory(path string) bool {
> In light of my comments on MkDirAllForUser, I think this should just
be ditched.
> Some callers will care about symlinks, some won't.

https://codereview.appspot.com/52950043/diff/1/utils/file.go#newcode106
> utils/file.go:106: if os.IsNotExist(err) {
> This is redundant, given the below check.

> https://codereview.appspot.com/52950043/diff/1/utils/sudo.go
> File utils/sudo.go (right):

> https://codereview.appspot.com/52950043/diff/1/utils/sudo.go#newcode63
> utils/sudo.go:63: if IsDirectory(dir) {
> How about symlinks?

> I think, rather than IsDirectory, use os.Lstat and os.IsNotExist. If
there's a
> file in the path, then the function can just fail when we attempt to
mkdir.

> IOW, something like:

> _, err := os.Lstat(dir)
> if os.IsNotExist(err) {
> if err := MkdirAllForUser(filepath.Dir(dir), perm); err != nil {
> return err
> }
> err = MkdirForUser(dir, perm)
> }
> if err != nil {
> return err
> }
> return ChownToUser(dir)

> https://codereview.appspot.com/52950043/diff/1/utils/sudo.go#newcode79
> utils/sudo.go:79: os.RemoveAll(topMostDir)
> This scares me. Can we please not remove things on failure? What if
chown $HOME
> is a symlink, and chmod fails? Then we try to RemoveAll $HOME.

LGTM pending changes

https://codereview.appspot.com/52950043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

On 2014/01/16 03:09:31, axw wrote:
> https://codereview.appspot.com/52950043/diff/1/utils/file.go
> File utils/file.go (right):

https://codereview.appspot.com/52950043/diff/1/utils/file.go#newcode104
> utils/file.go:104: func IsDirectory(path string) bool {
> In light of my comments on MkDirAllForUser, I think this should just
be ditched.
> Some callers will care about symlinks, some won't.

https://codereview.appspot.com/52950043/diff/1/utils/file.go#newcode106
> utils/file.go:106: if os.IsNotExist(err) {
> This is redundant, given the below check.

> https://codereview.appspot.com/52950043/diff/1/utils/sudo.go
> File utils/sudo.go (right):

> https://codereview.appspot.com/52950043/diff/1/utils/sudo.go#newcode63
> utils/sudo.go:63: if IsDirectory(dir) {
> How about symlinks?

> I think, rather than IsDirectory, use os.Lstat and os.IsNotExist. If
there's a
> file in the path, then the function can just fail when we attempt to
mkdir.

> IOW, something like:

> _, err := os.Lstat(dir)
> if os.IsNotExist(err) {
> if err := MkdirAllForUser(filepath.Dir(dir), perm); err != nil {
> return err
> }
> err = MkdirForUser(dir, perm)
> }
> if err != nil {
> return err
> }
> return ChownToUser(dir)

> https://codereview.appspot.com/52950043/diff/1/utils/sudo.go#newcode79
> utils/sudo.go:79: os.RemoveAll(topMostDir)
> This scares me. Can we please not remove things on failure? What if
chown $HOME
> is a symlink, and chmod fails? Then we try to RemoveAll $HOME.

LGTM pending changes

https://codereview.appspot.com/52950043/

Revision history for this message
Tim Penhey (thumper) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/juju/main_test.go'
2--- cmd/juju/main_test.go 2013-12-16 20:01:53 +0000
3+++ cmd/juju/main_test.go 2014-01-16 03:48:53 +0000
4@@ -16,14 +16,12 @@
5
6 "launchpad.net/gnuflag"
7 gc "launchpad.net/gocheck"
8- "launchpad.net/loggo"
9
10 "launchpad.net/juju-core/cmd"
11 "launchpad.net/juju-core/environs/config"
12 "launchpad.net/juju-core/juju/osenv"
13 _ "launchpad.net/juju-core/provider/dummy"
14 "launchpad.net/juju-core/testing"
15- "launchpad.net/juju-core/testing/testbase"
16 "launchpad.net/juju-core/version"
17 )
18
19@@ -32,7 +30,7 @@
20 }
21
22 type MainSuite struct {
23- testbase.LoggingSuite
24+ testing.FakeHomeSuite
25 }
26
27 var _ = gc.Suite(&MainSuite{})
28@@ -79,10 +77,6 @@
29 return helpText(&SyncToolsCommand{}, "juju sync-tools")
30 }
31
32-func (s *MainSuite) TestTearDown(c *gc.C) {
33- loggo.ResetLoggers()
34-}
35-
36 func (s *MainSuite) TestRunMain(c *gc.C) {
37 defer testing.MakeSampleHome(c).Restore()
38 // The test array structure needs to be inline here as some of the
39
40=== modified file 'utils/ssh/clientkeys.go'
41--- utils/ssh/clientkeys.go 2014-01-14 03:35:21 +0000
42+++ utils/ssh/clientkeys.go 2014-01-16 03:48:53 +0000
43@@ -61,7 +61,7 @@
44 // Directory exists but contains no keys;
45 // fall through and create one.
46 }
47- if err := os.MkdirAll(dir, 0700); err != nil {
48+ if err := utils.MkdirAllForUser(dir, 0700); err != nil {
49 return err
50 }
51 keyfile, key, err := generateClientKey(dir)
52@@ -97,6 +97,11 @@
53 os.Remove(privkeyFilename)
54 return "", nil, err
55 }
56+ if err := utils.ChownToUser(privkeyFilename, privkeyFilename+PublicKeySuffix); err != nil {
57+ os.Remove(privkeyFilename)
58+ os.Remove(privkeyFilename + PublicKeySuffix)
59+ return "", nil, err
60+ }
61 return privkeyFilename, clientPrivateKey, nil
62 }
63
64
65=== modified file 'utils/sudo.go'
66--- utils/sudo.go 2013-10-02 04:08:00 +0000
67+++ utils/sudo.go 2014-01-16 03:48:53 +0000
68@@ -6,9 +6,16 @@
69 import (
70 "fmt"
71 "os"
72+ "path/filepath"
73 "strconv"
74 )
75
76+// CheckIfRoot is a simple function that we can use to determine if
77+// the ownership of files and directories we create.
78+var CheckIfRoot = func() bool {
79+ return os.Getuid() == 0
80+}
81+
82 // SudoCallerIds returns the user id and group id of the SUDO caller.
83 // If either is unset, it returns zero for both values.
84 // An error is returned if the relevant environment variables
85@@ -30,3 +37,53 @@
86 }
87 return
88 }
89+
90+// MkdirForUser will call down to os.Mkdir and if the user is running as root,
91+// the ownership will be changed to the sudo user.
92+func MkdirForUser(dir string, perm os.FileMode) error {
93+ if err := os.Mkdir(dir, perm); err != nil {
94+ return err
95+ }
96+ return ChownToUser(dir)
97+}
98+
99+// MkdirAllForUser will call down to os.MkdirAll and if the user is running as
100+// root, the ownership will be changed to the sudo user for each directory
101+// that was created.
102+func MkdirAllForUser(dir string, perm os.FileMode) error {
103+ toCreate := []string{}
104+ path := dir
105+ for {
106+ _, err := os.Lstat(path)
107+ if os.IsNotExist(err) {
108+ toCreate = append(toCreate, path)
109+ } else {
110+ break
111+ }
112+ path = filepath.Dir(path)
113+ }
114+
115+ if err := os.MkdirAll(dir, perm); err != nil {
116+ return err
117+ }
118+ return ChownToUser(toCreate...)
119+}
120+
121+// ChownToUser will attempt to change the ownership of all the paths
122+// to the user returned by the SudoCallerIds method. Ownership change
123+// will only be attempted if we are running as root.
124+func ChownToUser(paths ...string) error {
125+ if !CheckIfRoot() {
126+ return nil
127+ }
128+ uid, gid, err := SudoCallerIds()
129+ if err != nil {
130+ return err
131+ }
132+ for _, path := range paths {
133+ if err := os.Chown(path, uid, gid); err != nil {
134+ return err
135+ }
136+ }
137+ return nil
138+}
139
140=== modified file 'utils/sudo_test.go'
141--- utils/sudo_test.go 2013-10-02 01:27:45 +0000
142+++ utils/sudo_test.go 2014-01-16 03:48:53 +0000
143@@ -5,9 +5,12 @@
144
145 import (
146 "os"
147+ "os/user"
148+ "path/filepath"
149
150 gc "launchpad.net/gocheck"
151
152+ jc "launchpad.net/juju-core/testing/checkers"
153 "launchpad.net/juju-core/testing/testbase"
154 "launchpad.net/juju-core/utils"
155 )
156@@ -16,6 +19,8 @@
157 testbase.LoggingSuite
158 }
159
160+var _ = gc.Suite(&sudoSuite{})
161+
162 func (s *sudoSuite) TestSudoCallerIds(c *gc.C) {
163 s.PatchEnvironment("SUDO_UID", "0")
164 s.PatchEnvironment("SUDO_GID", "0")
165@@ -56,3 +61,89 @@
166 }
167 }
168 }
169+
170+func (s *sudoSuite) TestMkDirForUserAsUser(c *gc.C) {
171+ dir := filepath.Join(c.MkDir(), "new-dir")
172+ err := utils.MkdirForUser(dir, 0755)
173+ c.Assert(err, gc.IsNil)
174+ c.Assert(dir, jc.IsDirectory)
175+}
176+
177+func (s *sudoSuite) setupBadSudoUid() {
178+ s.PatchEnvironment("SUDO_UID", "omg")
179+ s.PatchEnvironment("SUDO_GID", "omg")
180+ s.PatchValue(&utils.CheckIfRoot, func() bool { return true })
181+}
182+
183+func (s *sudoSuite) setupGoodSudoUid(c *gc.C) {
184+ user, err := user.Current()
185+ c.Assert(err, gc.IsNil)
186+ s.PatchEnvironment("SUDO_UID", user.Uid)
187+ s.PatchEnvironment("SUDO_GID", user.Gid)
188+ s.PatchValue(&utils.CheckIfRoot, func() bool { return true })
189+}
190+
191+func (s *sudoSuite) TestMkDirForUserRoot(c *gc.C) {
192+ s.setupGoodSudoUid(c)
193+ dir := filepath.Join(c.MkDir(), "new-dir")
194+ err := utils.MkdirForUser(dir, 0755)
195+ c.Assert(err, gc.IsNil)
196+ c.Assert(dir, jc.IsDirectory)
197+}
198+
199+func (s *sudoSuite) TestMkDirForUserWithError(c *gc.C) {
200+ s.setupBadSudoUid()
201+ dir := filepath.Join(c.MkDir(), "new-dir")
202+ err := utils.MkdirForUser(dir, 0755)
203+ c.Assert(err, gc.ErrorMatches, `invalid value "omg" for SUDO_UID`)
204+ // The directory is still there.
205+ c.Assert(dir, jc.IsDirectory)
206+}
207+
208+func (s *sudoSuite) TestMkDirAllForUserAsUser(c *gc.C) {
209+ dir := filepath.Join(c.MkDir(), "new-dir", "and-another")
210+ err := utils.MkdirAllForUser(dir, 0755)
211+ c.Assert(err, gc.IsNil)
212+ c.Assert(dir, jc.IsDirectory)
213+}
214+
215+func (s *sudoSuite) TestMkDirAllForUserRoot(c *gc.C) {
216+ s.setupGoodSudoUid(c)
217+
218+ dir := filepath.Join(c.MkDir(), "new-dir", "and-another")
219+ err := utils.MkdirAllForUser(dir, 0755)
220+ c.Assert(err, gc.IsNil)
221+ c.Assert(dir, jc.IsDirectory)
222+}
223+
224+func (s *sudoSuite) TestMkDirAllForUserWithError(c *gc.C) {
225+ s.setupBadSudoUid()
226+
227+ base := c.MkDir()
228+ dir := filepath.Join(base, "new-dir", "and-another")
229+ err := utils.MkdirAllForUser(dir, 0755)
230+ c.Assert(err, gc.ErrorMatches, `invalid value "omg" for SUDO_UID`)
231+ // The directory is still there.
232+ c.Assert(dir, jc.IsDirectory)
233+}
234+
235+func (s *sudoSuite) TestChownToUserNotRoot(c *gc.C) {
236+ // Just in case someone runs the suite as root, we don't want to fail.
237+ s.PatchValue(&utils.CheckIfRoot, func() bool { return false })
238+
239+ nonExistant := filepath.Join(c.MkDir(), "non-existant")
240+ err := utils.ChownToUser(nonExistant)
241+ c.Assert(err, gc.IsNil)
242+}
243+
244+func (s *sudoSuite) TestChownToUserBadUid(c *gc.C) {
245+ s.setupBadSudoUid()
246+ err := utils.ChownToUser(c.MkDir())
247+ c.Assert(err, gc.ErrorMatches, `invalid value "omg" for SUDO_UID`)
248+}
249+
250+func (s *sudoSuite) TestChownToUser(c *gc.C) {
251+ s.setupGoodSudoUid(c)
252+ err := utils.ChownToUser(c.MkDir(), c.MkDir())
253+ c.Assert(err, gc.IsNil)
254+}

Subscribers

People subscribed via source and target branches

to status/vote changes: