github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/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  // nonRootAllowed lists the commands that can be performed even when snapctl
   117  // is invoked not by root.
   118  var nonRootAllowed = []string{"get", "services", "set-health", "is-connected", "system-mode"}
   119  
   120  // Run runs the requested command.
   121  func Run(context *hookstate.Context, args []string, uid uint32) (stdout, stderr []byte, err error) {
   122  	if len(args) == 0 {
   123  		return nil, nil, fmt.Errorf("internal error: snapctl cannot run without args")
   124  	}
   125  
   126  	if !isAllowedToRun(uid, args) {
   127  		return nil, nil, &ForbiddenCommandError{Message: fmt.Sprintf("cannot use %q with uid %d, try with sudo", args[0], uid)}
   128  	}
   129  
   130  	parser := flags.NewNamedParser("snapctl", flags.PassDoubleDash|flags.HelpFlag)
   131  
   132  	// Create stdout/stderr buffers, and make sure commands use them.
   133  	var stdoutBuffer bytes.Buffer
   134  	var stderrBuffer bytes.Buffer
   135  	for name, cmdInfo := range commands {
   136  		cmd := cmdInfo.generator()
   137  		cmd.setStdout(&stdoutBuffer)
   138  		cmd.setStderr(&stderrBuffer)
   139  		cmd.setContext(context)
   140  
   141  		theCmd, err := parser.AddCommand(name, cmdInfo.shortHelp, cmdInfo.longHelp, cmd)
   142  		theCmd.Hidden = cmdInfo.hidden
   143  		if err != nil {
   144  			logger.Panicf("cannot add command %q: %s", name, err)
   145  		}
   146  	}
   147  
   148  	_, err = parser.ParseArgs(args)
   149  	return stdoutBuffer.Bytes(), stderrBuffer.Bytes(), err
   150  }
   151  
   152  func isAllowedToRun(uid uint32, args []string) bool {
   153  	// A command can run if any of the following are true:
   154  	//	* It runs as root
   155  	//	* It's contained in nonRootAllowed
   156  	//	* It's used with the -h or --help flags
   157  	// note: commands still need valid context and snaps can only access own config.
   158  	return uid == 0 ||
   159  		strutil.ListContains(nonRootAllowed, args[0]) ||
   160  		strutil.ListContains(args, "-h") ||
   161  		strutil.ListContains(args, "--help")
   162  }