diff --git a/extensions/ocs/pkg/service/v0/data/user.go b/extensions/ocs/pkg/service/v0/data/user.go index 34da73400e5..037d13e3ccd 100644 --- a/extensions/ocs/pkg/service/v0/data/user.go +++ b/extensions/ocs/pkg/service/v0/data/user.go @@ -8,22 +8,22 @@ type Users struct { // User holds the payload for a GetUser response type User struct { Enabled string `json:"enabled" xml:"enabled"` - UserID string `json:"id" xml:"id"`// UserID is mapped to the preferred_name attribute in accounts + UserID string `json:"id" xml:"id"` // UserID is mapped to the preferred_name attribute in accounts DisplayName string `json:"display-name" xml:"display-name"` LegacyDisplayName string `json:"displayname" xml:"displayname"` Email string `json:"email" xml:"email"` - Quota *Quota `json:"quota" xml:"quota"` + Quota *Quota `json:"quota,omitempty" xml:"quota,omitempty"` UIDNumber int64 `json:"uidnumber" xml:"uidnumber"` GIDNumber int64 `json:"gidnumber" xml:"gidnumber"` } // Quota holds quota information type Quota struct { - Free int64 `json:"free" xml:"free"` - Used int64 `json:"used" xml:"used"` - Total int64 `json:"total" xml:"total"` - Relative float32 `json:"relative" xml:"relative"` - Definition string `json:"definition" xml:"definition"` + Free int64 `json:"free,omitempty" xml:"free,omitempty"` + Used int64 `json:"used,omitempty" xml:"used,omitempty"` + Total int64 `json:"total,omitempty" xml:"total,omitempty"` + Relative float32 `json:"relative,omitempty" xml:"relative,omitempty"` + Definition string `json:"definition,omitempty" xml:"definition,omitempty"` } // SigningKey holds the Payload for a GetSigningKey response diff --git a/extensions/ocs/pkg/service/v0/users.go b/extensions/ocs/pkg/service/v0/users.go index 035288b980d..5f783caf829 100644 --- a/extensions/ocs/pkg/service/v0/users.go +++ b/extensions/ocs/pkg/service/v0/users.go @@ -10,9 +10,14 @@ import ( storemsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/store/v0" storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0" + "google.golang.org/grpc/metadata" - cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + cs3gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + cs3identity "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + cs3storage "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" revactx "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/go-chi/chi/v5" "github.com/go-micro/plugins/v4/client/grpc" "github.com/owncloud/ocis/v2/extensions/ocs/pkg/service/v0/data" @@ -48,12 +53,20 @@ func (o Ocs) GetUser(w http.ResponseWriter, r *http.Request) { o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error())) } - var user *cs3.User + currentUser, ok := revactx.ContextGetUser(r.Context()) + if !ok { + response.ErrRender(data.MetaServerError.StatusCode, "missing user in context") + return + } + + var user *cs3identity.User switch { case userid == "": - o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "missing user in context")) + o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "missing username")) + case userid == currentUser.Username: + user = currentUser case o.config.AccountBackend == "cs3": - user, err = o.fetchAccountFromCS3Backend(r.Context(), userid) + user, err = o.fetchUserFromCS3Backend(r.Context(), userid) default: o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend) } @@ -79,14 +92,12 @@ func (o Ocs) GetUser(w http.ResponseWriter, r *http.Request) { UIDNumber: user.UidNumber, GIDNumber: user.GidNumber, Enabled: "true", // TODO include in response only when admin? - // TODO query storage registry for free space? of home storage, maybe... - Quota: &data.Quota{ - Free: 2840756224000, - Used: 5059416668, - Total: 2845815640668, - Relative: 0.18, - Definition: "default", - }, + Quota: &data.Quota{}, + } + + // lightweight and federated users don't have access to their storage space + if currentUser.Id.Type != cs3identity.UserType_USER_TYPE_LIGHTWEIGHT && currentUser.Id.Type != cs3identity.UserType_USER_TYPE_FEDERATED { + o.fillPersonalQuota(r.Context(), d, user) } _, span := ocstracing.TraceProvider. @@ -97,6 +108,95 @@ func (o Ocs) GetUser(w http.ResponseWriter, r *http.Request) { o.mustRender(w, r, response.DataRender(d)) } +func (o Ocs) fillPersonalQuota(ctx context.Context, d *data.User, u *cs3identity.User) { + + gc, err := pool.GetGatewayServiceClient(o.config.Reva.Address) + if err != nil { + o.logger.Error().Err(err).Msg("error getting gateway client") + return + } + + aRes, err := gc.Authenticate(ctx, &cs3gateway.AuthenticateRequest{ + Type: "machine", + ClientId: "userid:" + u.Id.OpaqueId, + ClientSecret: o.config.MachineAuthAPIKey, + }) + + switch { + case err != nil: + o.logger.Error().Err(err).Msg("could not fill personal quota") + return + case aRes.Status.Code != cs3rpc.Code_CODE_OK: + o.logger.Error().Interface("status", aRes.Status).Msg("could not fill personal quota") + } + + ctx = metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, aRes.Token) + + res, err := gc.ListStorageSpaces(ctx, &cs3storage.ListStorageSpacesRequest{ + Filters: []*cs3storage.ListStorageSpacesRequest_Filter{ + { + Type: cs3storage.ListStorageSpacesRequest_Filter_TYPE_OWNER, + Term: &cs3storage.ListStorageSpacesRequest_Filter_Owner{ + Owner: aRes.User.Id, + }, + }, + { + Type: cs3storage.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE, + Term: &cs3storage.ListStorageSpacesRequest_Filter_SpaceType{ + SpaceType: "personal", + }, + }, + }, + }) + if err != nil { + o.logger.Error().Err(err).Msg("error calling ListStorageSpaces") + return + } + if res.Status.Code != cs3rpc.Code_CODE_OK { + o.logger.Debug().Interface("status", res.Status).Msg("ListStorageSpaces returned non OK result") + return + } + + if len(res.StorageSpaces) == 0 { + o.logger.Debug().Err(err).Msg("list spaces returned empty list") + return + } + + getQuotaRes, err := gc.GetQuota(ctx, &cs3gateway.GetQuotaRequest{Ref: &cs3storage.Reference{ + ResourceId: res.StorageSpaces[0].Root, + Path: ".", + }}) + if err != nil { + o.logger.Error().Err(err).Msg("error calling GetQuota") + return + } + if res.Status.Code != cs3rpc.Code_CODE_OK { + o.logger.Debug().Interface("status", res.Status).Msg("GetQuota returned non OK result") + return + } + + total := getQuotaRes.TotalBytes + used := getQuotaRes.UsedBytes + + d.Quota = &data.Quota{ + Used: int64(used), + // TODO support negative values or flags for the quota to carry special meaning: -1 = uncalculated, -2 = unknown, -3 = unlimited + // for now we can only report total and used + Total: int64(total), + // we cannot differentiate between `default` or a human readable `1 GB` defanation. + // The web ui can create a human readable string from the actual total if it is sot. Otherwise it has to leave out relative and total anyway. + // Definition: "default", + } + + // only calculate free and relative when total is available + if total > 0 { + d.Quota.Free = int64(total - used) + d.Quota.Relative = float32(float64(used) / float64(total)) + } else { + d.Quota.Definition = "none" // this indicates no quota / unlimited to the ui + } +} + // AddUser creates a new user account func (o Ocs) AddUser(w http.ResponseWriter, r *http.Request) { switch o.config.AccountBackend { @@ -241,7 +341,7 @@ func escapeValue(value string) string { return strings.ReplaceAll(value, "'", "''") } -func (o Ocs) fetchAccountFromCS3Backend(ctx context.Context, name string) (*cs3.User, error) { +func (o Ocs) fetchUserFromCS3Backend(ctx context.Context, name string) (*cs3identity.User, error) { backend := o.getCS3Backend() u, _, err := backend.GetUserByClaims(ctx, "username", name, false) if err != nil {