github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/cmd/snap/cmd_routine_file_access.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2020 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 main
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"github.com/jessevdk/go-flags"
    29  
    30  	"github.com/snapcore/snapd/client"
    31  	"github.com/snapcore/snapd/i18n"
    32  )
    33  
    34  type cmdRoutineFileAccess struct {
    35  	clientMixin
    36  	FileAccessOptions struct {
    37  		Snap installedSnapName
    38  		Path flags.Filename
    39  	} `positional-args:"true" required:"true"`
    40  }
    41  
    42  var shortRoutineFileAccessHelp = i18n.G("Return information about file access by a snap")
    43  var longRoutineFileAccessHelp = i18n.G(`
    44  The file-access command returns information about a snap's file system access.
    45  
    46  This command is used by the xdg-document-portal service to identify
    47  files that do not need to be proxied to provide access within
    48  confinement.
    49  
    50  File paths are interpreted as host file system paths.  The tool may
    51  return false negatives (e.g. report that a file path is unreadable,
    52  despite being readable under a different path).  It also does not
    53  check if file system permissions would render a file unreadable.
    54  `)
    55  
    56  func init() {
    57  	addRoutineCommand("file-access", shortRoutineFileAccessHelp, longRoutineFileAccessHelp, func() flags.Commander {
    58  		return &cmdRoutineFileAccess{}
    59  	}, nil, []argDesc{
    60  		{
    61  			// TRANSLATORS: This needs to begin with < and end with >
    62  			name: i18n.G("<snap>"),
    63  			// TRANSLATORS: This should not start with a lowercase letter.
    64  			desc: i18n.G("Snap name"),
    65  		},
    66  		{
    67  			// TRANSLATORS: This needs to begin with < and end with >
    68  			name: i18n.G("<path>"),
    69  			// TRANSLATORS: This should not start with a lowercase letter.
    70  			desc: i18n.G("File path"),
    71  		},
    72  	})
    73  }
    74  
    75  func (x *cmdRoutineFileAccess) Execute(args []string) error {
    76  	if len(args) > 0 {
    77  		return ErrExtraArgs
    78  	}
    79  
    80  	snapName := string(x.FileAccessOptions.Snap)
    81  	path := string(x.FileAccessOptions.Path)
    82  
    83  	snap, _, err := x.client.Snap(snapName)
    84  	if err != nil {
    85  		return fmt.Errorf("cannot retrieve info for snap %q: %v", snapName, err)
    86  	}
    87  
    88  	// Check whether the snap has home or removable-media plugs connected
    89  	connections, err := x.client.Connections(&client.ConnectionOptions{
    90  		Snap: snap.Name,
    91  	})
    92  	if err != nil {
    93  		return fmt.Errorf("cannot get connections for snap %q: %v", snap.Name, err)
    94  	}
    95  	var hasHome, hasRemovableMedia bool
    96  	for _, conn := range connections.Established {
    97  		if conn.Plug.Snap != snap.Name {
    98  			continue
    99  		}
   100  		switch conn.Interface {
   101  		case "home":
   102  			hasHome = true
   103  		case "removable-media":
   104  			hasRemovableMedia = true
   105  		}
   106  	}
   107  
   108  	access, err := x.checkAccess(snap, hasHome, hasRemovableMedia, path)
   109  	if err != nil {
   110  		return err
   111  	}
   112  	fmt.Fprintln(Stdout, access)
   113  	return nil
   114  }
   115  
   116  type FileAccess string
   117  
   118  const (
   119  	FileAccessHidden    FileAccess = "hidden"
   120  	FileAccessReadOnly  FileAccess = "read-only"
   121  	FileAccessReadWrite FileAccess = "read-write"
   122  )
   123  
   124  func splitPathAbs(path string) ([]string, error) {
   125  	// Abs also cleans the path, removing any ".." components
   126  	path, err := filepath.Abs(path)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	// Ignore the empty component before the first slash
   131  	return strings.Split(path, string(os.PathSeparator))[1:], nil
   132  }
   133  
   134  func pathHasPrefix(path, prefix []string) bool {
   135  	if len(path) < len(prefix) {
   136  		return false
   137  	}
   138  	for i := range prefix {
   139  		if path[i] != prefix[i] {
   140  			return false
   141  		}
   142  	}
   143  	return true
   144  }
   145  
   146  func (x *cmdRoutineFileAccess) checkAccess(snap *client.Snap, hasHome, hasRemovableMedia bool, path string) (FileAccess, error) {
   147  	// Classic confinement snaps run in the host system namespace,
   148  	// so can see everything.
   149  	if snap.Confinement == client.ClassicConfinement {
   150  		return FileAccessReadWrite, nil
   151  	}
   152  
   153  	pathParts, err := splitPathAbs(path)
   154  	if err != nil {
   155  		return "", err
   156  	}
   157  
   158  	// Snaps have access to $SNAP_DATA and $SNAP_COMMON
   159  	if pathHasPrefix(pathParts, []string{"var", "snap", snap.Name}) {
   160  		if len(pathParts) == 3 {
   161  			return FileAccessReadOnly, nil
   162  		}
   163  		switch pathParts[3] {
   164  		case "common", "current", snap.Revision.String():
   165  			return FileAccessReadWrite, nil
   166  		default:
   167  			return FileAccessReadOnly, nil
   168  		}
   169  	}
   170  
   171  	// Snaps with removable-media plugged can access removable
   172  	// media mount points.
   173  	if hasRemovableMedia {
   174  		if pathHasPrefix(pathParts, []string{"mnt"}) || pathHasPrefix(pathParts, []string{"media"}) || pathHasPrefix(pathParts, []string{"run", "media"}) {
   175  			return FileAccessReadWrite, nil
   176  		}
   177  	}
   178  
   179  	usr, err := userCurrent()
   180  	if err != nil {
   181  		return "", fmt.Errorf("cannot get the current user: %v", err)
   182  	}
   183  
   184  	home, err := splitPathAbs(usr.HomeDir)
   185  	if err != nil {
   186  		return "", err
   187  	}
   188  	if pathHasPrefix(pathParts, home) {
   189  		pathInHome := pathParts[len(home):]
   190  		// Snaps have access to $SNAP_USER_DATA and $SNAP_USER_COMMON
   191  		if pathHasPrefix(pathInHome, []string{"snap"}) {
   192  			if !pathHasPrefix(pathInHome, []string{"snap", snap.Name}) {
   193  				return FileAccessHidden, nil
   194  			}
   195  			if len(pathInHome) < 3 {
   196  				return FileAccessReadOnly, nil
   197  			}
   198  			switch pathInHome[2] {
   199  			case "common", "current", snap.Revision.String():
   200  				return FileAccessReadWrite, nil
   201  			default:
   202  				return FileAccessReadOnly, nil
   203  			}
   204  		}
   205  		// If the home interface is connected, the snap has
   206  		// access to other files in home, except top-level dot
   207  		// files.
   208  		if hasHome {
   209  			if len(pathInHome) == 0 || !strings.HasPrefix(pathInHome[0], ".") {
   210  				return FileAccessReadWrite, nil
   211  			}
   212  		}
   213  	}
   214  
   215  	return FileAccessHidden, nil
   216  }