From 6d465575f4883b913935c38b5983f10d3cc4d3e5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= <jfd@butonic.de>
Date: Fri, 23 Aug 2024 11:34:40 +0200
Subject: [PATCH] List OCM permissions as graph drive item permissions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
---
 changelog/unreleased/list-ocm-permissions.md  |   6 +
 .../service/v0/api_driveitem_permissions.go   |  11 ++
 services/graph/pkg/service/v0/base.go         | 147 ++++++++++++++++++
 services/graph/pkg/service/v0/utils.go        |  19 +++
 4 files changed, 183 insertions(+)
 create mode 100644 changelog/unreleased/list-ocm-permissions.md

diff --git a/changelog/unreleased/list-ocm-permissions.md b/changelog/unreleased/list-ocm-permissions.md
new file mode 100644
index 00000000000..5f5afa7f1c5
--- /dev/null
+++ b/changelog/unreleased/list-ocm-permissions.md
@@ -0,0 +1,6 @@
+Bugfix: List OCM permissions as graph drive item permissions
+
+The libre graph API now returns OCM shares when listing driveItem permissions.
+
+https://github.com/owncloud/ocis/pull/9905
+https://github.com/owncloud/ocis/issues/9898
diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions.go b/services/graph/pkg/service/v0/api_driveitem_permissions.go
index 0c571cbec3f..292a098b516 100644
--- a/services/graph/pkg/service/v0/api_driveitem_permissions.go
+++ b/services/graph/pkg/service/v0/api_driveitem_permissions.go
@@ -382,6 +382,17 @@ func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID
 		if err != nil {
 			return collectionOfPermissions, err
 		}
+		if s.config.IncludeOCMSharees {
+			driveItems, err = s.listOCMShares(ctx, []*ocm.ListOCMSharesRequest_Filter{
+				{
+					Type: ocm.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID,
+					Term: &ocm.ListOCMSharesRequest_Filter_ResourceId{ResourceId: itemID},
+				},
+			}, driveItems)
+			if err != nil {
+				return collectionOfPermissions, err
+			}
+		}
 	}
 	// finally get public shares, which are possible for spaceroots and "normal" resources
 	driveItems, err = s.listPublicShares(ctx, []*link.ListPublicSharesRequest_Filter{
diff --git a/services/graph/pkg/service/v0/base.go b/services/graph/pkg/service/v0/base.go
index dff462afdb9..6c8a58ecd6f 100644
--- a/services/graph/pkg/service/v0/base.go
+++ b/services/graph/pkg/service/v0/base.go
@@ -275,6 +275,34 @@ func (g BaseGraphService) listUserShares(ctx context.Context, filters []*collabo
 	return driveItems, nil
 }
 
+func (g BaseGraphService) listOCMShares(ctx context.Context, filters []*ocm.ListOCMSharesRequest_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())
+	}
+
+	concreteFilters := []*ocm.ListOCMSharesRequest_Filter{}
+	concreteFilters = append(concreteFilters, filters...)
+
+	lsOCMSharesRequest := ocm.ListOCMSharesRequest{
+		Filters: concreteFilters,
+	}
+
+	lsOCMSharesResponse, err := gatewayClient.ListOCMShares(ctx, &lsOCMSharesRequest)
+	if err != nil {
+		return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
+	}
+	if statusCode := lsOCMSharesResponse.GetStatus().GetCode(); statusCode != rpc.Code_CODE_OK {
+		return driveItems, errorcode.New(cs3StatusToErrCode(statusCode), lsOCMSharesResponse.Status.Message)
+	}
+	driveItems, err = g.cs3OCMSharesToDriveItems(ctx, lsOCMSharesResponse.Shares, driveItems)
+	if err != nil {
+		return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
+	}
+	return driveItems, nil
+}
+
 func (g BaseGraphService) listPublicShares(ctx context.Context, filters []*link.ListPublicSharesRequest_Filter, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
 
 	gatewayClient, err := g.gatewaySelector.Next()
@@ -343,6 +371,42 @@ func (g BaseGraphService) cs3UserSharesToDriveItems(ctx context.Context, shares
 	}
 	return driveItems, nil
 }
+func (g BaseGraphService) cs3OCMSharesToDriveItems(ctx context.Context, shares []*ocm.Share, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
+	for _, s := range shares {
+		g.logger.Debug().Interface("CS3 OCMShare", s).Msg("Got Share")
+		resIDStr := storagespace.FormatResourceID(s.ResourceId)
+		item, ok := driveItems[resIDStr]
+		if !ok {
+			itemptr, err := g.getDriveItem(ctx, &storageprovider.Reference{ResourceId: s.ResourceId})
+			if err != nil {
+				g.logger.Debug().Err(err).Interface("Share", s.ResourceId).Msg("could not stat ocm share, skipping")
+				continue
+			}
+			item = *itemptr
+		}
+
+		var condition string
+		switch {
+		case item.Folder != nil:
+			condition = unifiedrole.UnifiedRoleConditionFolderFederatedUser
+		case item.File != nil:
+			condition = unifiedrole.UnifiedRoleConditionFileFederatedUser
+		}
+		perm, err := g.cs3OCMShareToPermission(ctx, s, condition)
+
+		var errcode errorcode.Error
+		switch {
+		case errors.As(err, &errcode) && errcode.GetCode() == errorcode.ItemNotFound:
+			// The Grantee couldn't be found (user/group does not exist anymore)
+			continue
+		case err != nil:
+			return driveItems, err
+		}
+		item.Permissions = append(item.Permissions, *perm)
+		driveItems[resIDStr] = item
+	}
+	return driveItems, nil
+}
 
 func (g BaseGraphService) cs3UserShareToPermission(ctx context.Context, share *collaboration.Share, roleCondition string) (*libregraph.Permission, error) {
 	perm := libregraph.Permission{}
@@ -419,6 +483,89 @@ func (g BaseGraphService) cs3UserShareToPermission(ctx context.Context, share *c
 	}
 	return &perm, nil
 }
+func (g BaseGraphService) cs3OCMShareToPermission(ctx context.Context, share *ocm.Share, roleCondition string) (*libregraph.Permission, error) {
+	perm := libregraph.Permission{}
+	perm.SetRoles([]string{})
+	if roleCondition != unifiedrole.UnifiedRoleConditionDrive {
+		perm.SetId(share.GetId().GetOpaqueId())
+	}
+	grantedTo := libregraph.SharePointIdentitySet{}
+	// hm or use share.GetShareType() to determine the type of share???
+	switch share.GetGrantee().GetType() {
+	case storageprovider.GranteeType_GRANTEE_TYPE_USER:
+		user, err := cs3UserIdToIdentity(ctx, g.identityCache, share.Grantee.GetUserId())
+		switch {
+		case errors.Is(err, identity.ErrNotFound):
+			g.logger.Warn().Str("userid", share.Grantee.GetUserId().GetOpaqueId()).Msg("User not found by id")
+			// User does not seem to exist anymore, don't add a permission for this
+			return nil, errorcode.New(errorcode.ItemNotFound, "grantee does not exist")
+		case err != nil:
+			return nil, errorcode.New(errorcode.GeneralException, err.Error())
+		default:
+			grantedTo.SetUser(user)
+			if roleCondition == unifiedrole.UnifiedRoleConditionDrive {
+				perm.SetId("u:" + user.GetId())
+			}
+		}
+	case storageprovider.GranteeType_GRANTEE_TYPE_GROUP:
+		group, err := groupIdToIdentity(ctx, g.identityCache, share.Grantee.GetGroupId().GetOpaqueId())
+		switch {
+		case errors.Is(err, identity.ErrNotFound):
+			g.logger.Warn().Str("groupid", share.Grantee.GetGroupId().GetOpaqueId()).Msg("Group not found by id")
+			// Group not seem to exist anymore, don't add a permission for this
+			return nil, errorcode.New(errorcode.ItemNotFound, "grantee does not exist")
+		case err != nil:
+			return nil, errorcode.New(errorcode.GeneralException, err.Error())
+		default:
+			grantedTo.SetGroup(group)
+			if roleCondition == unifiedrole.UnifiedRoleConditionDrive {
+				perm.SetId("g:" + group.GetId())
+			}
+		}
+	}
+
+	// set expiration date
+	if share.GetExpiration() != nil {
+		perm.SetExpirationDateTime(cs3TimestampToTime(share.GetExpiration()))
+	}
+	// set cTime
+	if share.GetCtime() != nil {
+		perm.SetCreatedDateTime(cs3TimestampToTime(share.GetCtime()))
+	}
+	var permissions *storageprovider.ResourcePermissions
+	for _, role := range share.GetAccessMethods() {
+		if role.GetWebdavOptions().GetPermissions() != nil {
+			permissions = role.GetWebdavOptions().GetPermissions()
+		}
+	}
+
+	role := unifiedrole.CS3ResourcePermissionsToUnifiedRole(
+		permissions,
+		roleCondition,
+	)
+	if role != nil {
+		perm.SetRoles([]string{role.GetId()})
+	} else {
+		actions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(permissions)
+		perm.SetLibreGraphPermissionsActions(actions)
+		perm.SetRoles(nil)
+	}
+	perm.SetGrantedToV2(grantedTo)
+	if share.GetCreator() != nil {
+		identity, err := cs3UserIdToIdentity(ctx, g.identityCache, share.GetCreator())
+		if err != nil {
+			return nil, errorcode.New(errorcode.GeneralException, err.Error())
+		}
+		perm.SetInvitation(
+			libregraph.SharingInvitation{
+				InvitedBy: &libregraph.IdentitySet{
+					User: &identity,
+				},
+			},
+		)
+	}
+	return &perm, nil
+}
 
 func (g BaseGraphService) cs3PublicSharesToDriveItems(ctx context.Context, shares []*link.PublicShare, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
 	for _, s := range shares {
diff --git a/services/graph/pkg/service/v0/utils.go b/services/graph/pkg/service/v0/utils.go
index 9a1cd60ee93..d08a05567c4 100644
--- a/services/graph/pkg/service/v0/utils.go
+++ b/services/graph/pkg/service/v0/utils.go
@@ -108,6 +108,22 @@ func userIdToIdentity(ctx context.Context, cache identity.IdentityCache, userID
 	user, err := cache.GetUser(ctx, userID)
 	if err == nil {
 		identity.SetDisplayName(user.GetDisplayName())
+		identity.SetLibreGraphUserType(user.GetUserType())
+	}
+	return identity, err
+}
+
+// federatedIdToIdentity looks the user for the supplied id using the cache and returns it
+// as a libregraph.Identity
+func federatedIdToIdentity(ctx context.Context, cache identity.IdentityCache, userID string) (libregraph.Identity, error) {
+	identity := libregraph.Identity{
+		Id:                 libregraph.PtrString(userID),
+		LibreGraphUserType: libregraph.PtrString("Federated"),
+	}
+	user, err := cache.GetAcceptedUser(ctx, userID)
+	if err == nil {
+		identity.SetDisplayName(user.GetDisplayName())
+		identity.SetLibreGraphUserType(user.GetUserType())
 	}
 	return identity, err
 }
@@ -115,6 +131,9 @@ func userIdToIdentity(ctx context.Context, cache identity.IdentityCache, userID
 // cs3UserIdToIdentity looks up the user for the supplied cs3 userid using the cache and returns it
 // as a libregraph.Identity. Skips the user lookup if the id type is USER_TYPE_SPACE_OWNER
 func cs3UserIdToIdentity(ctx context.Context, cache identity.IdentityCache, cs3UserID *cs3User.UserId) (libregraph.Identity, error) {
+	if cs3UserID.GetType() == cs3User.UserType_USER_TYPE_FEDERATED {
+		return federatedIdToIdentity(ctx, cache, cs3UserID.GetOpaqueId())
+	}
 	if cs3UserID.GetType() != cs3User.UserType_USER_TYPE_SPACE_OWNER {
 		return userIdToIdentity(ctx, cache, cs3UserID.GetOpaqueId())
 	}