enhancement: add graph beta listPermissions endpoint (#7753)

* enhancement: add graph beta listPermissions endpoint

besides the new api endpoint it includes several utilities to simplify the graph api development.

* resolve drive and item id from the request path
* generic pointer and value utilities
* space root detection

* update GetDriveAndItemIDParam signature to return a error

* move errorcode package

* enhancement: add generic error code handling

* fix: rebase
This commit is contained in:
Florian Schade
2023-11-28 17:06:04 +01:00
committed by GitHub
parent eb6ec1311a
commit ad06a192d8
52 changed files with 946 additions and 220 deletions
+62
View File
@@ -0,0 +1,62 @@
package errorcode
import (
"slices"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
// FromCS3Status converts a CS3 status code into a corresponding local Error representation.
//
// It evaluates the provided CS3 status code and returns an equivalent graph Error.
// If the CS3 status code does not have a direct equivalent within the app,
// or is ignored, a general purpose Error is returned.
//
// This function is particularly useful when dealing with CS3 responses,
// and a unified error handling within the application is necessary.
func FromCS3Status(status *cs3rpc.Status, ignore ...cs3rpc.Code) *Error {
err := &Error{errorCode: GeneralException, msg: "unspecified error has occurred"}
if status != nil {
err.msg = status.GetMessage()
}
code := status.GetCode()
switch {
case slices.Contains(ignore, status.GetCode()):
fallthrough
case code == cs3rpc.Code_CODE_OK:
err = nil
case code == cs3rpc.Code_CODE_NOT_FOUND:
err.errorCode = ItemNotFound
case code == cs3rpc.Code_CODE_PERMISSION_DENIED:
err.errorCode = AccessDenied
case code == cs3rpc.Code_CODE_UNAUTHENTICATED:
err.errorCode = Unauthenticated
case code == cs3rpc.Code_CODE_INVALID_ARGUMENT:
err.errorCode = InvalidRequest
case code == cs3rpc.Code_CODE_ALREADY_EXISTS:
err.errorCode = NameAlreadyExists
case code == cs3rpc.Code_CODE_FAILED_PRECONDITION:
err.errorCode = PreconditionFailed
case code == cs3rpc.Code_CODE_UNIMPLEMENTED:
err.errorCode = NotSupported
}
return err
}
// FromStat transforms a *provider.StatResponse object and an error into an *Error.
//
// It takes a stat of type *provider.StatResponse, an error, and a variadic parameter of type cs3rpc.Code.
// If the error is not nil, it creates an Error object with the error message and a GeneralException code.
// If the error is nil, it invokes the FromCS3Status function with the StatResponse Status and the ignore codes.
func FromStat(stat *provider.StatResponse, err error, ignore ...cs3rpc.Code) *Error {
switch {
case err != nil:
return &Error{msg: err.Error(), errorCode: GeneralException}
default:
return FromCS3Status(stat.GetStatus(), ignore...)
}
}
+67
View File
@@ -0,0 +1,67 @@
package errorcode_test
import (
"errors"
"reflect"
"testing"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/owncloud/ocis/v2/ocis-pkg/conversions"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
func TestFromCS3Status(t *testing.T) {
var tests = []struct {
status *cs3rpc.Status
ignore []cs3rpc.Code
result *errorcode.Error
}{
{nil, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "unspecified error has occurred"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_OK}, nil, nil},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_NOT_FOUND}, []cs3rpc.Code{cs3rpc.Code_CODE_NOT_FOUND}, nil},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_PERMISSION_DENIED}, []cs3rpc.Code{cs3rpc.Code_CODE_NOT_FOUND, cs3rpc.Code_CODE_PERMISSION_DENIED}, nil},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_NOT_FOUND, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.ItemNotFound, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_PERMISSION_DENIED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.AccessDenied, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNAUTHENTICATED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.Unauthenticated, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INVALID_ARGUMENT, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.InvalidRequest, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_ALREADY_EXISTS, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.NameAlreadyExists, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_FAILED_PRECONDITION, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.PreconditionFailed, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNIMPLEMENTED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.NotSupported, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INVALID, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_CANCELLED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNKNOWN, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_RESOURCE_EXHAUSTED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_ABORTED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_OUT_OF_RANGE, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INTERNAL, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNAVAILABLE, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_REDIRECTION, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INSUFFICIENT_STORAGE, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_LOCKED, Message: "msg"}, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
}
for _, test := range tests {
if output := errorcode.FromCS3Status(test.status, test.ignore...); !reflect.DeepEqual(output, test.result) {
t.Error("Test Failed: {} expected, recieved: {}", test.result, output)
}
}
}
func TestFromStat(t *testing.T) {
var tests = []struct {
stat *provider.StatResponse
err error
result *errorcode.Error
}{
{nil, errors.New("some error"), conversions.ToPointer(errorcode.New(errorcode.GeneralException, "some error"))},
{&provider.StatResponse{Status: &cs3rpc.Status{Code: cs3rpc.Code_CODE_OK}}, nil, nil},
}
for _, test := range tests {
if output := errorcode.FromStat(test.stat, test.err); !reflect.DeepEqual(output, test.result) {
t.Error("Test Failed: {} expected, recieved: {}", test.result, output)
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
cs3group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
cs3user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
// Errors used by the interfaces
+1 -1
View File
@@ -12,7 +12,7 @@ import (
revautils "github.com/cs3org/reva/v2/pkg/utils"
"github.com/jellydator/ttlcache/v3"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
// IdentityCache implements a simple ttl based cache for looking up users and groups by ID
+1 -1
View File
@@ -13,7 +13,7 @@ import (
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
var (
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
const (
@@ -8,7 +8,7 @@ import (
"github.com/go-ldap/ldap/v3"
"github.com/libregraph/idm/pkg/ldapdn"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
type educationClassAttributeMap struct {
@@ -11,7 +11,7 @@ import (
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
type educationConfig struct {
@@ -10,7 +10,7 @@ import (
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/mocks"
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
@@ -8,7 +8,7 @@ import (
"github.com/go-ldap/ldap/v3"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
type educationUserAttributeMap struct {
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"github.com/libregraph/idm/pkg/ldapdn"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
type groupAttributeMap struct {
+4 -3
View File
@@ -3,15 +3,16 @@ package middleware
import (
"net/http"
gmmetadata "go-micro.dev/v4/metadata"
"google.golang.org/grpc/metadata"
"github.com/cs3org/reva/v2/pkg/auth/scope"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/token/manager/jwt"
"github.com/owncloud/ocis/v2/ocis-pkg/account"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
opkgm "github.com/owncloud/ocis/v2/ocis-pkg/middleware"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
gmmetadata "go-micro.dev/v4/metadata"
"google.golang.org/grpc/metadata"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
// authOptions initializes the available default options.
@@ -6,7 +6,7 @@ import (
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/roles"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
settings "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
)
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"crypto/subtle"
"net/http"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
var (
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
// ListApplications implements the Service interface.
@@ -12,7 +12,7 @@ import (
libregraph "github.com/owncloud/libre-graph-api-go"
settingsmsg "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/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
merrors "go-micro.dev/v4/errors"
)
+92 -24
View File
@@ -17,6 +17,7 @@ import (
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/go-chi/chi/v5"
@@ -25,12 +26,16 @@ import (
"golang.org/x/crypto/sha3"
"golang.org/x/sync/errgroup"
"github.com/cs3org/reva/v2/pkg/publicshare"
"github.com/cs3org/reva/v2/pkg/share"
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/log"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole"
"github.com/owncloud/ocis/v2/services/graph/pkg/validate"
)
@@ -244,16 +249,82 @@ func (g Graph) GetDriveItemChildren(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, &ListResponse{Value: files})
}
// Invite invites a user to a storage drive (space).
func (g Graph) Invite(w http.ResponseWriter, r *http.Request) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
// ListPermissions lists the permissions of a driveItem
func (g Graph) ListPermissions(w http.ResponseWriter, r *http.Request) {
gatewayClient, ok := g.GetGatewayClient(w, r)
if !ok {
return
}
_, itemID, err := g.extractDriveAndDriveItem(r)
_, itemID, err := g.GetDriveAndItemIDParam(r)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
ctx := r.Context()
statResponse, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &itemID}})
if errCode := errorcode.FromStat(statResponse, err); errCode != nil {
g.logger.Warn().Err(errCode).Interface("stat.res", statResponse)
errCode.Render(w, r)
return
}
permissionSet := *statResponse.GetInfo().GetPermissionSet()
allowedActions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(permissionSet)
collectionOfPermissions := libregraph.CollectionOfPermissionsWithAllowedValues{
LibreGraphPermissionsActionsAllowedValues: allowedActions,
LibreGraphPermissionsRolesAllowedValues: conversions.ToValueSlice(
unifiedrole.GetApplicableRoleDefinitionsForActions(
allowedActions,
unifiedrole.UnifiedRoleConditionGrantee,
g.config.FilesSharing.EnableResharing,
false,
),
),
}
for i, definition := range collectionOfPermissions.LibreGraphPermissionsRolesAllowedValues {
// the openapi spec defines that the rolePermissions should not be part of the response
definition.RolePermissions = nil
collectionOfPermissions.LibreGraphPermissionsRolesAllowedValues[i] = definition
}
driveItems := make(driveItemsByResourceID)
driveItems, err = g.listUserShares(ctx, []*collaboration.Filter{
share.ResourceIDFilter(conversions.ToPointer(itemID)),
}, driveItems)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
driveItems, err = g.listPublicShares(ctx, []*link.ListPublicSharesRequest_Filter{
publicshare.ResourceIDFilter(conversions.ToPointer(itemID)),
}, driveItems)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
for _, driveItem := range driveItems {
collectionOfPermissions.Value = append(collectionOfPermissions.Value, driveItem.Permissions...)
}
render.Status(r, http.StatusOK)
render.JSON(w, r, collectionOfPermissions)
}
// Invite invites a user to a storage drive (space).
func (g Graph) Invite(w http.ResponseWriter, r *http.Request) {
gatewayClient, ok := g.GetGatewayClient(w, r)
if !ok {
return
}
_, itemID, err := g.GetDriveAndItemIDParam(r)
if err != nil {
errorcode.RenderError(w, r, err)
return
@@ -268,23 +339,13 @@ func (g Graph) Invite(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err = validate.StructCtx(ctx, driveItemInvite); err != nil {
if err := validate.StructCtx(ctx, driveItemInvite); err != nil {
g.logger.Debug().Err(err).Interface("Body", r.Body).Msg("invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
statResponse, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &itemID}})
switch {
case err != nil:
fallthrough
case statResponse.GetStatus().GetCode() != cs3rpc.Code_CODE_OK:
g.logger.Debug().Err(err).Interface("itemID", itemID).Interface("Stat", statResponse).Msg("stat failed")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return
}
unifiedRolePermissions := []libregraph.UnifiedRolePermission{{AllowedResourceActions: driveItemInvite.LibreGraphPermissionsActions}}
unifiedRolePermissions := []*libregraph.UnifiedRolePermission{{AllowedResourceActions: driveItemInvite.LibreGraphPermissionsActions}}
for _, roleId := range driveItemInvite.GetRoles() {
role, err := unifiedrole.NewUnifiedRoleFromID(roleId, g.config.FilesSharing.EnableResharing)
if err != nil {
@@ -293,7 +354,14 @@ func (g Graph) Invite(w http.ResponseWriter, r *http.Request) {
return
}
unifiedRolePermissions = append(unifiedRolePermissions, role.GetRolePermissions()...)
unifiedRolePermissions = append(unifiedRolePermissions, conversions.ToPointerSlice(role.GetRolePermissions())...)
}
statResponse, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &itemID}})
if errCode := errorcode.FromStat(statResponse, err); errCode != nil {
g.logger.Warn().Err(errCode).Interface("stat.res", statResponse)
errCode.Render(w, r)
return
}
createShareErrors := sync.Map{}
@@ -349,7 +417,7 @@ func (g Graph) Invite(w http.ResponseWriter, r *http.Request) {
permission.GrantedToV2 = &libregraph.SharePointIdentitySet{
Group: &libregraph.Identity{
DisplayName: group.GetDisplayName(),
Id: libregraph.PtrString(group.GetId()),
Id: conversions.ToPointer(group.GetId()),
},
}
default:
@@ -368,7 +436,7 @@ func (g Graph) Invite(w http.ResponseWriter, r *http.Request) {
permission.GrantedToV2 = &libregraph.SharePointIdentitySet{
User: &libregraph.Identity{
DisplayName: user.GetDisplayName(),
Id: libregraph.PtrString(user.GetId()),
Id: conversions.ToPointer(user.GetId()),
},
}
}
@@ -388,7 +456,7 @@ func (g Graph) Invite(w http.ResponseWriter, r *http.Request) {
}
if id := createShareResponse.GetShare().GetId().GetOpaqueId(); id != "" {
permission.Id = libregraph.PtrString(id)
permission.Id = conversions.ToPointer(id)
}
if expiration := createShareResponse.GetShare().GetExpiration(); expiration != nil {
@@ -14,6 +14,7 @@ import (
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/chi/v5"
. "github.com/onsi/ginkgo/v2"
@@ -23,12 +24,15 @@ import (
"github.com/tidwall/gjson"
"google.golang.org/grpc"
"github.com/cs3org/reva/v2/pkg/storagespace"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/utils"
cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
"github.com/owncloud/ocis/v2/ocis-pkg/conversions"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"github.com/owncloud/ocis/v2/services/graph/mocks"
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
@@ -233,10 +237,7 @@ var _ = Describe("Driveitems", func() {
Describe("Invite", func() {
var (
itemID string
driveItemInvite *libregraph.DriveItemInvite
listSharesMock *mock.Call
listSharesResponse *collaboration.ListSharesResponse
statMock *mock.Call
statResponse *provider.StatResponse
getUserResponse *userpb.GetUserResponse
@@ -248,10 +249,9 @@ var _ = Describe("Driveitems", func() {
)
BeforeEach(func() {
itemID = "f0042750-23c5-441c-9f2c-ff7c53e5bd2a$cd621428-dfbe-44c1-9393-65bf0dd440a6!1177add3-b4eb-434e-a2e8-1859b31b17bf"
rctx := chi.NewRouteContext()
rctx.URLParams.Add("driveID", "f0042750-23c5-441c-9f2c-ff7c53e5bd2a$cd621428-dfbe-44c1-9393-65bf0dd440a6!cd621428-dfbe-44c1-9393-65bf0dd440a6")
rctx.URLParams.Add("itemID", itemID)
rctx.URLParams.Add("driveID", "1$2")
rctx.URLParams.Add("itemID", "1$2!3")
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
ctx = revactx.ContextSetUser(ctx, currentUser)
@@ -289,12 +289,6 @@ var _ = Describe("Driveitems", func() {
}
getGroupMock.Return(getGroupResponse, nil)
listSharesMock = gatewayClient.On("ListShares", mock.Anything, mock.Anything)
listSharesResponse = &collaboration.ListSharesResponse{
Status: status.NewOK(ctx),
}
listSharesMock.Return(listSharesResponse, nil)
createShareMock = gatewayClient.On("CreateShare", mock.Anything, mock.Anything)
createShareResponse = &collaboration.CreateShareResponse{
Status: status.NewOK(ctx),
@@ -378,53 +372,6 @@ var _ = Describe("Driveitems", func() {
Expect(jsonData.Get(`0.@libre\.graph\.permissions\.actions.0`).String()).To(Equal(unifiedrole.DriveItemContentRead))
})
It("validates the driveID", func() {
rctx := chi.NewRouteContext()
rctx.URLParams.Add("driveID", "")
ctx = context.WithValue(context.Background(), chi.RouteCtxKey, rctx)
svc.Invite(
rr,
httptest.NewRequest(http.MethodPost, "/", toJSONReader(driveItemInvite)).
WithContext(ctx),
)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("validates the itemID", func() {
rctx := chi.NewRouteContext()
rctx.URLParams.Add("driveID", "f0042750-23c5-441c-9f2c-ff7c53e5bd2a$cd621428-dfbe-44c1-9393-65bf0dd440a6!cd621428-dfbe-44c1-9393-65bf0dd440a6")
rctx.URLParams.Add("itemID", "")
ctx = context.WithValue(context.Background(), chi.RouteCtxKey, rctx)
svc.Invite(
rr,
httptest.NewRequest(http.MethodPost, "/", toJSONReader(driveItemInvite)).
WithContext(ctx),
)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("checks if the itemID and driveID is compatible to each other", func() {
rctx := chi.NewRouteContext()
rctx.URLParams.Add("driveID", "1$2!3")
rctx.URLParams.Add("itemID", "4$5!6")
ctx = context.WithValue(context.Background(), chi.RouteCtxKey, rctx)
svc.Invite(
rr,
httptest.NewRequest(http.MethodPost, "/", toJSONReader(driveItemInvite)).
WithContext(ctx),
)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("fails if the request body is empty", func() {
svc.Invite(
rr,
@@ -450,26 +397,6 @@ var _ = Describe("Driveitems", func() {
}, http.StatusBadRequest),
)
DescribeTable("Stat",
func(prep func(), code int) {
prep()
svc.Invite(
rr,
httptest.NewRequest(http.MethodPost, "/", toJSONReader(driveItemInvite)).
WithContext(ctx),
)
Expect(rr.Code).To(Equal(code))
statMock.Parent.AssertNumberOfCalls(GinkgoT(), "Stat", 1)
},
Entry("fails if not ok", func() {
statResponse.Status = status.NewNotFound(context.Background(), "")
}, http.StatusInternalServerError),
Entry("fails if errors", func() {
statMock.Return(nil, errors.New("error"))
}, http.StatusInternalServerError),
)
DescribeTable("GetGroup",
func(prep func(), code int) {
driveItemInvite.Recipients = []libregraph.DriveRecipient{
@@ -538,6 +465,90 @@ var _ = Describe("Driveitems", func() {
)
})
Describe("ListPermissions", func() {
var (
statMock *mock.Call
statResponse *provider.StatResponse
listSharesMock *mock.Call
listSharesResponse *collaboration.ListSharesResponse
listPublicSharesMock *mock.Call
listPublicSharesResponse *link.ListPublicSharesResponse
)
toResourceID := func(in string) *provider.ResourceId {
out, err := storagespace.ParseID(in)
Expect(err).To(BeNil())
return &out
}
BeforeEach(func() {
rctx := chi.NewRouteContext()
rctx.URLParams.Add("driveID", "1$2")
rctx.URLParams.Add("itemID", "1$2!3")
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
ctx = revactx.ContextSetUser(ctx, currentUser)
statMock = gatewayClient.On("Stat", mock.Anything, mock.Anything)
statResponse = &provider.StatResponse{
Status: status.NewOK(ctx),
Info: &provider.ResourceInfo{
Id: toResourceID("1$2!3"),
PermissionSet: unifiedrole.PermissionsToCS3ResourcePermissions(
conversions.ToPointerSlice(unifiedrole.NewViewerUnifiedRole(true).GetRolePermissions()),
),
Owner: &userpb.UserId{},
},
}
statMock.Return(statResponse, nil)
listSharesMock = gatewayClient.On("ListShares", mock.Anything, mock.Anything)
listSharesResponse = &collaboration.ListSharesResponse{
Status: status.NewOK(ctx),
Shares: []*collaboration.Share{{
Id: &collaboration.ShareId{OpaqueId: "123"},
ResourceId: toResourceID("1$2!3"),
Grantee: &provider.Grantee{},
Permissions: &collaboration.SharePermissions{
Permissions: unifiedrole.PermissionsToCS3ResourcePermissions(
conversions.ToPointerSlice(unifiedrole.NewViewerUnifiedRole(true).GetRolePermissions()),
),
},
}},
}
listSharesMock.Return(listSharesResponse, nil)
listPublicSharesMock = gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything)
listPublicSharesResponse = &link.ListPublicSharesResponse{
Status: status.NewOK(ctx),
}
listPublicSharesMock.Return(listPublicSharesResponse, nil)
})
It("lists permissions", func() {
svc.ListPermissions(
rr,
httptest.NewRequest(http.MethodGet, "/", nil).
WithContext(ctx),
)
Expect(rr.Code).To(Equal(http.StatusOK))
actions := gjson.Get(rr.Body.String(), `@libre\.graph\.permissions\.actions\.allowedValues`)
Expect(actions.Get("#").Num).To(Equal(float64(7)))
roles := gjson.Get(rr.Body.String(), `@libre\.graph\.permissions\.roles\.allowedValues`)
Expect(roles.Get("#").Num).To(Equal(float64(1)))
Expect(roles.Get("0.id").Str).To(Equal("b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5"))
Expect(roles.Get("0.rolePermissions").Exists()).To(BeFalse())
value := gjson.Get(rr.Body.String(), "value")
Expect(value.Get("#").Num).To(Equal(float64(1)))
Expect(value.Get("0.id").Str).To(Equal("123"))
})
})
Describe("GetRootDriveChildren", func() {
It("handles ListStorageSpaces not found", func() {
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
+9 -8
View File
@@ -19,19 +19,20 @@ import (
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
"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/service/v0/errorcode"
settingsServiceExt "github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults"
"github.com/pkg/errors"
merrors "go-micro.dev/v4/errors"
"golang.org/x/sync/errgroup"
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/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"
settingsServiceExt "github.com/owncloud/ocis/v2/services/settings/pkg/store/defaults"
)
const (
@@ -11,7 +11,7 @@ import (
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/events"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
@@ -22,9 +22,9 @@ import (
"github.com/owncloud/ocis/v2/services/graph/mocks"
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks"
service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/stretchr/testify/mock"
"google.golang.org/grpc"
)
@@ -11,8 +11,8 @@ import (
"github.com/CiscoM31/godata"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/identity"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
@@ -25,9 +25,9 @@ import (
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks"
service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
)
type schoolList struct {
@@ -15,7 +15,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
// GetEducationUsers implements the Service interface.
+1 -1
View File
@@ -26,8 +26,8 @@ import (
searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/identity"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
)
//go:generate make -C ../../.. generate
+1 -1
View File
@@ -29,8 +29,8 @@ import (
"github.com/owncloud/ocis/v2/services/graph/mocks"
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/pkg/errors"
"github.com/stretchr/testify/mock"
"google.golang.org/grpc"
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/CiscoM31/godata"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/events"
+1 -1
View File
@@ -22,9 +22,9 @@ import (
"github.com/owncloud/ocis/v2/services/graph/mocks"
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks"
service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/stretchr/testify/mock"
"google.golang.org/grpc"
)
+6 -4
View File
@@ -12,11 +12,12 @@ import (
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/linktype"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
)
// CreateLink creates a public link on the cs3 api
@@ -24,8 +25,9 @@ func (g Graph) CreateLink(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling create link")
_, driveItemID, ok := g.GetDriveAndItemIDParam(w, r)
if !ok {
_, driveItemID, err := g.GetDriveAndItemIDParam(r)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
@@ -1,9 +0,0 @@
package net
const (
// "github.com/cs3org/reva/v2/internal/http/services/datagateway" is internal so we redeclare it here
// HeaderTokenTransport holds the header key for the reva transfer token
HeaderTokenTransport = "X-Reva-Transfer"
// HeaderIfModifiedSince is used to mimic/pass on caching headers when using grpc
HeaderIfModifiedSince = "If-Modified-Since"
)
+4 -3
View File
@@ -7,11 +7,12 @@ import (
"github.com/CiscoM31/godata"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
// ChangeOwnPassword implements the Service interface. It allows the user to change
@@ -10,7 +10,7 @@ import (
"path/filepath"
"strconv"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
@@ -8,7 +8,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole"
)
+9 -7
View File
@@ -110,6 +110,7 @@ type Service interface {
CreateLink(w http.ResponseWriter, r *http.Request)
Invite(w http.ResponseWriter, r *http.Request)
ListPermissions(w http.ResponseWriter, r *http.Request)
DeletePermission(w http.ResponseWriter, r *http.Request)
GetTags(w http.ResponseWriter, r *http.Request)
@@ -202,8 +203,9 @@ func NewService(opts ...Option) (Graph, error) {
r.Get("/me/drive/sharedWithMe", svc.ListSharedWithMe)
r.Route("/drives/{driveID}/items/{itemID}", func(r chi.Router) {
r.Post("/invite", svc.Invite)
r.Post("/createLink", svc.CreateLink)
r.Get("/permissions", svc.ListPermissions)
r.Delete("/permissions/{permissionID}", svc.DeletePermission)
r.Post("/createLink", svc.CreateLink)
})
r.Route("/roleManagement/permissions/roleDefinitions", func(r chi.Router) {
r.Get("/", svc.GetRoleDefinitions)
@@ -222,13 +224,13 @@ func NewService(opts ...Option) (Graph, error) {
})
r.Route("/me", func(r chi.Router) {
r.Get("/", svc.GetMe)
r.Get("/drive", svc.GetUserDrive)
r.Route("/drives", func(r chi.Router) {
r.Get("/", svc.GetDrives)
})
r.Get("/drive/root/children", svc.GetRootDriveChildren)
r.Post("/changePassword", svc.ChangeOwnPassword)
r.Patch("/", svc.PatchMe)
r.Route("/drive", func(r chi.Router) {
r.Get("/", svc.GetUserDrive)
r.Get("/root/children", svc.GetRootDriveChildren)
})
r.Get("/drives", svc.GetDrives)
r.Post("/changePassword", svc.ChangeOwnPassword)
})
r.Route("/users", func(r chi.Router) {
r.With(requireAdmin).Get("/", svc.GetUsers)
+16 -11
View File
@@ -9,12 +9,14 @@ import (
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/share"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/cs3org/reva/v2/pkg/share"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/identity"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole"
)
@@ -27,13 +29,13 @@ func (g Graph) GetSharedByMe(w http.ResponseWriter, r *http.Request) {
driveItems := make(driveItemsByResourceID)
var err error
driveItems, err = g.listUserShares(ctx, driveItems)
driveItems, err = g.listUserShares(ctx, nil, driveItems)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
driveItems, err = g.listPublicShares(ctx, driveItems)
driveItems, err = g.listPublicShares(ctx, nil, driveItems)
if err != nil {
errorcode.RenderError(w, r, err)
return
@@ -48,19 +50,21 @@ func (g Graph) GetSharedByMe(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, &ListResponse{Value: res})
}
func (g Graph) listUserShares(ctx context.Context, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
func (g Graph) listUserShares(ctx context.Context, filters []*collaboration.Filter, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Error().Err(err).Msg("could not select next gateway client")
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
filters := []*collaboration.Filter{
concreteFilters := []*collaboration.Filter{
share.UserGranteeFilter(),
share.GroupGranteeFilter(),
}
concreteFilters = append(concreteFilters, filters...)
lsUserSharesRequest := collaboration.ListSharesRequest{
Filters: filters,
Filters: concreteFilters,
}
lsUserSharesResponse, err := gatewayClient.ListShares(ctx, &lsUserSharesRequest)
@@ -77,7 +81,7 @@ func (g Graph) listUserShares(ctx context.Context, driveItems driveItemsByResour
return driveItems, nil
}
func (g Graph) listPublicShares(ctx context.Context, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
func (g Graph) listPublicShares(ctx context.Context, filters []*link.ListPublicSharesRequest_Filter, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
@@ -85,10 +89,11 @@ func (g Graph) listPublicShares(ctx context.Context, driveItems driveItemsByReso
return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
}
filters := []*link.ListPublicSharesRequest_Filter{}
var concreteFilters []*link.ListPublicSharesRequest_Filter
concreteFilters = append(concreteFilters, filters...)
req := link.ListPublicSharesRequest{
Filters: filters,
Filters: concreteFilters,
}
lsPublicSharesResponse, err := gatewayClient.ListPublicShares(ctx, &req)
@@ -12,8 +12,8 @@ import (
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/identity"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
)
// ListSharedWithMe lists the files shared with the current user.
@@ -25,9 +25,9 @@ import (
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks"
service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
)
var _ = Describe("SharedWithMe", func() {
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
searchsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/search/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
"go-micro.dev/v4/metadata"
)
+1 -1
View File
@@ -31,8 +31,8 @@ import (
settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0"
settings "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/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/identity"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
ocissettingssvc "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
)
+33 -11
View File
@@ -5,8 +5,10 @@ import (
"io"
"net/http"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
)
// StrictJSONUnmarshal is a wrapper around json.Unmarshal that returns an error if the json contains unknown fields.
@@ -16,36 +18,56 @@ func StrictJSONUnmarshal(r io.Reader, v interface{}) error {
return dec.Decode(v)
}
// IsSpaceRoot returns true if the resourceID is a space root.
func IsSpaceRoot(rid *storageprovider.ResourceId) bool {
if rid == nil {
return false
}
if rid.GetSpaceId() == "" || rid.GetOpaqueId() == "" {
return false
}
return rid.GetSpaceId() == rid.GetOpaqueId()
}
// GetDriveAndItemIDParam parses the driveID and itemID from the request,
// validates the common fields and returns the parsed IDs if ok.
func (g Graph) GetDriveAndItemIDParam(w http.ResponseWriter, r *http.Request) (storageprovider.ResourceId, storageprovider.ResourceId, bool) {
func (g Graph) GetDriveAndItemIDParam(r *http.Request) (storageprovider.ResourceId, storageprovider.ResourceId, error) {
empty := storageprovider.ResourceId{}
driveID, err := parseIDParam(r, "driveID")
if err != nil {
g.logger.Debug().Err(err).Msg("could not parse driveID")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid driveID")
return empty, empty, false
return empty, empty, errorcode.New(errorcode.InvalidRequest, "invalid driveID")
}
itemID, err := parseIDParam(r, "itemID")
if err != nil {
g.logger.Debug().Err(err).Msg("could not parse itemID")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid itemID")
return empty, empty, false
return empty, empty, errorcode.New(errorcode.InvalidRequest, "invalid itemID")
}
if itemID.GetOpaqueId() == "" {
g.logger.Debug().Interface("driveID", driveID).Interface("itemID", itemID).Msg("empty item opaqueID")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid itemID")
return empty, empty, false
return empty, empty, errorcode.New(errorcode.InvalidRequest, "invalid itemID")
}
if driveID.GetStorageId() != itemID.GetStorageId() || driveID.GetSpaceId() != itemID.GetSpaceId() {
g.logger.Debug().Interface("driveID", driveID).Interface("itemID", itemID).Msg("driveID and itemID do not match")
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "driveID and itemID do not match")
return empty, empty, false
return empty, empty, errorcode.New(errorcode.ItemNotFound, "driveID and itemID do not match")
}
return driveID, itemID, true
return driveID, itemID, nil
}
// GetGatewayClient returns a gateway client from the gatewaySelector.
func (g Graph) GetGatewayClient(w http.ResponseWriter, r *http.Request) (gateway.GatewayAPIClient, bool) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return nil, false
}
return gatewayClient, true
}
@@ -0,0 +1,89 @@
package svc_test
import (
"context"
"net/http"
"net/http/httptest"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/chi/v5"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0"
)
var _ = Describe("Utils", func() {
var (
svc service.Graph
)
BeforeEach(func() {
cfg := defaults.FullDefaultConfig()
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
svc, _ = service.NewService(
service.Config(cfg),
)
})
DescribeTable("GetDriveAndItemIDParam",
func(driveID, itemID string, shouldPass bool) {
rctx := chi.NewRouteContext()
rctx.URLParams.Add("driveID", driveID)
rctx.URLParams.Add("itemID", itemID)
extractedDriveID, extractedItemID, err := svc.GetDriveAndItemIDParam(
httptest.NewRequest(http.MethodGet, "/", nil).
WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rctx),
),
)
switch shouldPass {
case true:
Expect(err).To(BeNil())
parsedItemID, _ := storagespace.ParseID(itemID)
Expect(extractedItemID).To(Equal(parsedItemID))
parsedDriveID, _ := storagespace.ParseID(driveID)
Expect(extractedDriveID).To(Equal(parsedDriveID))
default:
Expect(err).ToNot(BeNil())
}
},
Entry("fails: invalid driveID", "", "1$2!3", false),
Entry("fails: invalid itemID", "1$2", "", false),
Entry("fails: incompatible driveID and itemID", "1$2", "3$4!5", false),
Entry("fails: no itemID opaqueId", "1$2", "3$4", false),
Entry("pass: valid driveID and itemID", "1$2", "1$2!5", true),
)
DescribeTable("IsSpaceRoot",
func(resourceID *provider.ResourceId, isRoot bool) {
Expect(service.IsSpaceRoot(resourceID)).To(Equal(isRoot))
},
Entry("spaceId and opaqueID equal", &provider.ResourceId{
StorageId: "1",
OpaqueId: "2",
SpaceId: "2",
}, true),
Entry("nil", nil, false),
Entry("spaceID empty", &provider.ResourceId{
StorageId: "1",
OpaqueId: "2",
}, false),
Entry("opaqueID empty", &provider.ResourceId{
StorageId: "1",
SpaceId: "3",
}, false),
Entry("spaceID and opaqueID unequal", &provider.ResourceId{
OpaqueId: "2",
SpaceId: "3",
}, false),
)
})
+67 -1
View File
@@ -1,7 +1,9 @@
package unifiedrole
import (
"cmp"
"errors"
"slices"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/conversions"
@@ -217,8 +219,72 @@ func GetBuiltinRoleDefinitionList(resharing bool) []*libregraph.UnifiedRoleDefin
}
}
// GetApplicableRoleDefinitionsForActions returns a list of role definitions
// that match the provided actions and constraints
func GetApplicableRoleDefinitionsForActions(actions []string, constraints string, resharing, descending bool) []*libregraph.UnifiedRoleDefinition {
var definitions []*libregraph.UnifiedRoleDefinition
for _, definition := range GetBuiltinRoleDefinitionList(resharing) {
match := true
for _, permission := range definition.GetRolePermissions() {
if permission.GetCondition() != constraints {
match = false
break
}
for _, action := range permission.GetAllowedResourceActions() {
if !slices.Contains(actions, action) {
match = false
break
}
}
}
if !match {
continue
}
definitions = append(definitions, definition)
}
return WeightRoleDefinitions(definitions, descending)
}
// WeightRoleDefinitions sorts the provided role definitions by the number of permissions[n].actions they grant,
// the implementation is optimistic and assumes that the weight relies on the number of available actions.
// descending - false - sorts the roles from least to most permissions
// descending - true - sorts the roles from most to least permissions
func WeightRoleDefinitions(roleDefinitions []*libregraph.UnifiedRoleDefinition, descending bool) []*libregraph.UnifiedRoleDefinition {
slices.SortFunc(roleDefinitions, func(i, j *libregraph.UnifiedRoleDefinition) int {
var ia []string
for _, rp := range i.GetRolePermissions() {
ia = append(ia, rp.GetAllowedResourceActions()...)
}
var ja []string
for _, rp := range j.GetRolePermissions() {
ja = append(ja, rp.GetAllowedResourceActions()...)
}
switch descending {
case true:
return cmp.Compare(len(ja), len(ia))
default:
return cmp.Compare(len(ia), len(ja))
}
})
for i, definition := range roleDefinitions {
definition.LibreGraphWeight = libregraph.PtrInt32(int32(i) + 1)
}
// return for the sage of consistency, optional because the slice is modified in place
return roleDefinitions
}
// PermissionsToCS3ResourcePermissions converts the provided libregraph UnifiedRolePermissions to a cs3 ResourcePermissions
func PermissionsToCS3ResourcePermissions(unifiedRolePermissions []libregraph.UnifiedRolePermission) *provider.ResourcePermissions {
func PermissionsToCS3ResourcePermissions(unifiedRolePermissions []*libregraph.UnifiedRolePermission) *provider.ResourcePermissions {
p := &provider.ResourcePermissions{}
for _, permission := range unifiedRolePermissions {
@@ -2,8 +2,11 @@ package unifiedrole_test
import (
"fmt"
"slices"
rConversions "github.com/cs3org/reva/v2/pkg/conversions"
"github.com/owncloud/ocis/v2/ocis-pkg/conversions"
"github.com/cs3org/reva/v2/pkg/conversions"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/types"
@@ -14,24 +17,26 @@ import (
var _ = Describe("unifiedroles", func() {
DescribeTable("CS3ResourcePermissionsToUnifiedRole",
func(legacyRole *conversions.Role, unifiedRole *libregraph.UnifiedRoleDefinition) {
func(legacyRole *rConversions.Role, unifiedRole *libregraph.UnifiedRoleDefinition) {
cs3perm := legacyRole.CS3ResourcePermissions()
r := unifiedrole.CS3ResourcePermissionsToUnifiedRole(*cs3perm, unifiedrole.UnifiedRoleConditionGrantee, true)
Expect(r.GetId()).To(Equal(unifiedRole.GetId()))
},
Entry(conversions.RoleViewer, conversions.NewViewerRole(true), unifiedrole.NewViewerUnifiedRole(true)),
Entry(conversions.RoleEditor, conversions.NewEditorRole(true), unifiedrole.NewEditorUnifiedRole(true)),
Entry(conversions.RoleFileEditor, conversions.NewFileEditorRole(true), unifiedrole.NewFileEditorUnifiedRole(true)),
Entry(conversions.RoleCoowner, conversions.NewCoownerRole(), unifiedrole.NewCoownerUnifiedRole()),
Entry(conversions.RoleManager, conversions.NewManagerRole(), unifiedrole.NewManagerUnifiedRole()),
Entry(rConversions.RoleViewer, rConversions.NewViewerRole(true), unifiedrole.NewViewerUnifiedRole(true)),
Entry(rConversions.RoleEditor, rConversions.NewEditorRole(true), unifiedrole.NewEditorUnifiedRole(true)),
Entry(rConversions.RoleFileEditor, rConversions.NewFileEditorRole(true), unifiedrole.NewFileEditorUnifiedRole(true)),
Entry(rConversions.RoleCoowner, rConversions.NewCoownerRole(), unifiedrole.NewCoownerUnifiedRole()),
Entry(rConversions.RoleManager, rConversions.NewManagerRole(), unifiedrole.NewManagerUnifiedRole()),
)
DescribeTable("UnifiedRolePermissionsToCS3ResourcePermissions",
func(cs3Role *conversions.Role, libregraphRole *libregraph.UnifiedRoleDefinition, match bool) {
func(cs3Role *rConversions.Role, libregraphRole *libregraph.UnifiedRoleDefinition, match bool) {
permsFromCS3 := cs3Role.CS3ResourcePermissions()
permsFromUnifiedRole := unifiedrole.PermissionsToCS3ResourcePermissions(libregraphRole.RolePermissions)
permsFromUnifiedRole := unifiedrole.PermissionsToCS3ResourcePermissions(
conversions.ToPointerSlice(libregraphRole.RolePermissions),
)
var matcher types.GomegaMatcher
@@ -43,14 +48,203 @@ var _ = Describe("unifiedroles", func() {
Expect(permsFromCS3).To(matcher)
},
Entry(conversions.RoleViewer, conversions.NewViewerRole(true), unifiedrole.NewViewerUnifiedRole(true), true),
Entry(conversions.RoleEditor, conversions.NewEditorRole(true), unifiedrole.NewEditorUnifiedRole(true), true),
Entry(conversions.RoleFileEditor, conversions.NewFileEditorRole(true), unifiedrole.NewFileEditorUnifiedRole(true), true),
Entry(conversions.RoleCoowner, conversions.NewCoownerRole(), unifiedrole.NewCoownerUnifiedRole(), true),
Entry(conversions.RoleManager, conversions.NewManagerRole(), unifiedrole.NewManagerUnifiedRole(), true),
Entry("no match", conversions.NewFileEditorRole(true), unifiedrole.NewManagerUnifiedRole(), false),
Entry(rConversions.RoleViewer, rConversions.NewViewerRole(true), unifiedrole.NewViewerUnifiedRole(true), true),
Entry(rConversions.RoleEditor, rConversions.NewEditorRole(true), unifiedrole.NewEditorUnifiedRole(true), true),
Entry(rConversions.RoleFileEditor, rConversions.NewFileEditorRole(true), unifiedrole.NewFileEditorUnifiedRole(true), true),
Entry(rConversions.RoleCoowner, rConversions.NewCoownerRole(), unifiedrole.NewCoownerUnifiedRole(), true),
Entry(rConversions.RoleManager, rConversions.NewManagerRole(), unifiedrole.NewManagerUnifiedRole(), true),
Entry("no match", rConversions.NewFileEditorRole(true), unifiedrole.NewManagerUnifiedRole(), false),
)
DescribeTable("WeightRoleDefinitions",
func(roleDefinitions []*libregraph.UnifiedRoleDefinition, descending bool, expectedDefinitions []*libregraph.UnifiedRoleDefinition) {
for i, generatedDefinition := range unifiedrole.WeightRoleDefinitions(roleDefinitions, descending) {
Expect(generatedDefinition.Id).To(Equal(expectedDefinitions[i].Id))
}
},
Entry("ascending",
[]*libregraph.UnifiedRoleDefinition{
unifiedrole.NewViewerUnifiedRole(false),
unifiedrole.NewFileEditorUnifiedRole(false),
},
false,
[]*libregraph.UnifiedRoleDefinition{
unifiedrole.NewViewerUnifiedRole(false),
unifiedrole.NewFileEditorUnifiedRole(false),
},
),
Entry("descending",
[]*libregraph.UnifiedRoleDefinition{
unifiedrole.NewViewerUnifiedRole(false),
unifiedrole.NewFileEditorUnifiedRole(false),
},
true,
[]*libregraph.UnifiedRoleDefinition{
unifiedrole.NewFileEditorUnifiedRole(false),
unifiedrole.NewViewerUnifiedRole(false),
},
),
)
{
rolesToAction := func(definitions ...*libregraph.UnifiedRoleDefinition) []string {
var actions []string
for _, definition := range definitions {
for _, permission := range definition.GetRolePermissions() {
for _, action := range permission.GetAllowedResourceActions() {
if slices.Contains(actions, action) {
continue
}
actions = append(actions, action)
}
}
}
return actions
}
DescribeTable("GetApplicableRoleDefinitionsForActions",
func(givenActions []string, constraints string, resharing bool, expectedDefinitions []*libregraph.UnifiedRoleDefinition) {
generatedDefinitions := unifiedrole.GetApplicableRoleDefinitionsForActions(givenActions, constraints, resharing, false)
Expect(len(generatedDefinitions)).To(Equal(len(expectedDefinitions)))
for i, generatedDefinition := range generatedDefinitions {
Expect(generatedDefinition.Id).To(Equal(expectedDefinitions[i].Id))
Expect(*generatedDefinition.LibreGraphWeight).To(Equal(int32(i + 1)))
}
generatedActions := rolesToAction(generatedDefinitions...)
Expect(len(givenActions) >= len(generatedActions)).To(BeTrue())
for _, generatedAction := range generatedActions {
Expect(slices.Contains(givenActions, generatedAction)).To(BeTrue())
}
},
Entry(
"ViewerUnifiedRole",
rolesToAction(unifiedrole.NewViewerUnifiedRole(false)),
unifiedrole.UnifiedRoleConditionGrantee,
false,
[]*libregraph.UnifiedRoleDefinition{
unifiedrole.NewViewerUnifiedRole(false),
},
),
Entry(
"ViewerUnifiedRole | share",
rolesToAction(unifiedrole.NewViewerUnifiedRole(true)),
unifiedrole.UnifiedRoleConditionGrantee,
true,
[]*libregraph.UnifiedRoleDefinition{
unifiedrole.NewViewerUnifiedRole(true),
},
),
Entry(
"NewFileEditorUnifiedRole",
rolesToAction(unifiedrole.NewFileEditorUnifiedRole(false)),
unifiedrole.UnifiedRoleConditionGrantee,
false,
[]*libregraph.UnifiedRoleDefinition{
unifiedrole.NewViewerUnifiedRole(false),
unifiedrole.NewFileEditorUnifiedRole(false),
},
),
Entry(
"NewFileEditorUnifiedRole - share",
rolesToAction(unifiedrole.NewFileEditorUnifiedRole(true)),
unifiedrole.UnifiedRoleConditionGrantee,
true,
[]*libregraph.UnifiedRoleDefinition{
unifiedrole.NewViewerUnifiedRole(true),
unifiedrole.NewFileEditorUnifiedRole(true),
},
),
Entry(
"NewEditorUnifiedRole",
rolesToAction(unifiedrole.NewEditorUnifiedRole(false)),
unifiedrole.UnifiedRoleConditionGrantee,
false,
[]*libregraph.UnifiedRoleDefinition{
unifiedrole.NewUploaderUnifiedRole(),
unifiedrole.NewViewerUnifiedRole(false),
unifiedrole.NewFileEditorUnifiedRole(false),
unifiedrole.NewEditorUnifiedRole(false),
},
),
Entry(
"NewEditorUnifiedRole - share",
rolesToAction(unifiedrole.NewEditorUnifiedRole(true)),
unifiedrole.UnifiedRoleConditionGrantee,
true,
[]*libregraph.UnifiedRoleDefinition{
unifiedrole.NewUploaderUnifiedRole(),
unifiedrole.NewViewerUnifiedRole(true),
unifiedrole.NewFileEditorUnifiedRole(true),
unifiedrole.NewEditorUnifiedRole(true),
},
),
Entry(
"GetBuiltinRoleDefinitionList",
rolesToAction(unifiedrole.GetBuiltinRoleDefinitionList(false)...),
unifiedrole.UnifiedRoleConditionGrantee,
false,
[]*libregraph.UnifiedRoleDefinition{
unifiedrole.NewUploaderUnifiedRole(),
unifiedrole.NewViewerUnifiedRole(false),
unifiedrole.NewFileEditorUnifiedRole(false),
unifiedrole.NewEditorUnifiedRole(false),
unifiedrole.NewCoownerUnifiedRole(),
unifiedrole.NewManagerUnifiedRole(),
},
),
Entry(
"GetBuiltinRoleDefinitionList - share",
rolesToAction(unifiedrole.GetBuiltinRoleDefinitionList(true)...),
unifiedrole.UnifiedRoleConditionGrantee,
true,
[]*libregraph.UnifiedRoleDefinition{
unifiedrole.NewUploaderUnifiedRole(),
unifiedrole.NewViewerUnifiedRole(true),
unifiedrole.NewFileEditorUnifiedRole(true),
unifiedrole.NewEditorUnifiedRole(true),
unifiedrole.NewCoownerUnifiedRole(),
unifiedrole.NewManagerUnifiedRole(),
},
),
Entry(
"single",
[]string{unifiedrole.DriveItemQuotaRead},
unifiedrole.UnifiedRoleConditionGrantee,
true,
[]*libregraph.UnifiedRoleDefinition{},
),
Entry(
"mixed",
append(rolesToAction(unifiedrole.NewUploaderUnifiedRole()), unifiedrole.DriveItemQuotaRead),
unifiedrole.UnifiedRoleConditionGrantee,
true,
[]*libregraph.UnifiedRoleDefinition{
unifiedrole.NewUploaderUnifiedRole(),
},
),
)
}
{
var newUnifiedRoleFromIDEntries []TableEntry
for _, resharing := range []bool{true, false} {