github.com/wtsi-ssg/wrstat/v4@v4.5.1/server/filter.go (about) 1 /******************************************************************************* 2 * Copyright (c) 2023 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 "os/user" 30 "strconv" 31 "strings" 32 33 "github.com/gin-gonic/gin" 34 gas "github.com/wtsi-hgi/go-authserver" 35 "github.com/wtsi-ssg/wrstat/v4/dgut" 36 "github.com/wtsi-ssg/wrstat/v4/summary" 37 ) 38 39 // makeFilterFromContext extracts the user's filter requests, and returns a tree 40 // filter. 41 func makeFilterFromContext(c *gin.Context) (*dgut.Filter, error) { 42 groups, users, types := getFilterArgsFromContext(c) 43 44 filterGIDs, err := getWantedIDs(groups, groupNameToGID) 45 if err != nil { 46 return nil, err 47 } 48 49 return makeFilterGivenGIDs(filterGIDs, users, types) 50 } 51 52 func getFilterArgsFromContext(c *gin.Context) (groups string, users string, types string) { 53 groups = c.Query("groups") 54 users = c.Query("users") 55 types = c.Query("types") 56 57 return 58 } 59 60 // groupNameToGID converts group name to GID. 61 func groupNameToGID(name string) (string, error) { 62 g, err := user.LookupGroup(name) 63 if err != nil { 64 return "", err 65 } 66 67 return g.Gid, nil 68 } 69 70 // getWantedIDs splits the given comma separated names in to a slice and then 71 // passes each name to the given callback to convert it to an id, then returns 72 // a slice of the ids. Returns nil if names is blank. 73 func getWantedIDs(names string, cb func(name string) (string, error)) ([]uint32, error) { 74 splitNames := splitCommaSeparatedString(names) 75 76 ids := make([]uint32, len(splitNames)) 77 78 for i, name := range splitNames { 79 id, err := cb(name) 80 if err != nil { 81 return nil, err 82 } 83 84 ids[i] = idStringsToInts(id) 85 } 86 87 return ids, nil 88 } 89 90 // splitCommaSeparatedString splits the given comma separated string in to a 91 // slice of string. Returns nil if value is blank. 92 func splitCommaSeparatedString(value string) []string { 93 var parts []string 94 if value != "" { 95 parts = strings.Split(value, ",") 96 } 97 98 return parts 99 } 100 101 // idStringToInt converts a an id string to a uint32. 102 func idStringsToInts(idString string) uint32 { 103 // no error is possible here, with the number string coming from an OS 104 // lookup. 105 //nolint:errcheck 106 id, _ := strconv.ParseUint(idString, 10, 32) 107 108 return uint32(id) 109 } 110 111 func makeFilterGivenGIDs(filterGIDs []uint32, users, types string) (*dgut.Filter, error) { 112 filterUIDs, err := userIDsFromNames(users) 113 if err != nil { 114 return nil, err 115 } 116 117 return makeTreeFilter(filterGIDs, filterUIDs, types) 118 } 119 120 // userIDsFromNames returns the user IDs that correspond to the given comma 121 // separated list of user names. This does not check the usernames stored in the 122 // JWT, because users are allowed to know about files owned by other users in 123 // the groups they belong to; security restrictions are purely based on the 124 // enforced restrictedGroups(). 125 func userIDsFromNames(users string) ([]uint32, error) { 126 ids, err := getWantedIDs(users, gas.UserNameToUID) 127 if err != nil { 128 return nil, err 129 } 130 131 return ids, nil 132 } 133 134 // makeTreeFilter creates a filter from string args. 135 func makeTreeFilter(gids, uids []uint32, types string) (*dgut.Filter, error) { 136 filter := makeTreeGroupFilter(gids) 137 138 addUsersToFilter(filter, uids) 139 140 err := addTypesToFilter(filter, types) 141 142 return filter, err 143 } 144 145 // makeTreeGroupFilter creates a filter for groups. 146 func makeTreeGroupFilter(gids []uint32) *dgut.Filter { 147 if len(gids) == 0 { 148 return &dgut.Filter{} 149 } 150 151 return &dgut.Filter{GIDs: gids} 152 } 153 154 // addUsersToFilter adds a filter for users to the given filter. 155 func addUsersToFilter(filter *dgut.Filter, uids []uint32) { 156 if len(uids) == 0 { 157 return 158 } 159 160 filter.UIDs = uids 161 } 162 163 // addTypesToFilter adds a filter for types to the given filter. 164 func addTypesToFilter(filter *dgut.Filter, types string) error { 165 if types == "" { 166 return nil 167 } 168 169 tnames := splitCommaSeparatedString(types) 170 fts := make([]summary.DirGUTFileType, len(tnames)) 171 172 for i, name := range tnames { 173 ft, err := summary.FileTypeStringToDirGUTFileType(name) 174 if err != nil { 175 return err 176 } 177 178 fts[i] = ft 179 } 180 181 filter.FTs = fts 182 183 return nil 184 } 185 186 // allowedGIDs checks our JWT if present, and will return the GIDs that 187 // user is allowed to query. If the user is not restricted on GIDs, returns nil. 188 func (s *Server) allowedGIDs(c *gin.Context) (map[uint32]bool, error) { 189 var allowedIDs []string 190 191 var err error 192 193 if u := s.getUserFromContext(c); u != nil { 194 allowedIDs, err = s.userGIDs(u) 195 if err != nil { 196 return nil, err 197 } 198 } 199 200 if allowedIDs == nil { 201 return nil, nil //nolint:nilnil 202 } 203 204 allowed := make(map[uint32]bool, len(allowedIDs)) 205 206 for _, id := range allowedIDs { 207 converted, erra := strconv.Atoi(id) 208 if erra != nil { 209 return nil, erra 210 } 211 212 allowed[uint32(converted)] = true 213 } 214 215 return allowed, nil 216 } 217 218 // getUserFromContext extracts the User information from our JWT. Returns nil if 219 // we're not doing auth. 220 func (s *Server) getUserFromContext(c *gin.Context) *gas.User { 221 if s.AuthRouter() == nil { 222 return nil 223 } 224 225 return s.GetUser(c) 226 } 227 228 // makeRestrictedFilterFromContext extracts the user's filter requests, as 229 // restricted by their jwt, and returns a tree filter. 230 func (s *Server) makeRestrictedFilterFromContext(c *gin.Context) (*dgut.Filter, error) { 231 groups, users, types := getFilterArgsFromContext(c) 232 233 restrictedGIDs, err := s.getRestrictedGIDs(c, groups) 234 if err != nil { 235 return nil, err 236 } 237 238 return makeFilterGivenGIDs(restrictedGIDs, users, types) 239 } 240 241 func (s *Server) getRestrictedGIDs(c *gin.Context, groups string) ([]uint32, error) { 242 filterGIDs, err := getWantedIDs(groups, groupNameToGID) 243 if err != nil { 244 return nil, err 245 } 246 247 allowedGIDs, err := s.allowedGIDs(c) 248 if err != nil { 249 return nil, err 250 } 251 252 return restrictGIDs(allowedGIDs, filterGIDs) 253 } 254 255 // restrictGIDs returns the keys of allowedIDs that are in wantedIDs. Will 256 // return allowedIDs if wanted is empty; will return wantedIDs if allowedIDs is 257 // nil. Returns an error if you don't want any of the allowedIDs. 258 func restrictGIDs(allowedIDs map[uint32]bool, wantedIDs []uint32) ([]uint32, error) { 259 if allowedIDs == nil { 260 return wantedIDs, nil 261 } 262 263 ids := make([]uint32, 0, len(allowedIDs)) 264 265 for id := range allowedIDs { 266 ids = append(ids, id) 267 } 268 269 if len(wantedIDs) == 0 { 270 return ids, nil 271 } 272 273 var final []uint32 //nolint:prealloc 274 275 for _, id := range wantedIDs { 276 if !allowedIDs[id] { 277 continue 278 } 279 280 final = append(final, id) 281 } 282 283 if final == nil { 284 return nil, ErrBadQuery 285 } 286 287 return final, nil 288 }