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  }