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 }