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 }