github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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  // ForbiddenCommandError conveys that a command cannot be invoked in some context
    98  type ForbiddenCommandError struct {
    99  	Message string
   100  }
   101  
   102  func (f ForbiddenCommandError) Error() string {
   103  	return f.Message
   104  }
   105  
   106  // ForbiddenCommand contains information about an attempt to use a command in a context where it is not allowed.
   107  type ForbiddenCommand struct {
   108  	Uid  uint32
   109  	Name string
   110  }
   111  
   112  func (f *ForbiddenCommand) Execute(args []string) error {
   113  	return &ForbiddenCommandError{Message: fmt.Sprintf("cannot use %q with uid %d, try with sudo", f.Name, f.Uid)}
   114  }
   115  
   116  // Run runs the requested command.
   117  func Run(context *hookstate.Context, args []string, uid uint32) (stdout, stderr []byte, err error) {
   118  	parser := flags.NewParser(nil, flags.PassDoubleDash|flags.HelpFlag)
   119  
   120  	// Create stdout/stderr buffers, and make sure commands use them.
   121  	var stdoutBuffer bytes.Buffer
   122  	var stderrBuffer bytes.Buffer
   123  	for name, cmdInfo := range commands {
   124  		var data interface{}
   125  		// commands listed here will be allowed for regular users
   126  		// note: commands still need valid context and snaps can only access own config.
   127  		if uid == 0 || name == "get" || name == "services" || name == "set-health" {
   128  			cmd := cmdInfo.generator()
   129  			cmd.setStdout(&stdoutBuffer)
   130  			cmd.setStderr(&stderrBuffer)
   131  			cmd.setContext(context)
   132  			data = cmd
   133  		} else {
   134  			data = &ForbiddenCommand{Uid: uid, Name: name}
   135  		}
   136  		theCmd, err := parser.AddCommand(name, cmdInfo.shortHelp, cmdInfo.longHelp, data)
   137  		theCmd.Hidden = cmdInfo.hidden
   138  		if err != nil {
   139  			logger.Panicf("cannot add command %q: %s", name, err)
   140  		}
   141  	}
   142  
   143  	_, err = parser.ParseArgs(args)
   144  	return stdoutBuffer.Bytes(), stderrBuffer.Bytes(), err
   145  }