Skip to content

Commit

Permalink
feature: add beta drive listing endpoints to the graph api (owncloud#…
Browse files Browse the repository at this point in the history
…7861)

* feature: add beta drive listing endpoints to the graph api and hydrate them to contain the new grantedtoV2 property and use unified roles instead of the cs3 roles

* enhancement: make use of owner conditions for drive listing

* enhancement: provide GetDrivesV1Beta1 and GetAllDrivesV1Beta1 graph endpoint tests
  • Loading branch information
fschade authored and 2403905 committed Dec 8, 2023
1 parent 70ef551 commit 2872931
Show file tree
Hide file tree
Showing 14 changed files with 578 additions and 84 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ require (
github.com/onsi/gomega v1.30.0
github.com/open-policy-agent/opa v0.59.0
github.com/orcaman/concurrent-map v1.0.0
github.com/owncloud/libre-graph-api-go v1.0.5-0.20231128074031-fdcdb2371356
github.com/owncloud/libre-graph-api-go v1.0.5-0.20231201125350-a08244876423
github.com/pkg/errors v0.9.1
github.com/pkg/xattr v0.4.9
github.com/prometheus/client_golang v1.17.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1790,8 +1790,8 @@ github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35uk
github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY=
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
github.com/owncloud/libre-graph-api-go v1.0.5-0.20231128074031-fdcdb2371356 h1:JjjpyUlD5nKF79QMpQ7/KVq41hh6f49GJNuxCYgMIMA=
github.com/owncloud/libre-graph-api-go v1.0.5-0.20231128074031-fdcdb2371356/go.mod h1:v2aAl5IwEI8t+GmcWvBd+bvJMYp9Vf1hekLuRf0UnEs=
github.com/owncloud/libre-graph-api-go v1.0.5-0.20231201125350-a08244876423 h1:G3i2n+lY6cTEerVEearRliEGeAxFuFQN0qM/1mdCQvs=
github.com/owncloud/libre-graph-api-go v1.0.5-0.20231201125350-a08244876423/go.mod h1:v2aAl5IwEI8t+GmcWvBd+bvJMYp9Vf1hekLuRf0UnEs=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
Expand Down
5 changes: 3 additions & 2 deletions services/graph/pkg/errorcode/errorcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@ func (e Error) Render(w http.ResponseWriter, r *http.Request) {
switch e.errorCode {
case AccessDenied:
status = http.StatusForbidden
case
InvalidRange:
case NotSupported:
status = http.StatusNotImplemented
case InvalidRange:
status = http.StatusRequestedRangeNotSatisfiable
case InvalidRequest:
status = http.StatusBadRequest
Expand Down
208 changes: 178 additions & 30 deletions services/graph/pkg/service/v0/drives.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ import (
merrors "go-micro.dev/v4/errors"
"golang.org/x/sync/errgroup"

revaConversions "github.com/cs3org/reva/v2/pkg/conversions"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/owncloud/ocis/v2/ocis-pkg/conversions"
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
v0 "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole"
settingsServiceExt "github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults"
)

Expand All @@ -57,19 +60,175 @@ var (
ErrForbiddenCharacter = fmt.Errorf("spacenames must not contain %v", _invalidSpaceNameCharacters)
)

// GetDrives lists all drives the current user has access to
func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) {
g.getDrives(w, r, false)
// GetDrives serves as a factory method that returns the appropriate
// http.Handler function based on the specified API version.
func (g Graph) GetDrives(version APIVersion) http.HandlerFunc {
switch version {
case APIVersion_1:
return g.GetDrivesV1
case APIVersion_1_Beta_1:
return g.GetDrivesV1Beta1
default:
return func(w http.ResponseWriter, r *http.Request) {
errorcode.New(errorcode.NotSupported, "api version not supported").Render(w, r)
}
}
}

// GetDrivesV1 attempts to retrieve the current users drives;
// it lists all drives the current user has access to.
func (g Graph) GetDrivesV1(w http.ResponseWriter, r *http.Request) {
spaces, errCode := g.getDrives(r, false)
if errCode != nil {
errCode.Render(w, r)
return
}

render.Status(r, http.StatusOK)

switch {
case spaces == nil && errCode == nil:
render.JSON(w, r, nil)
default:
render.JSON(w, r, &ListResponse{Value: spaces})
}

}

// GetDrivesV1Beta1 is the same as the GetDrivesV1 endpoint, expect:
// it includes the grantedtoV2 property
// it uses unified roles instead of the cs3 representations
func (g Graph) GetDrivesV1Beta1(w http.ResponseWriter, r *http.Request) {
spaces, errCode := g.getDrivesBeta(r, false)
if errCode != nil {
errCode.Render(w, r)
return
}

render.Status(r, http.StatusOK)

switch {
case spaces == nil && errCode == nil:
render.JSON(w, r, nil)
default:
render.JSON(w, r, &ListResponse{Value: spaces})
}
}

// GetAllDrives lists all drives, including other user's drives, if the current
// user has the permission.
func (g Graph) GetAllDrives(w http.ResponseWriter, r *http.Request) {
g.getDrives(w, r, true)
// GetAllDrives serves as a factory method that returns the appropriate
// http.Handler function based on the specified API version.
func (g Graph) GetAllDrives(version APIVersion) http.HandlerFunc {
switch version {
case APIVersion_1:
return g.GetAllDrivesV1
case APIVersion_1_Beta_1:
return g.GetAllDrivesV1Beta1
default:
return func(w http.ResponseWriter, r *http.Request) {
errorcode.New(errorcode.NotSupported, "api version not supported").Render(w, r)
}
}
}

// GetAllDrivesV1 attempts to retrieve the current users drives;
// it includes another user's drives, if the current user has the permission.
func (g Graph) GetAllDrivesV1(w http.ResponseWriter, r *http.Request) {
spaces, errCode := g.getDrives(r, true)
if errCode != nil {
errCode.Render(w, r)
return
}

render.Status(r, http.StatusOK)

switch {
case spaces == nil && errCode == nil:
render.JSON(w, r, nil)
default:
render.JSON(w, r, &ListResponse{Value: spaces})
}
}

// GetAllDrivesV1Beta1 is the same as the GetAllDrivesV1 endpoint, expect:
// it includes the grantedtoV2 property
// it uses unified roles instead of the cs3 representations
func (g Graph) GetAllDrivesV1Beta1(w http.ResponseWriter, r *http.Request) {
drives, errCode := g.getDrivesBeta(r, true)
if errCode != nil {
errCode.Render(w, r)
return
}

render.Status(r, http.StatusOK)

switch {
case drives == nil && errCode == nil:
render.JSON(w, r, nil)
default:
render.JSON(w, r, &ListResponse{Value: drives})
}
}

// getDrivesBeta retrieves the drives associated with the given request 'r'.
// It updates the 'GrantedToIdentities' to 'GrantedToV2',
// which represents the transition from legacy identity representation to a newer version.
// It also maps the old role names to their new unified role identifiers.
func (g Graph) getDrivesBeta(r *http.Request, unrestricted bool) ([]*libregraph.Drive, *errorcode.Error) {
drives, errCode := g.getDrives(r, unrestricted)
if errCode != nil {
return nil, errCode
}

for _, drive := range drives {
for i, permission := range drive.GetRoot().Permissions {
grantedToIdentities := permission.GetGrantedToIdentities()

if len(grantedToIdentities) < 1 {
continue
}

permission.GrantedToIdentities = nil
grantedToIdentity := grantedToIdentities[0]

permission.GrantedToV2 = &libregraph.SharePointIdentitySet{
User: grantedToIdentity.User,
Group: grantedToIdentity.Group,
}

for i, role := range permission.GetRoles() {

// v1 implementation for getDrives > ** > cs3PermissionsToLibreGraph
// does not use space related role names, we first need to resolve the correct descriptor.
switch role {
case revaConversions.RoleViewer:
role = revaConversions.RoleSpaceViewer
case revaConversions.RoleEditor:
role = revaConversions.RoleSpaceEditor
}

cs3Role := revaConversions.RoleFromName(role, g.config.FilesSharing.EnableResharing)
uniRole := unifiedrole.CS3ResourcePermissionsToUnifiedRole(
*cs3Role.CS3ResourcePermissions(),
unifiedrole.UnifiedRoleConditionOwner,
g.config.FilesSharing.EnableResharing,
)

if uniRole == nil {
continue
}

permission.Roles[i] = uniRole.GetId()
}

drive.Root.Permissions[i] = permission
}
}

return drives, nil
}

// getDrives implements the Service interface.
func (g Graph) getDrives(w http.ResponseWriter, r *http.Request, unrestricted bool) {
func (g Graph) getDrives(r *http.Request, unrestricted bool) ([]*libregraph.Drive, *errorcode.Error) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().
Interface("query", r.URL.Query()).
Expand All @@ -80,23 +239,20 @@ func (g Graph) getDrives(w http.ResponseWriter, r *http.Request, unrestricted bo
odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
if err != nil {
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get drives: query error")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
return nil, conversions.ToPointer(errorcode.New(errorcode.InvalidRequest, err.Error()))
}
ctx := r.Context()

filters, err := generateCs3Filters(odataReq)
if err != nil {
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get drives: error parsing filters")
errorcode.NotSupported.Render(w, r, http.StatusNotImplemented, err.Error())
return
return nil, conversions.ToPointer(errorcode.New(errorcode.NotSupported, err.Error()))
}
if !unrestricted {
user, ok := revactx.ContextGetUser(r.Context())
if !ok {
logger.Debug().Msg("could not create drive: invalid user")
errorcode.NotAllowed.Render(w, r, http.StatusUnauthorized, "invalid user")
return
return nil, conversions.ToPointer(errorcode.New(errorcode.AccessDenied, "invalid user"))
}
filters = append(filters, &storageprovider.ListStorageSpacesRequest_Filter{
Type: storageprovider.ListStorageSpacesRequest_Filter_TYPE_USER,
Expand All @@ -114,43 +270,35 @@ func (g Graph) getDrives(w http.ResponseWriter, r *http.Request, unrestricted bo
switch {
case err != nil:
logger.Error().Err(err).Msg("could not get drives: transport error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
return nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, err.Error()))
case res.Status.Code != cs3rpc.Code_CODE_OK:
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
// return an empty list
render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{})
return
// ok, empty return
return nil, nil
}
logger.Debug().Str("message", res.GetStatus().GetMessage()).Msg("could not get drives: grpc error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
return
return nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, res.Status.Message))
}

webDavBaseURL, err := g.getWebDavBaseURL()
if err != nil {
logger.Error().Err(err).Str("url", webDavBaseURL.String()).Msg("could not get drives: error parsing url")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
return nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, err.Error()))
}

spaces, err := g.formatDrives(ctx, webDavBaseURL, res.StorageSpaces)
if err != nil {
logger.Debug().Err(err).Msg("could not get drives: error parsing grpc response")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
return nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, err.Error()))
}

spaces, err = sortSpaces(odataReq, spaces)
if err != nil {
logger.Debug().Err(err).Msg("could not get drives: error sorting the spaces list according to query")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
return nil, conversions.ToPointer(errorcode.New(errorcode.InvalidRequest, err.Error()))
}

render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{Value: spaces})
return spaces, nil
}

// GetSingleDrive does a lookup of a single space by spaceId
Expand Down
20 changes: 17 additions & 3 deletions services/graph/pkg/service/v0/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ import (

gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/go-chi/chi/v5"
"github.com/jellydator/ttlcache/v3"
"go-micro.dev/v4/client"
mevents "go-micro.dev/v4/events"
"go.opentelemetry.io/otel/trace"
"google.golang.org/protobuf/types/known/emptypb"

"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/storagespace"

"github.com/owncloud/ocis/v2/ocis-pkg/keycloak"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0"
Expand Down Expand Up @@ -123,6 +124,19 @@ const (
SpaceImageSpecialFolderName = "image"
)

type APIVersion int

const (
// APIVersion_1 represents the first version of the API.
APIVersion_1 APIVersion = iota + 1

// APIVersion_1_Beta_1 refers to the beta version of the API.
// It is typically used for testing purposes and may have more
// inconsistencies and bugs than the stable version as it is
// still in the testing phase, use it with caution.
APIVersion_1_Beta_1
)

// TODO might be different for /education/users vs /users
func (g Graph) parseMemberRef(ref string) (string, string, error) {
memberURL, err := url.ParseRequestURI(ref)
Expand Down
Loading

0 comments on commit 2872931

Please sign in to comment.