github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/hookstate/ctlcmd/ctlcmd.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2021 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 // Package ctlcmd contains the various snapctl subcommands. 21 package ctlcmd 22 23 import ( 24 "bytes" 25 "fmt" 26 "io" 27 28 "github.com/jessevdk/go-flags" 29 30 "github.com/snapcore/snapd/logger" 31 "github.com/snapcore/snapd/overlord/hookstate" 32 "github.com/snapcore/snapd/strutil" 33 ) 34 35 type baseCommand struct { 36 stdout io.Writer 37 stderr io.Writer 38 c *hookstate.Context 39 } 40 41 func (c *baseCommand) setStdout(w io.Writer) { 42 c.stdout = w 43 } 44 45 func (c *baseCommand) printf(format string, a ...interface{}) { 46 if c.stdout != nil { 47 fmt.Fprintf(c.stdout, format, a...) 48 } 49 } 50 51 func (c *baseCommand) setStderr(w io.Writer) { 52 c.stderr = w 53 } 54 55 func (c *baseCommand) errorf(format string, a ...interface{}) { 56 if c.stderr != nil { 57 fmt.Fprintf(c.stderr, format, a...) 58 } 59 } 60 61 func (c *baseCommand) setContext(context *hookstate.Context) { 62 c.c = context 63 } 64 65 func (c *baseCommand) context() *hookstate.Context { 66 return c.c 67 } 68 69 type command interface { 70 setStdout(w io.Writer) 71 setStderr(w io.Writer) 72 73 setContext(context *hookstate.Context) 74 context() *hookstate.Context 75 76 Execute(args []string) error 77 } 78 79 type commandInfo struct { 80 shortHelp string 81 longHelp string 82 generator func() command 83 hidden bool 84 } 85 86 var commands = make(map[string]*commandInfo) 87 88 func addCommand(name, shortHelp, longHelp string, generator func() command) *commandInfo { 89 cmd := &commandInfo{ 90 shortHelp: shortHelp, 91 longHelp: longHelp, 92 generator: generator, 93 } 94 commands[name] = cmd 95 return cmd 96 } 97 98 // UnsuccessfulError carries a specific exit code to be returned to the client. 99 type UnsuccessfulError struct { 100 ExitCode int 101 } 102 103 func (e UnsuccessfulError) Error() string { 104 return fmt.Sprintf("unsuccessful with exit code: %d", e.ExitCode) 105 } 106 107 // ForbiddenCommandError conveys that a command cannot be invoked in some context 108 type ForbiddenCommandError struct { 109 Message string 110 } 111 112 func (f ForbiddenCommandError) Error() string { 113 return f.Message 114 } 115 116 // ForbiddenCommand contains information about an attempt to use a command in a context where it is not allowed. 117 type ForbiddenCommand struct { 118 Uid uint32 119 Name string 120 } 121 122 func (f *ForbiddenCommand) Execute(args []string) error { 123 return &ForbiddenCommandError{Message: fmt.Sprintf("cannot use %q with uid %d, try with sudo", f.Name, f.Uid)} 124 } 125 126 // nonRootAllowed lists the commands that can be performed even when snapctl 127 // is invoked not by root. 128 var nonRootAllowed = []string{"get", "services", "set-health", "is-connected", "system-mode"} 129 130 // Run runs the requested command. 131 func Run(context *hookstate.Context, args []string, uid uint32) (stdout, stderr []byte, err error) { 132 parser := flags.NewNamedParser("snapctl", flags.PassDoubleDash|flags.HelpFlag) 133 134 // Create stdout/stderr buffers, and make sure commands use them. 135 var stdoutBuffer bytes.Buffer 136 var stderrBuffer bytes.Buffer 137 for name, cmdInfo := range commands { 138 var data interface{} 139 // commands listed here will be allowed for regular users 140 // note: commands still need valid context and snaps can only access own config. 141 if uid == 0 || strutil.ListContains(nonRootAllowed, name) { 142 cmd := cmdInfo.generator() 143 cmd.setStdout(&stdoutBuffer) 144 cmd.setStderr(&stderrBuffer) 145 cmd.setContext(context) 146 data = cmd 147 } else { 148 data = &ForbiddenCommand{Uid: uid, Name: name} 149 } 150 theCmd, err := parser.AddCommand(name, cmdInfo.shortHelp, cmdInfo.longHelp, data) 151 theCmd.Hidden = cmdInfo.hidden 152 if err != nil { 153 logger.Panicf("cannot add command %q: %s", name, err) 154 } 155 } 156 157 _, err = parser.ParseArgs(args) 158 return stdoutBuffer.Bytes(), stderrBuffer.Bytes(), err 159 }