gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/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 MissingContextError struct { 36 subcommand string 37 } 38 39 func (e *MissingContextError) Error() string { 40 return fmt.Sprintf(`cannot invoke snapctl operation commands (here %q) from outside of a snap`, e.subcommand) 41 } 42 43 type baseCommand struct { 44 stdout io.Writer 45 stderr io.Writer 46 c *hookstate.Context 47 name string 48 } 49 50 func (c *baseCommand) setName(name string) { 51 c.name = name 52 } 53 54 func (c *baseCommand) setStdout(w io.Writer) { 55 c.stdout = w 56 } 57 58 func (c *baseCommand) printf(format string, a ...interface{}) { 59 if c.stdout != nil { 60 fmt.Fprintf(c.stdout, format, a...) 61 } 62 } 63 64 func (c *baseCommand) setStderr(w io.Writer) { 65 c.stderr = w 66 } 67 68 func (c *baseCommand) errorf(format string, a ...interface{}) { 69 if c.stderr != nil { 70 fmt.Fprintf(c.stderr, format, a...) 71 } 72 } 73 74 func (c *baseCommand) setContext(context *hookstate.Context) { 75 c.c = context 76 } 77 78 func (c *baseCommand) context() *hookstate.Context { 79 return c.c 80 } 81 82 func (c *baseCommand) ensureContext() (context *hookstate.Context, err error) { 83 if c.c == nil { 84 err = &MissingContextError{c.name} 85 } 86 return c.c, err 87 } 88 89 type command interface { 90 setName(name string) 91 92 setStdout(w io.Writer) 93 setStderr(w io.Writer) 94 95 setContext(context *hookstate.Context) 96 context() *hookstate.Context 97 98 Execute(args []string) error 99 } 100 101 type commandInfo struct { 102 shortHelp string 103 longHelp string 104 generator func() command 105 hidden bool 106 } 107 108 var commands = make(map[string]*commandInfo) 109 110 func addCommand(name, shortHelp, longHelp string, generator func() command) *commandInfo { 111 cmd := &commandInfo{ 112 shortHelp: shortHelp, 113 longHelp: longHelp, 114 generator: generator, 115 } 116 commands[name] = cmd 117 return cmd 118 } 119 120 // UnsuccessfulError carries a specific exit code to be returned to the client. 121 type UnsuccessfulError struct { 122 ExitCode int 123 } 124 125 func (e UnsuccessfulError) Error() string { 126 return fmt.Sprintf("unsuccessful with exit code: %d", e.ExitCode) 127 } 128 129 // ForbiddenCommandError conveys that a command cannot be invoked in some context 130 type ForbiddenCommandError struct { 131 Message string 132 } 133 134 func (f ForbiddenCommandError) Error() string { 135 return f.Message 136 } 137 138 // nonRootAllowed lists the commands that can be performed even when snapctl 139 // is invoked not by root. 140 var nonRootAllowed = []string{"get", "services", "set-health", "is-connected", "system-mode"} 141 142 // Run runs the requested command. 143 func Run(context *hookstate.Context, args []string, uid uint32) (stdout, stderr []byte, err error) { 144 if len(args) == 0 { 145 return nil, nil, fmt.Errorf("internal error: snapctl cannot run without args") 146 } 147 148 if !isAllowedToRun(uid, args) { 149 return nil, nil, &ForbiddenCommandError{Message: fmt.Sprintf("cannot use %q with uid %d, try with sudo", args[0], uid)} 150 } 151 152 parser := flags.NewNamedParser("snapctl", flags.PassDoubleDash|flags.HelpFlag) 153 154 // Create stdout/stderr buffers, and make sure commands use them. 155 var stdoutBuffer bytes.Buffer 156 var stderrBuffer bytes.Buffer 157 for name, cmdInfo := range commands { 158 cmd := cmdInfo.generator() 159 cmd.setName(name) 160 cmd.setStdout(&stdoutBuffer) 161 cmd.setStderr(&stderrBuffer) 162 cmd.setContext(context) 163 164 theCmd, err := parser.AddCommand(name, cmdInfo.shortHelp, cmdInfo.longHelp, cmd) 165 theCmd.Hidden = cmdInfo.hidden 166 if err != nil { 167 logger.Panicf("cannot add command %q: %s", name, err) 168 } 169 } 170 171 _, err = parser.ParseArgs(args) 172 return stdoutBuffer.Bytes(), stderrBuffer.Bytes(), err 173 } 174 175 func isAllowedToRun(uid uint32, args []string) bool { 176 // A command can run if any of the following are true: 177 // * It runs as root 178 // * It's contained in nonRootAllowed 179 // * It's used with the -h or --help flags 180 // note: commands still need valid context and snaps can only access own config. 181 return uid == 0 || 182 strutil.ListContains(nonRootAllowed, args[0]) || 183 strutil.ListContains(args, "-h") || 184 strutil.ListContains(args, "--help") 185 }