github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocs/handlers/cloud/users/users.go (about)

     1  // Copyright 2018-2021 CERN
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  package users
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"net/http"
    25  	"net/url"
    26  	"strconv"
    27  
    28  	cs3gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
    29  	cs3identity "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    30  	cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    31  	cs3storage "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    32  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/config"
    33  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/response"
    34  	"github.com/cs3org/reva/v2/pkg/appctx"
    35  	"github.com/cs3org/reva/v2/pkg/conversions"
    36  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    37  	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
    38  	"github.com/cs3org/reva/v2/pkg/utils"
    39  	"github.com/go-chi/chi/v5"
    40  )
    41  
    42  // Handler renders user data for the user id given in the url path
    43  type Handler struct {
    44  	gatewayAddr string
    45  }
    46  
    47  // Init initializes this and any contained handlers
    48  func (h *Handler) Init(c *config.Config) {
    49  	h.gatewayAddr = c.GatewaySvc
    50  }
    51  
    52  // GetGroups handles GET requests on /cloud/users/groups
    53  // TODO: implement
    54  func (h *Handler) GetGroups(w http.ResponseWriter, r *http.Request) {
    55  	ctx := r.Context()
    56  
    57  	user := chi.URLParam(r, "userid")
    58  	// FIXME use ldap to fetch user info
    59  	u, ok := ctxpkg.ContextGetUser(ctx)
    60  	if !ok {
    61  		response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "missing user in context", fmt.Errorf("missing user in context"))
    62  		return
    63  	}
    64  	if user != u.Username {
    65  		// FIXME allow fetching other users info? only for admins
    66  		response.WriteOCSError(w, r, http.StatusForbidden, "user id mismatch", fmt.Errorf("%s tried to access %s user info endpoint", u.Id.OpaqueId, user))
    67  		return
    68  	}
    69  
    70  	response.WriteOCSSuccess(w, r, &Groups{Groups: u.Groups})
    71  }
    72  
    73  // Quota holds quota information
    74  type Quota struct {
    75  	Free       int64   `json:"free,omitempty" xml:"free,omitempty"`
    76  	Used       int64   `json:"used,omitempty" xml:"used,omitempty"`
    77  	Total      int64   `json:"total,omitempty" xml:"total,omitempty"`
    78  	Relative   float32 `json:"relative,omitempty" xml:"relative,omitempty"`
    79  	Definition string  `json:"definition,omitempty" xml:"definition,omitempty"`
    80  }
    81  
    82  // User holds user data
    83  type User struct {
    84  	Enabled     string `json:"enabled" xml:"enabled"`
    85  	Quota       *Quota `json:"quota,omitempty" xml:"quota,omitempty"`
    86  	Email       string `json:"email" xml:"email"`
    87  	DisplayName string `json:"displayname" xml:"displayname"` // is used in ocs/v(1|2).php/cloud/users/{username} - yes this is different from the /user endpoint
    88  	UserType    string `json:"user-type" xml:"user-type"`
    89  	UIDNumber   int64  `json:"uidnumber,omitempty" xml:"uidnumber,omitempty"`
    90  	GIDNumber   int64  `json:"gidnumber,omitempty" xml:"gidnumber,omitempty"`
    91  	// FIXME home should never be exposed ... even in oc 10, well only the admin can call this endpoint ...
    92  	// Home                 string `json:"home" xml:"home"`
    93  	TwoFactorAuthEnabled bool  `json:"two_factor_auth_enabled" xml:"two_factor_auth_enabled"`
    94  	LastLogin            int64 `json:"last_login" xml:"last_login"`
    95  }
    96  
    97  // Groups holds group data
    98  type Groups struct {
    99  	Groups []string `json:"groups" xml:"groups>element"`
   100  }
   101  
   102  // GetUsers handles GET requests on /cloud/users
   103  // Only allow self-read currently. TODO: List Users and Get on other users (both require
   104  // administrative privileges)
   105  func (h *Handler) GetUsers(w http.ResponseWriter, r *http.Request) {
   106  	userid := chi.URLParam(r, "userid")
   107  	userid, err := url.PathUnescape(userid)
   108  	if err != nil {
   109  		response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not unescape username", err)
   110  		return
   111  	}
   112  
   113  	currentUser, ok := ctxpkg.ContextGetUser(r.Context())
   114  	if !ok {
   115  		response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "missing user in context", fmt.Errorf("missing user in context"))
   116  		return
   117  	}
   118  
   119  	var user *cs3identity.User
   120  	switch {
   121  	case userid == "":
   122  		response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing username", fmt.Errorf("missing username"))
   123  		return
   124  	case userid == currentUser.Username:
   125  		user = currentUser
   126  	default:
   127  		// FIXME allow fetching other users info? only for admins
   128  		response.WriteOCSError(w, r, http.StatusForbidden, "user id mismatch", fmt.Errorf("%s tried to access %s user info endpoint", currentUser.Id.OpaqueId, user))
   129  		return
   130  	}
   131  
   132  	d := &User{
   133  		Enabled:     "true", // TODO include in response only when admin?
   134  		DisplayName: user.DisplayName,
   135  		Email:       user.Mail,
   136  		UserType:    conversions.UserTypeString(user.Id.Type),
   137  		Quota:       &Quota{},
   138  	}
   139  	// TODO how do we fill lastlogin of a user when another user (with the necessary permissions) looks up the user?
   140  	// TODO someone needs to fill last-login
   141  	if lastLogin := utils.ReadPlainFromOpaque(user.Opaque, "last-login"); lastLogin != "" {
   142  		d.LastLogin, _ = strconv.ParseInt(lastLogin, 10, 64)
   143  	}
   144  
   145  	// lightweight and federated users don't have access to their storage space
   146  	if currentUser.Id.Type != cs3identity.UserType_USER_TYPE_LIGHTWEIGHT && currentUser.Id.Type != cs3identity.UserType_USER_TYPE_FEDERATED {
   147  		h.fillPersonalQuota(r.Context(), d, user)
   148  	}
   149  
   150  	response.WriteOCSSuccess(w, r, d)
   151  }
   152  
   153  func (h Handler) fillPersonalQuota(ctx context.Context, d *User, u *cs3identity.User) {
   154  
   155  	sublog := appctx.GetLogger(ctx)
   156  
   157  	gc, err := pool.GetGatewayServiceClient(h.gatewayAddr)
   158  	if err != nil {
   159  		sublog.Error().Err(err).Msg("error getting gateway client")
   160  		return
   161  	}
   162  
   163  	res, err := gc.ListStorageSpaces(ctx, &cs3storage.ListStorageSpacesRequest{
   164  		Filters: []*cs3storage.ListStorageSpacesRequest_Filter{
   165  			{
   166  				Type: cs3storage.ListStorageSpacesRequest_Filter_TYPE_OWNER,
   167  				Term: &cs3storage.ListStorageSpacesRequest_Filter_Owner{
   168  					Owner: u.Id,
   169  				},
   170  			},
   171  			{
   172  				Type: cs3storage.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE,
   173  				Term: &cs3storage.ListStorageSpacesRequest_Filter_SpaceType{
   174  					SpaceType: "personal",
   175  				},
   176  			},
   177  		},
   178  	})
   179  	if err != nil {
   180  		sublog.Error().Err(err).Msg("error calling ListStorageSpaces")
   181  		return
   182  	}
   183  	if res.Status.Code != cs3rpc.Code_CODE_OK {
   184  		return
   185  	}
   186  
   187  	if len(res.StorageSpaces) == 0 {
   188  		sublog.Error().Err(err).Msg("list spaces returned empty list")
   189  		return
   190  
   191  	}
   192  
   193  	getQuotaRes, err := gc.GetQuota(ctx, &cs3gateway.GetQuotaRequest{Ref: &cs3storage.Reference{
   194  		ResourceId: res.StorageSpaces[0].Root,
   195  		Path:       ".",
   196  	}})
   197  	if err != nil {
   198  		sublog.Error().Err(err).Msg("error calling GetQuota")
   199  		return
   200  	}
   201  	if res.Status.Code != cs3rpc.Code_CODE_OK {
   202  		sublog.Debug().Interface("status", res.Status).Msg("GetQuota returned non OK result")
   203  		return
   204  	}
   205  
   206  	total := getQuotaRes.TotalBytes
   207  	used := getQuotaRes.UsedBytes
   208  
   209  	d.Quota = &Quota{
   210  		Used: int64(used),
   211  		// TODO support negative values or flags for the quota to carry special meaning: -1 = uncalculated, -2 = unknown, -3 = unlimited
   212  		// for now we can only report total and used
   213  		Total: int64(total),
   214  		// we cannot differentiate between `default` or a human readable `1 GB` definition.
   215  		// The web UI can create a human readable string from the actual total if it is set. Otherwise it has to leave out relative and total anyway.
   216  		// Definition: "default",
   217  	}
   218  
   219  	if raw := utils.ReadPlainFromOpaque(getQuotaRes.Opaque, "remaining"); raw != "" {
   220  		d.Quota.Free, _ = strconv.ParseInt(raw, 10, 64)
   221  	}
   222  
   223  	// only calculate free and relative when total is available
   224  	if total > 0 {
   225  		d.Quota.Free = int64(total - used)
   226  		d.Quota.Relative = float32((float64(used) / float64(total)) * 100.0)
   227  	} else {
   228  		d.Quota.Definition = "none" // this indicates no quota / unlimited to the ui
   229  	}
   230  }