mirror of https://github.com/knative/client.git
296 lines
8.0 KiB
Go
296 lines
8.0 KiB
Go
// Copyright © 2019 The Knative Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// +build !windows
|
|
|
|
// This file doesn't compile for Windows platform, it defines respective build tag (build tag),
|
|
// the respective functionality is present in plugin_verifier_windows.go file in same dir,
|
|
// which only compiles for Windows platform.
|
|
|
|
package plugin
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// pluginVerifier verifies that existing kn commands are not overridden
|
|
type pluginVerifier struct {
|
|
root *cobra.Command
|
|
seenPlugins map[string]string
|
|
}
|
|
|
|
// collect errors and warnings on the way
|
|
type errorsAndWarnings struct {
|
|
errors []string
|
|
warnings []string
|
|
}
|
|
|
|
// Create new verifier
|
|
func newPluginVerifier(root *cobra.Command) *pluginVerifier {
|
|
return &pluginVerifier{
|
|
root: root,
|
|
seenPlugins: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// permission bits for execute
|
|
const (
|
|
UserExecute = 1 << 6
|
|
GroupExecute = 1 << 3
|
|
OtherExecute = 1 << 0
|
|
)
|
|
|
|
// Verify implements pathVerifier and determines if a given path
|
|
// is valid depending on whether or not it overwrites an existing
|
|
// kn command path, or a previously seen plugin.
|
|
// This method is not idempotent and must be called for each path only once.
|
|
func (v *pluginVerifier) verify(eaw errorsAndWarnings, path string) errorsAndWarnings {
|
|
if v.root == nil {
|
|
return eaw.addError("unable to verify path with nil root")
|
|
}
|
|
|
|
// Verify that plugin actually exists
|
|
fileInfo, err := os.Stat(path)
|
|
if err != nil {
|
|
if err == os.ErrNotExist {
|
|
return eaw.addError("cannot find plugin in %s", path)
|
|
}
|
|
return eaw.addError("cannot stat %s: %v", path, err)
|
|
}
|
|
|
|
eaw = v.addErrorIfWrongPrefix(eaw, path)
|
|
eaw = v.addWarningIfNotExecutable(eaw, path, fileInfo)
|
|
eaw = v.addWarningIfAlreadySeen(eaw, path)
|
|
eaw = v.addErrorIfOverwritingExistingCommand(eaw, path)
|
|
|
|
// Remember each verified plugin for duplicate check
|
|
v.seenPlugins[filepath.Base(path)] = path
|
|
|
|
return eaw
|
|
}
|
|
|
|
func (v *pluginVerifier) addWarningIfAlreadySeen(eaw errorsAndWarnings, path string) errorsAndWarnings {
|
|
fileName := filepath.Base(path)
|
|
if existingPath, ok := v.seenPlugins[fileName]; ok {
|
|
return eaw.addWarning("%s is ignored because it is shadowed by an equally named plugin: %s.", path, existingPath)
|
|
}
|
|
return eaw
|
|
}
|
|
|
|
func (v *pluginVerifier) addErrorIfOverwritingExistingCommand(eaw errorsAndWarnings, path string) errorsAndWarnings {
|
|
fileName := filepath.Base(path)
|
|
cmds := strings.Split(fileName, "-")
|
|
if len(cmds) < 2 {
|
|
return eaw.addError("%s is not a valid plugin filename as its missing a prefix", fileName)
|
|
}
|
|
cmds = cmds[1:]
|
|
|
|
// Check both, commands with underscore and with dash because plugins can be called with both
|
|
overwrittenCommands := make(map[string]bool)
|
|
for _, c := range [][]string{cmds, convertUnderscoresToDashes(cmds)} {
|
|
cmd, _, err := v.root.Find(c)
|
|
if err == nil {
|
|
if !InAllowedExtensibleCommandGroups(cmd.Name()) {
|
|
overwrittenCommands[cmd.CommandPath()] = true
|
|
}
|
|
}
|
|
}
|
|
for command := range overwrittenCommands {
|
|
eaw.addError("%s overwrites existing built-in command: %s", fileName, command)
|
|
}
|
|
return eaw
|
|
}
|
|
|
|
func (v *pluginVerifier) addErrorIfWrongPrefix(eaw errorsAndWarnings, path string) errorsAndWarnings {
|
|
fileName := filepath.Base(path)
|
|
// Only pick the first prefix as it is very like that it will be reduced to
|
|
// a single prefix anyway (PR pending)
|
|
prefix := ValidPluginFilenamePrefixes[0]
|
|
if !strings.HasPrefix(fileName, prefix+"-") {
|
|
eaw.addWarning("%s plugin doesn't start with plugin prefix %s", fileName, prefix)
|
|
}
|
|
return eaw
|
|
}
|
|
|
|
func (v *pluginVerifier) addWarningIfNotExecutable(eaw errorsAndWarnings, path string, fileInfo os.FileInfo) errorsAndWarnings {
|
|
if runtime.GOOS == "windows" {
|
|
return checkForWindowsExecutable(eaw, fileInfo, path)
|
|
}
|
|
|
|
mode := fileInfo.Mode()
|
|
if !mode.IsRegular() && !isSymlink(mode) {
|
|
return eaw.addWarning("%s is not a file", path)
|
|
}
|
|
perms := uint32(mode.Perm())
|
|
|
|
var sys *syscall.Stat_t
|
|
var ok bool
|
|
if sys, ok = fileInfo.Sys().(*syscall.Stat_t); !ok {
|
|
// We can check the files' owner/group
|
|
return eaw.addWarning("cannot check owner/group of file %s", path)
|
|
}
|
|
|
|
isOwner := checkIfUserIsFileOwner(sys.Uid)
|
|
isInGroup, err := checkIfUserInGroup(sys.Gid)
|
|
if err != nil {
|
|
return eaw.addError("cannot get group ids for checking executable status of file %s", path)
|
|
}
|
|
|
|
// User is owner and owner can execute
|
|
if canOwnerExecute(perms, isOwner) {
|
|
return eaw
|
|
}
|
|
|
|
// User is in group which can execute, but user is not file owner
|
|
if canGroupExecute(perms, isOwner, isInGroup) {
|
|
return eaw
|
|
}
|
|
|
|
// All can execute, and the user is not file owner and not in the file's perm group
|
|
if canOtherExecute(perms, isOwner, isInGroup) {
|
|
return eaw
|
|
}
|
|
|
|
return eaw.addWarning("%s is not executable by current user", path)
|
|
}
|
|
|
|
func checkForWindowsExecutable(eaw errorsAndWarnings, fileInfo os.FileInfo, path string) errorsAndWarnings {
|
|
fileExt := strings.ToLower(filepath.Ext(fileInfo.Name()))
|
|
|
|
switch fileExt {
|
|
case ".bat", ".cmd", ".com", ".exe", ".ps1":
|
|
return eaw
|
|
}
|
|
return eaw.addWarning("%s is not executable as it does not have the proper extension", path)
|
|
}
|
|
|
|
func checkIfUserInGroup(gid uint32) (bool, error) {
|
|
groups, err := os.Getgroups()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, g := range groups {
|
|
if int(gid) == g {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func checkIfUserIsFileOwner(uid uint32) bool {
|
|
if int(uid) == os.Getuid() {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Check if all can execute, and the user is not file owner and not in the file's perm group
|
|
func canOtherExecute(perms uint32, isOwner bool, isInGroup bool) bool {
|
|
if perms&OtherExecute != 0 {
|
|
if os.Getuid() == 0 {
|
|
return true
|
|
}
|
|
if !isOwner && !isInGroup {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Check if user is owner and owner can execute
|
|
func canOwnerExecute(perms uint32, isOwner bool) bool {
|
|
if perms&UserExecute != 0 {
|
|
if os.Getuid() == 0 {
|
|
return true
|
|
}
|
|
if isOwner {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Check if user is in group which can execute, but user is not file owner
|
|
func canGroupExecute(perms uint32, isOwner bool, isInGroup bool) bool {
|
|
if perms&GroupExecute != 0 {
|
|
if os.Getuid() == 0 {
|
|
return true
|
|
}
|
|
if !isOwner && isInGroup {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (eaw *errorsAndWarnings) addError(format string, args ...interface{}) errorsAndWarnings {
|
|
eaw.errors = append(eaw.errors, fmt.Sprintf(format, args...))
|
|
return *eaw
|
|
}
|
|
|
|
func (eaw *errorsAndWarnings) addWarning(format string, args ...interface{}) errorsAndWarnings {
|
|
eaw.warnings = append(eaw.warnings, fmt.Sprintf(format, args...))
|
|
return *eaw
|
|
}
|
|
|
|
func (eaw *errorsAndWarnings) printWarningsAndErrors(out io.Writer) {
|
|
printSection(out, "ERROR", eaw.errors)
|
|
printSection(out, "WARNING", eaw.warnings)
|
|
}
|
|
|
|
func (eaw *errorsAndWarnings) combinedError() error {
|
|
if len(eaw.errors) == 0 {
|
|
return nil
|
|
}
|
|
return errors.New(strings.Join(eaw.errors, ","))
|
|
}
|
|
|
|
func printSection(out io.Writer, label string, values []string) {
|
|
if len(values) > 0 {
|
|
printLabelWithConditionalPluralS(out, label, len(values))
|
|
for _, value := range values {
|
|
fmt.Fprintf(out, " - %s\n", value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func printLabelWithConditionalPluralS(out io.Writer, label string, nr int) {
|
|
if nr == 1 {
|
|
fmt.Fprintf(out, "%s:\n", label)
|
|
} else {
|
|
fmt.Fprintf(out, "%ss:\n", label)
|
|
}
|
|
}
|
|
|
|
func convertUnderscoresToDashes(cmds []string) []string {
|
|
ret := make([]string, len(cmds))
|
|
for i := range cmds {
|
|
ret[i] = strings.ReplaceAll(cmds[i], "_", "-")
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func isSymlink(mode os.FileMode) bool {
|
|
return mode&os.ModeSymlink != 0
|
|
}
|