github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/overlord/hookstate/ctlcmd/ctlcmd.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 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/snapcore/snapd/logger"
    29  	"github.com/snapcore/snapd/overlord/hookstate"
    30  
    31  	"github.com/jessevdk/go-flags"
    32  )
    33  
    34  type baseCommand struct {
    35  	stdout io.Writer
    36  	stderr io.Writer
    37  	c      *hookstate.Context
    38  }
    39  
    40  func (c *baseCommand) setStdout(w io.Writer) {
    41  	c.stdout = w
    42  }
    43  
    44  func (c *baseCommand) printf(format string, a ...interface{}) {
    45  	if c.stdout != nil {
    46  		fmt.Fprintf(c.stdout, format, a...)
    47  	}
    48  }
    49  
    50  func (c *baseCommand) setStderr(w io.Writer) {
    51  	c.stderr = w
    52  }
    53  
    54  func (c *baseCommand) errorf(format string, a ...interface{}) {
    55  	if c.stderr != nil {
    56  		fmt.Fprintf(c.stderr, format, a...)
    57  	}
    58  }
    59  
    60  func (c *baseCommand) setContext(context *hookstate.Context) {
    61  	c.c = context
    62  }
    63  
    64  func (c *baseCommand) context() *hookstate.Context {
    65  	return c.c
    66  }
    67  
    68  type command interface {
    69  	setStdout(w io.Writer)
    70  	setStderr(w io.Writer)
    71  
    72  	setContext(context *hookstate.Context)
    73  	context() *hookstate.Context
    74  
    75  	Execute(args []string) error
    76  }
    77  
    78  type commandInfo struct {
    79  	shortHelp string
    80  	longHelp  string
    81  	generator func() command
    82  	hidden    bool
    83  }
    84  
    85  var commands = make(map[string]*commandInfo)
    86  
    87  func addCommand(name, shortHelp, longHelp string, generator func() command) *commandInfo {
    88  	cmd := &commandInfo{
    89  		shortHelp: shortHelp,
    90  		longHelp:  longHelp,
    91  		generator: generator,
    92  	}
    93  	commands[name] = cmd
    94  	return cmd
    95  }
    96  
    97  // UnsuccessfulError carries a specific exit code to be returned to the client.
    98  type UnsuccessfulError struct {
    99  	ExitCode int
   100  }
   101  
   102  func (e UnsuccessfulError) Error() string {
   103  	return fmt.Sprintf("unsuccessful with exit code: %d", e.ExitCode)
   104  }
   105  
   106  // ForbiddenCommandError conveys that a command cannot be invoked in some context
   107  type ForbiddenCommandError struct {
   108  	Message string
   109  }
   110  
   111  func (f ForbiddenCommandError) Error() string {
   112  	return f.Message
   113  }
   114  
   115  // ForbiddenCommand contains information about an attempt to use a command in a context where it is not allowed.
   116  type ForbiddenCommand struct {
   117  	Uid  uint32
   118  	Name string
   119  }
   120  
   121  func (f *ForbiddenCommand) Execute(args []string) error {
   122  	return &ForbiddenCommandError{Message: fmt.Sprintf("cannot use %q with uid %d, try with sudo", f.Name, f.Uid)}
   123  }
   124  
   125  // Run runs the requested command.
   126  func Run(context *hookstate.Context, args []string, uid uint32) (stdout, stderr []byte, err error) {
   127  	parser := flags.NewNamedParser("snapctl", flags.PassDoubleDash|flags.HelpFlag)
   128  
   129  	// Create stdout/stderr buffers, and make sure commands use them.
   130  	var stdoutBuffer bytes.Buffer
   131  	var stderrBuffer bytes.Buffer
   132  	for name, cmdInfo := range commands {
   133  		var data interface{}
   134  		// commands listed here will be allowed for regular users
   135  		// note: commands still need valid context and snaps can only access own config.
   136  		if uid == 0 || name == "get" || name == "services" || name == "set-health" || name == "is-connected" {
   137  			cmd := cmdInfo.generator()
   138  			cmd.setStdout(&stdoutBuffer)
   139  			cmd.setStderr(&stderrBuffer)
   140  			cmd.setContext(context)
   141  			data = cmd
   142  		} else {
   143  			data = &ForbiddenCommand{Uid: uid, Name: name}
   144  		}
   145  		theCmd, err := parser.AddCommand(name, cmdInfo.shortHelp, cmdInfo.longHelp, data)
   146  		theCmd.Hidden = cmdInfo.hidden
   147  		if err != nil {
   148  			logger.Panicf("cannot add command %q: %s", name, err)
   149  		}
   150  	}
   151  
   152  	_, err = parser.ParseArgs(args)
   153  	return stdoutBuffer.Bytes(), stderrBuffer.Bytes(), err
   154  }