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  }