Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add beta drive listing endpoints to the graph api #7861

Merged
merged 4 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.29.0
github.com/open-policy-agent/opa v0.51.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 @@ -1789,8 +1789,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