diff options
| author | Shulhan <ms@kilabit.info> | 2023-10-21 17:17:13 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2023-10-21 17:17:13 +0700 |
| commit | 761212298e017f58345aa948aebedc2c07b73753 (patch) | |
| tree | 28c556aa99678979a4162aa0e50d036d287a96ad | |
| parent | 28f9ab76a83efcfcf777e571fea826e2dfc3e28e (diff) | |
| download | awwan-761212298e017f58345aa948aebedc2c07b73753.tar.xz | |
all: implement remote "#get:" and "#put:" with owner and mode
In remote environment, using magic command "#get:" or "#put:" with owner
and mode like "#get:$OWNER+$MODE" or "#put:$OWNER+MODE" will changes
the file owner to $USER or $GROUP and/or permission to $MODE.
The file owner will not works if user does not have permission.
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | awwan.go | 5 | ||||
| -rw-r--r-- | awwan_play_test.go | 192 | ||||
| -rw-r--r-- | session.go | 40 | ||||
| -rw-r--r-- | ssh_client.go | 25 | ||||
| -rw-r--r-- | testdata/play/awwanssh.test/awwan.env | 2 | ||||
| -rw-r--r-- | testdata/play/awwanssh.test/get.aww | 12 | ||||
| -rw-r--r-- | testdata/play/awwanssh.test/get_test.data | 20 | ||||
| -rw-r--r-- | testdata/play/awwanssh.test/plain.txt | 1 | ||||
| -rw-r--r-- | testdata/play/awwanssh.test/put.aww | 12 | ||||
| -rw-r--r-- | testdata/play/awwanssh.test/put_test.data | 12 |
11 files changed, 315 insertions, 7 deletions
@@ -25,4 +25,5 @@ /testdata/encrypt-without-passphrase/.awwan.env.vault /testdata/local/.cache /testdata/local/put_with_mode.txt +/testdata/play/.cache/ /www-awwan @@ -292,7 +292,10 @@ func (aww *Awwan) Play(req *Request) (err error) { return fmt.Errorf("%s: %w", logp, err) } - ses.executeScriptOnRemote(req, pos) + err = ses.executeScriptOnRemote(req, pos) + if err != nil { + return fmt.Errorf(`%s: %w`, logp, err) + } } return nil diff --git a/awwan_play_test.go b/awwan_play_test.go new file mode 100644 index 0000000..7bcc827 --- /dev/null +++ b/awwan_play_test.go @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: 2023 M. Shulhan <ms@kilabit.info> +// SPDX-License-Identifier: GPL-3.0-or-later + +//go:build integration + +package awwan + +import ( + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/shuLhan/share/lib/test" +) + +type testCaseGetPut struct { + desc string + fileDest string + lineRange string + + expContent string + expError string + + expMode fs.FileMode +} + +func TestAwwan_Play_Get(t *testing.T) { + var ( + baseDir = `testdata/play` + scriptDir = filepath.Join(baseDir, `awwanssh.test`) + scriptFile = filepath.Join(scriptDir, `get.aww`) + + tdata *test.Data + aww *Awwan + err error + ) + + tdata, err = test.LoadData(filepath.Join(scriptDir, `get_test.data`)) + if err != nil { + t.Fatal(err) + } + + aww, err = New(baseDir) + if err != nil { + t.Fatal(err) + } + + var cases = []testCaseGetPut{{ + desc: `WithoutPermission`, + lineRange: `3`, + expError: string(tdata.Output[`WithoutPermission:error`]), + }, { + desc: `WithMode`, + lineRange: `7`, + fileDest: filepath.Join(scriptDir, `tmp`, `get_with_mode.txt`), + expContent: string(tdata.Output[`/etc/os-release`]), + expMode: 0624, + }, { + desc: `WithOwner`, + lineRange: `12`, + fileDest: filepath.Join(scriptDir, `tmp`, `get_with_owner.txt`), + expContent: string(tdata.Output[`/etc/os-release`]), + expMode: 0644, + expError: string(tdata.Output[`WithOwner:error`]), + }} + + var ( + c testCaseGetPut + fi os.FileInfo + gotContent []byte + ) + + for _, c = range cases { + t.Log(c.desc) + + if len(c.fileDest) != 0 { + _ = os.Remove(c.fileDest) + } + + var req = NewRequest(CommandModePlay, scriptFile, c.lineRange) + + err = aww.Play(req) + if err != nil { + test.Assert(t, `play error`, c.expError, err.Error()) + } + + if len(c.fileDest) == 0 { + continue + } + + // File successfully copied but maybe error when setting + // owner or permission. + + gotContent, err = os.ReadFile(c.fileDest) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, `content`, c.expContent, string(gotContent)) + + fi, err = os.Stat(c.fileDest) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, `mode`, c.expMode, fi.Mode().Perm()) + } +} + +func TestAwwan_Play_Put(t *testing.T) { + var ( + baseDir = `testdata/play` + scriptDir = filepath.Join(baseDir, `awwanssh.test`) + scriptFile = filepath.Join(scriptDir, `put.aww`) + + tdata *test.Data + aww *Awwan + err error + ) + + tdata, err = test.LoadData(filepath.Join(scriptDir, `put_test.data`)) + if err != nil { + t.Fatal(err) + } + + aww, err = New(baseDir) + if err != nil { + t.Fatal(err) + } + + var cases = []testCaseGetPut{{ + desc: `WithoutPermission`, + lineRange: `3`, + expError: string(tdata.Output[`WithoutPermission:error`]), + }, { + desc: `WithMode`, + lineRange: `7`, + fileDest: `/home/awwanssh/put_with_mode.txt`, + expContent: string(tdata.Output[`plain.txt`]), + expMode: 0624, + }, { + desc: `WithOwner`, + lineRange: `12`, + fileDest: `/home/awwanssh/put_with_owner.txt`, + expContent: string(tdata.Output[`plain.txt`]), + expMode: 0666, + expError: string(tdata.Output[`WithOwner:error`]), + }} + + var ( + c testCaseGetPut + fi os.FileInfo + gotContent []byte + ) + + for _, c = range cases { + t.Log(c.desc) + + if len(c.fileDest) != 0 { + _ = os.Remove(c.fileDest) + } + + var req = NewRequest(CommandModePlay, scriptFile, c.lineRange) + + err = aww.Play(req) + if err != nil { + test.Assert(t, `play error`, c.expError, err.Error()) + } + + if len(c.fileDest) == 0 { + continue + } + + // File successfully copied but maybe error when setting + // owner or permission. + + gotContent, err = os.ReadFile(c.fileDest) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, `content`, c.expContent, string(gotContent)) + + fi, err = os.Stat(c.fileDest) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, `mode`, c.expMode, fi.Mode().Perm()) + } +} @@ -170,13 +170,29 @@ func (ses *Session) Copy(stmt *Statement) (err error) { // Get copy file from remote to local. func (ses *Session) Get(stmt *Statement) (err error) { - var logp = "Get" + var ( + logp = `Get` + src = stmt.args[0] + dst = stmt.args[1] + ) - err = ses.sshc.get(stmt.args[0], stmt.args[1]) + err = ses.sshc.get(src, dst) if err != nil { return fmt.Errorf(`%s: %w`, logp, err) } - + if stmt.mode != 0 { + err = os.Chmod(dst, stmt.mode) + if err != nil { + return fmt.Errorf(`%s: chmod %o %q: %w`, logp, stmt.mode, dst, err) + } + } + if len(stmt.owner) != 0 { + var chownStmt = fmt.Sprintf(`chown %s %q`, stmt.owner, dst) + err = libexec.Run(chownStmt, nil, nil) + if err != nil { + return fmt.Errorf(`%s: %s: %w`, logp, chownStmt, err) + } + } return nil } @@ -205,6 +221,18 @@ func (ses *Session) Put(stmt *Statement) (err error) { if err != nil { return fmt.Errorf("%s: %w", logp, err) } + if stmt.mode != 0 { + err = ses.sshc.chmod(dst, stmt.mode) + if err != nil { + return fmt.Errorf(`%s: %w`, logp, err) + } + } + if len(stmt.owner) != 0 { + err = ses.sshc.chown(dst, stmt.owner) + if err != nil { + return fmt.Errorf(`%s: %w`, logp, err) + } + } return nil } @@ -416,7 +444,7 @@ func (ses *Session) executeScriptOnLocal(req *Request, pos linePosition) (err er return nil } -func (ses *Session) executeScriptOnRemote(req *Request, pos linePosition) { +func (ses *Session) executeScriptOnRemote(req *Request, pos linePosition) (err error) { var max = int64(len(req.script.stmts)) if pos.start > max { return @@ -440,7 +468,6 @@ func (ses *Session) executeScriptOnRemote(req *Request, pos linePosition) { fmt.Fprintf(req.stdout, "\n--> %s: %3d: %s\n", ses.sshc.conn, x, stmt.String()) - var err error switch stmt.kind { case statementKindDefault: err = ses.sshc.conn.Execute(string(stmt.raw)) @@ -455,9 +482,10 @@ func (ses *Session) executeScriptOnRemote(req *Request, pos linePosition) { } if err != nil { fmt.Fprintf(req.stderr, "!!! %s\n", err) - break + return err } } + return nil } // generateFileInput read the content of file input "in", apply the session diff --git a/ssh_client.go b/ssh_client.go index 71f3b3b..692f050 100644 --- a/ssh_client.go +++ b/ssh_client.go @@ -6,6 +6,7 @@ package awwan import ( "fmt" "io" + "io/fs" "path/filepath" "github.com/shuLhan/share/lib/ascii" @@ -81,6 +82,30 @@ func newSshClient(section *config.Section, dirTmp string, stdout, stderr io.Writ return sshc, nil } +// chmod change the remoteFile permission. +func (sshc *sshClient) chmod(remoteFile string, perm fs.FileMode) (err error) { + var chmodStmt = fmt.Sprintf(`chmod %o %q`, perm, remoteFile) + + err = sshc.conn.Execute(chmodStmt) + if err != nil { + return err + } + return nil +} + +// chown change the owner of remoteFile. +// The owner parameter can be set to user only "user", group only +// ":group", or user and group "user:group". +func (sshc *sshClient) chown(remoteFile, owner string) (err error) { + var chownStmt = fmt.Sprintf(`chown %s %q`, owner, remoteFile) + + err = sshc.conn.Execute(chownStmt) + if err != nil { + return err + } + return nil +} + // get the remote file and write it to local path. func (sshc *sshClient) get(remote, local string) (err error) { if sshc.sftpc == nil { diff --git a/testdata/play/awwanssh.test/awwan.env b/testdata/play/awwanssh.test/awwan.env new file mode 100644 index 0000000..642535b --- /dev/null +++ b/testdata/play/awwanssh.test/awwan.env @@ -0,0 +1,2 @@ +[host] +name = awwanssh.test diff --git a/testdata/play/awwanssh.test/get.aww b/testdata/play/awwanssh.test/get.aww new file mode 100644 index 0000000..c10ae27 --- /dev/null +++ b/testdata/play/awwanssh.test/get.aww @@ -0,0 +1,12 @@ +## Get without read permission. + +#get: /etc/shadow {{.ScriptDir}}/tmp/shadow + +## Get and changes file permission. + +#get:+624 /etc/os-release {{.ScriptDir}}/tmp/get_with_mode.txt + +## Setting group to "bin" should be an error since current user does not have +## privileged. + +#get:awwan:bin /etc/os-release {{.ScriptDir}}/tmp/get_with_owner.txt diff --git a/testdata/play/awwanssh.test/get_test.data b/testdata/play/awwanssh.test/get_test.data new file mode 100644 index 0000000..ad4577d --- /dev/null +++ b/testdata/play/awwanssh.test/get_test.data @@ -0,0 +1,20 @@ +Test input and output for "#get". + +<<< WithoutPermission:error +Play: Get: Get: permission denied + +<<< /etc/os-release +NAME="Arch Linux" +PRETTY_NAME="Arch Linux" +ID=arch +BUILD_ID=rolling +ANSI_COLOR="38;2;23;147;209" +HOME_URL="https://archlinux.org/" +DOCUMENTATION_URL="https://wiki.archlinux.org/" +SUPPORT_URL="https://bbs.archlinux.org/" +BUG_REPORT_URL="https://bugs.archlinux.org/" +PRIVACY_POLICY_URL="https://terms.archlinux.org/docs/privacy-policy/" +LOGO=archlinux-logo + +<<< WithOwner:error +Play: Get: chown awwan:bin "/home/awwan/src/testdata/play/awwanssh.test/tmp/get_with_owner.txt": exit status 1 diff --git a/testdata/play/awwanssh.test/plain.txt b/testdata/play/awwanssh.test/plain.txt new file mode 100644 index 0000000..5067c47 --- /dev/null +++ b/testdata/play/awwanssh.test/plain.txt @@ -0,0 +1 @@ +The host name is {{.Val "host::name"}}. diff --git a/testdata/play/awwanssh.test/put.aww b/testdata/play/awwanssh.test/put.aww new file mode 100644 index 0000000..9779ed2 --- /dev/null +++ b/testdata/play/awwanssh.test/put.aww @@ -0,0 +1,12 @@ +## Put without write permission. + +#put: {{.ScriptDir}}/plain.txt /etc/plain.txt + +## Put and changes file permission. + +#put:+624 {{.ScriptDir}}/plain.txt put_with_mode.txt + +## Setting group to "bin" should be an error since current user does not have +## privileged. + +#put:awwan:bin+666 {{.ScriptDir}}/plain.txt put_with_owner.txt diff --git a/testdata/play/awwanssh.test/put_test.data b/testdata/play/awwanssh.test/put_test.data new file mode 100644 index 0000000..61ac09c --- /dev/null +++ b/testdata/play/awwanssh.test/put_test.data @@ -0,0 +1,12 @@ +Test input and output for "#put". + +<<< plain.txt +The host name is awwanssh.test. + + +<<< WithoutPermission:error +Play: Put: Put: permission denied + +<<< WithOwner:error +Play: Put: ssh: Run "chown awwan:bin \"put_with_owner.txt\"": Process exited with status 1 + |
