github.com/wtsi-ssg/wrstat/v3@v3.2.3/server/where.go (about) 1 /******************************************************************************* 2 * Copyright (c) 2022 Genome Research Ltd. 3 * 4 * Author: Sendu Bala <sb10@sanger.ac.uk> 5 * 6 * Permission is hereby granted, free of charge, to any person obtaining 7 * a copy of this software and associated documentation files (the 8 * "Software"), to deal in the Software without restriction, including 9 * without limitation the rights to use, copy, modify, merge, publish, 10 * distribute, sublicense, and/or sell copies of the Software, and to 11 * permit persons to whom the Software is furnished to do so, subject to 12 * the following conditions: 13 * 14 * The above copyright notice and this permission notice shall be included 15 * in all copies or substantial portions of the Software. 16 * 17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 ******************************************************************************/ 25 26 package server 27 28 import ( 29 "net/http" 30 "os/user" 31 "strconv" 32 "strings" 33 34 "github.com/gin-gonic/gin" 35 gas "github.com/wtsi-hgi/go-authserver" 36 "github.com/wtsi-ssg/wrstat/v3/dgut" 37 "github.com/wtsi-ssg/wrstat/v3/summary" 38 ) 39 40 // getWhere responds with a list of directory stats describing where data is on 41 // disks. LoadDGUTDB() must already have been called. This is called when there 42 // is a GET on /rest/v1/where or /rest/v1/auth/where. 43 func (s *Server) getWhere(c *gin.Context) { 44 dir := c.DefaultQuery("dir", defaultDir) 45 splits := c.DefaultQuery("splits", defaultSplits) 46 47 filter, err := s.getFilter(c) 48 if err != nil { 49 c.AbortWithError(http.StatusBadRequest, err) //nolint:errcheck 50 51 return 52 } 53 54 s.treeMutex.Lock() 55 defer s.treeMutex.Unlock() 56 57 dcss, err := s.tree.Where(dir, filter, convertSplitsValue(splits)) 58 if err != nil { 59 c.AbortWithError(http.StatusBadRequest, err) //nolint:errcheck 60 61 return 62 } 63 64 c.IndentedJSON(http.StatusOK, s.dcssToSummaries(dcss)) 65 } 66 67 // getFilter extracts the user's filter requests, as restricted by their jwt, 68 // and returns a tree filter. 69 func (s *Server) getFilter(c *gin.Context) (*dgut.Filter, error) { 70 groups := c.Query("groups") 71 users := c.Query("users") 72 types := c.Query("types") 73 74 filterGIDs, err := s.restrictedGroups(c, groups) 75 if err != nil { 76 return nil, err 77 } 78 79 filterUIDs, err := s.userIDsFromNames(users) 80 if err != nil { 81 return nil, err 82 } 83 84 return makeTreeFilter(filterGIDs, filterUIDs, types) 85 } 86 87 // restrictedGroups checks our JWT if present, and will return the GIDs that 88 // user is allowed to query. If groups arg is not blank, but a comma separated 89 // list of group names, further limits the GIDs returned to be amongst those. If 90 // the user is not restricted on GIDs, returns all the given group names as 91 // GIDs. 92 func (s *Server) restrictedGroups(c *gin.Context, groups string) ([]string, error) { 93 ids, wanted, err := getWantedIDs(groups, groupNameToGID) 94 if err != nil { 95 return nil, err 96 } 97 98 var allowedIDs []string 99 100 if u := s.getUserFromContext(c); u != nil { 101 allowedIDs, err = s.userGIDs(u) 102 if err != nil { 103 return nil, err 104 } 105 } 106 107 if allowedIDs == nil { 108 return ids, nil 109 } 110 111 return restrictIDsToWanted(allowedIDs, wanted) 112 } 113 114 // groupNameToGID converts group name to GID. 115 func groupNameToGID(name string) (string, error) { 116 g, err := user.LookupGroup(name) 117 if err != nil { 118 return "", err 119 } 120 121 return g.Gid, nil 122 } 123 124 // getWantedIDs splits the given comma separated names in to a slice and then 125 // passes each name to the given callback to convert it to an id, then returns 126 // a slice of the ids, along with a map where the slice elements are the keys. 127 // Both will be nil if names is blank. 128 func getWantedIDs(names string, cb func(name string) (string, error)) ([]string, map[string]bool, error) { 129 splitNames := splitCommaSeparatedString(names) 130 131 ids := make([]string, len(splitNames)) 132 wanted := make(map[string]bool, len(splitNames)) 133 134 for i, name := range splitNames { 135 id, err := cb(name) 136 if err != nil { 137 return nil, nil, err 138 } 139 140 ids[i] = id 141 wanted[id] = true 142 } 143 144 return ids, wanted, nil 145 } 146 147 // splitCommaSeparatedString splits the given comma separated string in to a 148 // slice of string. Returns nil if value is blank. 149 func splitCommaSeparatedString(value string) []string { 150 var parts []string 151 if value != "" { 152 parts = strings.Split(value, ",") 153 } 154 155 return parts 156 } 157 158 // getUserFromContext extracts the User information from our JWT. Returns nil if 159 // we're not doing auth. 160 func (s *Server) getUserFromContext(c *gin.Context) *gas.User { 161 if s.AuthRouter() == nil { 162 return nil 163 } 164 165 return s.GetUser(c) 166 } 167 168 // restrictIDsToWanted returns the elements of ids that are in wanted. Will 169 // return ids if wanted is empty. Returns an error if you don't want any of the 170 // given ids. 171 func restrictIDsToWanted(ids []string, wanted map[string]bool) ([]string, error) { 172 if len(wanted) == 0 { 173 return ids, nil 174 } 175 176 var final []string //nolint:prealloc 177 178 for _, id := range ids { 179 if !wanted[id] { 180 continue 181 } 182 183 final = append(final, id) 184 } 185 186 if final == nil { 187 return nil, ErrBadQuery 188 } 189 190 return final, nil 191 } 192 193 // userIDsFromNames returns the user IDs that correspond to the given comma 194 // separated list of user names. This does not check the usernames stored in the 195 // JWT, because users are allowed to know about files owned by other users in 196 // the groups they belong to; security restrictions are purely based on the 197 // enforced restrictedGroups(). 198 func (s *Server) userIDsFromNames(users string) ([]string, error) { 199 ids, _, err := getWantedIDs(users, gas.UserNameToUID) 200 if err != nil { 201 return nil, err 202 } 203 204 return ids, nil 205 } 206 207 // makeTreeFilter creates a filter from string args. 208 func makeTreeFilter(gids, uids []string, types string) (*dgut.Filter, error) { 209 filter := makeTreeGroupFilter(gids) 210 211 addUsersToFilter(filter, uids) 212 213 err := addTypesToFilter(filter, types) 214 215 return filter, err 216 } 217 218 // makeTreeGroupFilter creates a filter for groups. 219 func makeTreeGroupFilter(gids []string) *dgut.Filter { 220 if len(gids) == 0 { 221 return &dgut.Filter{} 222 } 223 224 return &dgut.Filter{GIDs: idStringsToInts(gids)} 225 } 226 227 // idStringsToInts converts a slice of id strings into uint32s. 228 func idStringsToInts(idStrings []string) []uint32 { 229 ids := make([]uint32, len(idStrings)) 230 231 for i, idStr := range idStrings { 232 // no error is possible here, with the number string coming from an OS 233 // lookup. 234 //nolint:errcheck 235 id, _ := strconv.ParseUint(idStr, 10, 32) 236 237 ids[i] = uint32(id) 238 } 239 240 return ids 241 } 242 243 // addUsersToFilter adds a filter for users to the given filter. 244 func addUsersToFilter(filter *dgut.Filter, uids []string) { 245 if len(uids) == 0 { 246 return 247 } 248 249 filter.UIDs = idStringsToInts(uids) 250 } 251 252 // addTypesToFilter adds a filter for types to the given filter. 253 func addTypesToFilter(filter *dgut.Filter, types string) error { 254 if types == "" { 255 return nil 256 } 257 258 tnames := splitCommaSeparatedString(types) 259 fts := make([]summary.DirGUTFileType, len(tnames)) 260 261 for i, name := range tnames { 262 ft, err := summary.FileTypeStringToDirGUTFileType(name) 263 if err != nil { 264 return err 265 } 266 267 fts[i] = ft 268 } 269 270 filter.FTs = fts 271 272 return nil 273 } 274 275 // convertSplitsValue converts the given number string in to an int. On failure, 276 // returns our default value for splits of 2. 277 func convertSplitsValue(splits string) int { 278 splitsN, err := strconv.ParseUint(splits, 10, 8) 279 if err != nil { 280 return convertSplitsValue(defaultSplits) 281 } 282 283 return int(splitsN) 284 }