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  }