Repository: owncloud/ocis
Pull request #7860 (2023-11-30 16:55:24)

graph: update permissions
This is an initial implementation of PATCH support on drives/{driveid}/items/{itemid}/permissions/{id}.
It focusses on updating user shares for now. It's possible to update the expirationDate, roles and/or libregraphResourceActions.

Updating the permissions of a space root or a public link share is currently not implemented. Both should come with a follow up PR.

Somehow the usershareprovider is also still lacking checks if the sharer is actually allowed to set the requested permissions (e.g. when re-sharing). Still need to investigate that, but that something for a reva PR.


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7860)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL)

[![56.4%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/50-16px.png '56.4%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_coverage&view=list) [56.4% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7860)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL) [4 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL)

[![78.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '78.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_coverage&view=list) [78.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7860)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL) [4 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL)

[![78.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '78.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_coverage&view=list) [78.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7860)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL) [4 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL)

[![76.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '76.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_coverage&view=list) [76.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7860)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL) [4 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL)

[![76.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '76.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_coverage&view=list) [76.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7860)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7860&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL) [4 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7860&resolved=false&types=CODE_SMELL)

[![76.5%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '76.5%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_coverage&view=list) [76.5% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7860&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go
index 38e960204ea..83406696f51 100644
--- a/services/graph/pkg/service/v0/driveitems.go
+++ b/services/graph/pkg/service/v0/driveitems.go
@@ -27,6 +27,7 @@ import (
 	libregraph "github.com/owncloud/libre-graph-api-go"
 	"golang.org/x/crypto/sha3"
 	"golang.org/x/sync/errgroup"
+	"google.golang.org/protobuf/types/known/fieldmaskpb"
 
 	"github.com/cs3org/reva/v2/pkg/publicshare"
 	"github.com/cs3org/reva/v2/pkg/share"
@@ -609,6 +610,65 @@ func (g Graph) Invite(w http.ResponseWriter, r *http.Request) {
 	render.JSON(w, r, &ListResponse{Value: value})
 }
 
+// UpdatePermission updates a Permission of a Drive item
+func (g Graph) UpdatePermission(w http.ResponseWriter, r *http.Request) {
+	_, itemID, err := g.GetDriveAndItemIDParam(r)
+	if err != nil {
+		errorcode.RenderError(w, r, err)
+		return
+	}
+
+	permissionID, err := url.PathUnescape(chi.URLParam(r, "permissionID"))
+
+	if err != nil {
+		g.logger.Debug().Err(err).Msg("could not parse permissionID")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid permissionID")
+		return
+	}
+
+	permission := &libregraph.Permission{}
+	if err := StrictJSONUnmarshal(r.Body, permission); err != nil {
+		g.logger.Debug().Err(err).Interface("Body", r.Body).Msg("failed unmarshalling request body")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid request body")
+		return
+	}
+
+	ctx := r.Context()
+	if err := validate.StructCtx(ctx, permission); 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
+	}
+
+	oldPermission, sharedResourceId, err := g.getPermissionByID(ctx, permissionID)
+	if err != nil {
+		errorcode.RenderError(w, r, err)
+		return
+	}
+
+	// The resourceID of the shared resource need to match the item ID from the Request Path
+	// otherwise this is an invalid Request.
+	if !utils.ResourceIDEqual(sharedResourceId, &itemID) {
+		g.logger.Debug().Msg("resourceID of shared does not match itemID")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "permissionID and itemID do not match")
+		return
+	}
+
+	// We don't implement updating link permissions yet
+	if _, ok := oldPermission.GetLinkOk(); ok {
+		errorcode.NotSupported.Render(w, r, http.StatusNotImplemented, "not implemented")
+		return
+	}
+	// This is a user share
+	updatedPermission, err := g.updateUserShare(ctx, permissionID, oldPermission, permission)
+	if err != nil {
+		errorcode.RenderError(w, r, err)
+	}
+	render.Status(r, http.StatusOK)
+	render.JSON(w, r, &updatedPermission)
+	return
+}
+
 // DeletePermission removes a Permission from a Drive item
 func (g Graph) DeletePermission(w http.ResponseWriter, r *http.Request) {
 	_, itemID, err := g.GetDriveAndItemIDParam(r)
@@ -629,7 +689,7 @@ func (g Graph) DeletePermission(w http.ResponseWriter, r *http.Request) {
 
 	// Check if the id is refering to a User Share
 	sharedResourceId, err := g.getUserPermissionResourceID(ctx, permissionID)
-	var errcode *errorcode.Error
+	var errcode errorcode.Error
 	if err != nil && errors.As(err, &errcode) && errcode.GetCode() == errorcode.ItemNotFound {
 		// there is no user share with that ID, so lets check if it is referring to a public link
 		isUserPermission = false
@@ -666,7 +726,43 @@ func (g Graph) DeletePermission(w http.ResponseWriter, r *http.Request) {
 	return
 }
 
+func (g Graph) getPermissionByID(ctx context.Context, permissionID string) (*libregraph.Permission, *storageprovider.ResourceId, error) {
+	share, err := g.getCS3UserShareByID(ctx, permissionID)
+	if err == nil {
+		permission, err := g.cs3UserShareToPermission(ctx, share)
+		if err != nil {
+			return nil, nil, err
+		}
+		return permission, share.GetResourceId(), nil
+	}
+
+	var errcode errorcode.Error
+	if errors.As(err, &errcode) && errcode.GetCode() == errorcode.ItemNotFound {
+		// there is no user share with that id, check if this is a public link
+		publicShare, err := g.getCS3PublicShareByID(ctx, permissionID)
+		if err != nil {
+			return nil, nil, err
+		}
+		permission, err := g.libreGraphPermissionFromCS3PublicShare(publicShare)
+		if err != nil {
+			return nil, nil, err
+		}
+		return permission, publicShare.GetResourceId(), nil
+	}
+
+	return nil, nil, err
+
+}
+
 func (g Graph) getUserPermissionResourceID(ctx context.Context, permissionID string) (*storageprovider.ResourceId, error) {
+	share, err := g.getCS3UserShareByID(ctx, permissionID)
+	if err != nil {
+		return nil, err
+	}
+	return share.GetResourceId(), nil
+}
+
+func (g Graph) getCS3UserShareByID(ctx context.Context, permissionID string) (*collaboration.Share, error) {
 	gatewayClient, err := g.gatewaySelector.Next()
 	if err != nil {
 		g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
@@ -684,9 +780,86 @@ func (g Graph) getUserPermissionResourceID(ctx context.Context, permissionID str
 			},
 		})
 	if errCode := errorcode.FromCS3Status(getShareResp.GetStatus(), err); errCode != nil {
-		return nil, errCode
+		return nil, *errCode
+	}
+	return getShareResp.GetShare(), nil
+}
+
+func (g Graph) updateUserShare(ctx context.Context, permissionID string, oldPermission, newPermission *libregraph.Permission) (*libregraph.Permission, error) {
+	gatewayClient, err := g.gatewaySelector.Next()
+	if err != nil {
+		g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
+		return nil, err
+	}
+
+	cs3UpdateShareReq := &collaboration.UpdateShareRequest{
+		Ref: &collaboration.ShareReference{
+			Spec: &collaboration.ShareReference_Id{
+				Id: &collaboration.ShareId{
+					OpaqueId: permissionID,
+				},
+			},
+		},
+		Share: &collaboration.Share{},
+	}
+	fieldmask := []string{}
+	if expiration, ok := newPermission.GetExpirationDateTimeOk(); ok {
+		fieldmask = append(fieldmask, "expiration")
+		if expiration != nil {
+			cs3UpdateShareReq.Share.Expiration = utils.TimeToTS(*expiration)
+		}
+	}
+	var roles, allowedResourceActions []string
+	var permissionsUpdated, ok bool
+	if roles, ok = newPermission.GetRolesOk(); ok && len(roles) > 0 {
+		for _, roleId := range roles {
+			role, err := unifiedrole.NewUnifiedRoleFromID(roleId, g.config.FilesSharing.EnableResharing)
+			if err != nil {
+				g.logger.Debug().Err(err).Interface("role", role).Msg("unable to convert requested role")
+				return nil, err
+			}
+
+			// FIXME: When setting permissions on a space, we need to use UnifiedRoleConditionOwner here
+			allowedResourceActions = unifiedrole.GetAllowedResourceActions(role, unifiedrole.UnifiedRoleConditionGrantee)
+			if len(allowedResourceActions) == 0 {
+				return nil, errorcode.New(errorcode.InvalidRequest, "role not applicable to this resource")
+			}
+		}
+		permissionsUpdated = true
+	} else if allowedResourceActions, ok = newPermission.GetLibreGraphPermissionsActionsOk(); ok && len(allowedResourceActions) > 0 {
+		permissionsUpdated = true
+	}
+
+	if permissionsUpdated {
+		cs3ResourcePermissions := unifiedrole.PermissionsToCS3ResourcePermissions(
+			[]*libregraph.UnifiedRolePermission{
+				{
+
+					AllowedResourceActions: allowedResourceActions,
+				},
+			},
+		)
+		cs3UpdateShareReq.Share.Permissions = &collaboration.SharePermissions{
+			Permissions: cs3ResourcePermissions,
+		}
+		fieldmask = append(fieldmask, "permissions")
+	}
+
+	cs3UpdateShareReq.UpdateMask = &fieldmaskpb.FieldMask{
+		Paths: fieldmask,
+	}
+
+	updateUserShareResp, err := gatewayClient.UpdateShare(ctx, cs3UpdateShareReq)
+	if errCode := errorcode.FromCS3Status(updateUserShareResp.GetStatus(), err); errCode != nil {
+		return nil, *errCode
+	}
+
+	permission, err := g.cs3UserShareToPermission(ctx, updateUserShareResp.GetShare())
+	if err != nil {
+		return nil, err
 	}
-	return getShareResp.Share.GetResourceId(), nil
+
+	return permission, nil
 }
 
 func (g Graph) removeUserShare(ctx context.Context, permissionID string) error {
@@ -708,13 +881,21 @@ func (g Graph) removeUserShare(ctx context.Context, permissionID string) error {
 		})
 
 	if errCode := errorcode.FromCS3Status(removeShareResp.GetStatus(), err); errCode != nil {
-		return errCode
+		return *errCode
 	}
 	// We need to return an untyped nil here otherwise the error==nil check won't work
 	return nil
 }
 
 func (g Graph) getLinkPermissionResourceID(ctx context.Context, permissionID string) (*storageprovider.ResourceId, error) {
+	share, err := g.getCS3PublicShareByID(ctx, permissionID)
+	if err != nil {
+		return nil, err
+	}
+	return share.GetResourceId(), nil
+}
+
+func (g Graph) getCS3PublicShareByID(ctx context.Context, permissionID string) (*link.PublicShare, error) {
 	gatewayClient, err := g.gatewaySelector.Next()
 	if err != nil {
 		g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
@@ -733,9 +914,9 @@ func (g Graph) getLinkPermissionResourceID(ctx context.Context, permissionID str
 		},
 	)
 	if errCode := errorcode.FromCS3Status(getPublicShareResp.GetStatus(), err); errCode != nil {
-		return nil, errCode
+		return nil, *errCode
 	}
-	return getPublicShareResp.Share.GetResourceId(), nil
+	return getPublicShareResp.GetShare(), nil
 }
 
 func (g Graph) removePublicShare(ctx context.Context, permissionID string) error {
@@ -756,7 +937,7 @@ func (g Graph) removePublicShare(ctx context.Context, permissionID string) error
 			},
 		})
 	if errcode := errorcode.FromCS3Status(removePublicShareResp.GetStatus(), err); errcode != nil {
-		return errcode
+		return *errcode
 	}
 	// We need to return an untyped nil here otherwise the error==nil check won't work
 	return nil
diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go
index b552a2b0b76..fd0679bd588 100644
--- a/services/graph/pkg/service/v0/service.go
+++ b/services/graph/pkg/service/v0/service.go
@@ -113,6 +113,7 @@ type Service interface {
 
 	Invite(w http.ResponseWriter, r *http.Request)
 	ListPermissions(w http.ResponseWriter, r *http.Request)
+	UpdatePermission(w http.ResponseWriter, r *http.Request)
 	DeletePermission(w http.ResponseWriter, r *http.Request)
 
 	CreateUploadSession(w http.ResponseWriter, r *http.Request)
@@ -210,6 +211,17 @@ func NewService(opts ...Option) (Graph, error) {
 					r.Get("/sharedWithMe", svc.ListSharedWithMe)
 				})
 			})
+			r.Route("/drives/{driveID}/items/{itemID}", func(r chi.Router) {
+				r.Post("/invite", svc.Invite)
+				r.Route("/permissions", func(r chi.Router) {
+					r.Get("/", svc.ListPermissions)
+					r.Route("/{permissionID}", func(r chi.Router) {
+						r.Delete("/", svc.DeletePermission)
+						r.Patch("/", svc.UpdatePermission)
+					})
+				})
+				r.Post("/createLink", svc.CreateLink)
+			})
 
 			r.Route("/drives", func(r chi.Router) {
 				r.Get("/", svc.GetAllDrives(APIVersion_1_Beta_1))
diff --git a/services/graph/pkg/service/v0/sharedbyme.go b/services/graph/pkg/service/v0/sharedbyme.go
index 085b44c4a4f..0d308b847b9 100644
--- a/services/graph/pkg/service/v0/sharedbyme.go
+++ b/services/graph/pkg/service/v0/sharedbyme.go
@@ -124,63 +124,77 @@ func (g Graph) cs3UserSharesToDriveItems(ctx context.Context, shares []*collabor
 			}
 			item = *itemptr
 		}
-		perm := libregraph.Permission{}
-		perm.SetRoles([]string{})
-		perm.SetId(s.Id.OpaqueId)
-		grantedTo := libregraph.SharePointIdentitySet{}
-		var li libregraph.Identity
-		switch s.Grantee.Type {
-		case storageprovider.GranteeType_GRANTEE_TYPE_USER:
-			user, err := g.identityCache.GetUser(ctx, s.Grantee.GetUserId().GetOpaqueId())
-			switch {
-			case errors.Is(err, identity.ErrNotFound):
-				g.logger.Warn().Str("userid", s.Grantee.GetUserId().GetOpaqueId()).Msg("User not found by id")
-				// User does not seem to exist anymore, don't add a permission for this
-				continue
-			case err != nil:
-				return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
-			default:
-				li.SetDisplayName(user.GetDisplayName())
-				li.SetId(user.GetId())
-				grantedTo.SetUser(li)
-			}
-		case storageprovider.GranteeType_GRANTEE_TYPE_GROUP:
-			group, err := g.identityCache.GetGroup(ctx, s.Grantee.GetGroupId().GetOpaqueId())
-			switch {
-			case errors.Is(err, identity.ErrNotFound):
-				g.logger.Warn().Str("groupid", s.Grantee.GetGroupId().GetOpaqueId()).Msg("Group not found by id")
-				// Group not seem to exist anymore, don't add a permission for this
-				continue
-			case err != nil:
-				return driveItems, errorcode.New(errorcode.GeneralException, err.Error())
-			default:
-				li.SetDisplayName(group.GetDisplayName())
-				li.SetId(group.GetId())
-				grantedTo.SetGroup(li)
-			}
+		perm, err := g.cs3UserShareToPermission(ctx, s)
+
+		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
+}
 
-		// set expiration date
-		if s.GetExpiration() != nil {
-			perm.SetExpirationDateTime(cs3TimestampToTime(s.GetExpiration()))
+func (g Graph) cs3UserShareToPermission(ctx context.Context, share *collaboration.Share) (*libregraph.Permission, error) {
+	perm := libregraph.Permission{}
+	perm.SetRoles([]string{})
+	perm.SetId(share.Id.OpaqueId)
+	grantedTo := libregraph.SharePointIdentitySet{}
+	var li libregraph.Identity
+	switch share.GetGrantee().GetType() {
+	case storageprovider.GranteeType_GRANTEE_TYPE_USER:
+		user, err := g.identityCache.GetUser(ctx, share.Grantee.GetUserId().GetOpaqueId())
+		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:
+			li.SetDisplayName(user.GetDisplayName())
+			li.SetId(user.GetId())
+			grantedTo.SetUser(li)
 		}
-		role := unifiedrole.CS3ResourcePermissionsToUnifiedRole(
-			*s.GetPermissions().GetPermissions(),
-			unifiedrole.UnifiedRoleConditionGrantee,
-			g.config.FilesSharing.EnableResharing,
-		)
-		if role != nil {
-			perm.SetRoles([]string{role.GetId()})
-		} else {
-			actions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(*s.GetPermissions().GetPermissions())
-			perm.SetLibreGraphPermissionsActions(actions)
-			perm.SetRoles(nil)
+	case storageprovider.GranteeType_GRANTEE_TYPE_GROUP:
+		group, err := g.identityCache.GetGroup(ctx, 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:
+			li.SetDisplayName(group.GetDisplayName())
+			li.SetId(group.GetId())
+			grantedTo.SetGroup(li)
 		}
-		perm.SetGrantedToV2(grantedTo)
-		item.Permissions = append(item.Permissions, perm)
-		driveItems[resIDStr] = item
 	}
-	return driveItems, nil
+
+	// set expiration date
+	if share.GetExpiration() != nil {
+		perm.SetExpirationDateTime(cs3TimestampToTime(share.GetExpiration()))
+	}
+	role := unifiedrole.CS3ResourcePermissionsToUnifiedRole(
+		*share.GetPermissions().GetPermissions(),
+		unifiedrole.UnifiedRoleConditionGrantee,
+		g.config.FilesSharing.EnableResharing,
+	)
+	if role != nil {
+		perm.SetRoles([]string{role.GetId()})
+	} else {
+		actions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(*share.GetPermissions().GetPermissions())
+		perm.SetLibreGraphPermissionsActions(actions)
+		perm.SetRoles(nil)
+	}
+	perm.SetGrantedToV2(grantedTo)
+	return &perm, nil
 }
 
 func (g Graph) cs3PublicSharesToDriveItems(ctx context.Context, shares []*link.PublicShare, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
diff --git a/services/graph/pkg/unifiedrole/unifiedrole.go b/services/graph/pkg/unifiedrole/unifiedrole.go
index bfc3e1ec833..58890f8e81c 100644
--- a/services/graph/pkg/unifiedrole/unifiedrole.go
+++ b/services/graph/pkg/unifiedrole/unifiedrole.go
@@ -486,3 +486,12 @@ func convert(role *conversions.Role) []string {
 	}
 	return CS3ResourcePermissionsToLibregraphActions(*role.CS3ResourcePermissions())
 }
+
+func GetAllowedResourceActions(role *libregraph.UnifiedRoleDefinition, condition string) []string {
+	for _, p := range role.GetRolePermissions() {
+		if p.GetCondition() == condition {
+			return p.GetAllowedResourceActions()
+		}
+	}
+	return []string{}
+}
diff --git a/services/graph/pkg/validate/libregraph.go b/services/graph/pkg/validate/libregraph.go
index 0317e2a5a03..3cd523d0785 100644
--- a/services/graph/pkg/validate/libregraph.go
+++ b/services/graph/pkg/validate/libregraph.go
@@ -12,6 +12,7 @@ import (
 // initLibregraph initializes libregraph validation
 func initLibregraph(v *validator.Validate) {
 	driveItemInvite(v)
+	permission(v)
 }
 
 // driveItemInvite validates libregraph.DriveItemInvite
@@ -27,55 +28,79 @@ func driveItemInvite(v *validator.Validate) {
 	v.RegisterStructValidation(func(sl validator.StructLevel) {
 		driveItemInvite := sl.Current().Interface().(libregraph.DriveItemInvite)
 
-		totalRoles := len(driveItemInvite.Roles)
-		totalActions := len(driveItemInvite.LibreGraphPermissionsActions)
+		rolesAndActions(sl, driveItemInvite.Roles, driveItemInvite.LibreGraphPermissionsActions, false)
 
-		switch {
-		case totalRoles != 0 && totalActions != 0:
-			fallthrough
-		case totalRoles == totalActions:
-			sl.ReportError(driveItemInvite.Roles, "Roles", "Roles", "one_or_another", "")
-			sl.ReportError(driveItemInvite.LibreGraphPermissionsActions, "LibreGraphPermissionsActions", "LibreGraphPermissionsActions", "one_or_another", "")
+	}, s)
+}
+
+// permission validates libregraph.Permission
+func permission(v *validator.Validate) {
+	s := libregraph.Permission{}
+
+	v.RegisterStructValidationMapRules(map[string]string{
+		"Roles": "max=1",
+	}, s)
+	v.RegisterStructValidation(func(sl validator.StructLevel) {
+		permission := sl.Current().Interface().(libregraph.Permission)
+
+		if _, ok := permission.GetIdOk(); ok {
+			sl.ReportError(permission.Id, "Id", "Id", "readonly", "")
 		}
 
-		var availableRoles []string
-		var availableActions []string
-		for _, definition := range append(
-			unifiedrole.GetBuiltinRoleDefinitionList(true),
-			unifiedrole.GetBuiltinRoleDefinitionList(false)...,
-		) {
-			if slices.Contains(availableRoles, definition.GetId()) {
-				continue
-			}
+		rolesAndActions(sl, permission.Roles, permission.LibreGraphPermissionsActions, true)
+	}, s)
+}
 
-			availableRoles = append(availableRoles, definition.GetId())
+func rolesAndActions(sl validator.StructLevel, roles, actions []string, allowEmpty bool) {
+	totalRoles := len(roles)
+	totalActions := len(actions)
+
+	switch {
+	case allowEmpty && totalRoles == 0 && totalActions == 0:
+		break
+	case totalRoles != 0 && totalActions != 0:
+		fallthrough
+	case totalRoles == totalActions:
+		sl.ReportError(roles, "Roles", "Roles", "one_or_another", "")
+		sl.ReportError(actions, "LibreGraphPermissionsActions", "LibreGraphPermissionsActions", "one_or_another", "")
+	}
+
+	var availableRoles []string
+	var availableActions []string
+	for _, definition := range append(
+		unifiedrole.GetBuiltinRoleDefinitionList(true),
+		unifiedrole.GetBuiltinRoleDefinitionList(false)...,
+	) {
+		if slices.Contains(availableRoles, definition.GetId()) {
+			continue
+		}
 
-			for _, permission := range definition.GetRolePermissions() {
-				for _, action := range permission.GetAllowedResourceActions() {
-					if slices.Contains(availableActions, action) {
-						continue
-					}
+		availableRoles = append(availableRoles, definition.GetId())
 
-					availableActions = append(availableActions, action)
+		for _, permission := range definition.GetRolePermissions() {
+			for _, action := range permission.GetAllowedResourceActions() {
+				if slices.Contains(availableActions, action) {
+					continue
 				}
-			}
-		}
 
-		for _, role := range driveItemInvite.Roles {
-			if slices.Contains(availableRoles, role) {
-				continue
+				availableActions = append(availableActions, action)
 			}
+		}
+	}
 
-			sl.ReportError(driveItemInvite.Roles, "Roles", "Roles", "available_role", "")
+	for _, role := range roles {
+		if slices.Contains(availableRoles, role) {
+			continue
 		}
 
-		for _, role := range driveItemInvite.LibreGraphPermissionsActions {
-			if slices.Contains(availableActions, role) {
-				continue
-			}
+		sl.ReportError(roles, "Roles", "Roles", "available_role", "")
+	}
 
-			sl.ReportError(driveItemInvite.LibreGraphPermissionsActions, "LibreGraphPermissionsActions", "LibreGraphPermissionsActions", "available_action", "")
+	for _, role := range actions {
+		if slices.Contains(availableActions, role) {
+			continue
 		}
 
-	}, s)
+		sl.ReportError(actions, "LibreGraphPermissionsActions", "LibreGraphPermissionsActions", "available_action", "")
+	}
 }


Test patch:
diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go
index e3eb2cb2562..909d6ed3ff8 100644
--- a/services/graph/pkg/service/v0/driveitems_test.go
+++ b/services/graph/pkg/service/v0/driveitems_test.go
@@ -12,10 +12,12 @@ import (
 
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
 	grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
+	user "github.com/cs3org/go-cs3apis/cs3/identity/user/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"
+	roleconversions "github.com/cs3org/reva/v2/pkg/conversions"
 	"github.com/go-chi/chi/v5"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
@@ -70,8 +72,6 @@ var _ = Describe("Driveitems", func() {
 	BeforeEach(func() {
 		eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
 
-		rr = httptest.NewRecorder()
-
 		pool.RemoveSelector("GatewaySelector" + "com.owncloud.api.gateway")
 		gatewayClient = &cs3mocks.GatewayAPIClient{}
 		gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient](
@@ -248,6 +248,270 @@ var _ = Describe("Driveitems", func() {
 		})
 	})
 
+	Describe("UpdatePermission", func() {
+		var (
+			driveItemPermission        *libregraph.Permission
+			getShareMockResponse       *collaboration.GetShareResponse
+			getPublicShareMockResponse *link.GetPublicShareResponse
+			getUserMockResponse        *user.GetUserResponse
+			updateShareMockResponse    *collaboration.UpdateShareResponse
+		)
+		BeforeEach(func() {
+			rr = httptest.NewRecorder()
+
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("driveID", "1$2")
+			rctx.URLParams.Add("itemID", "1$2!3")
+			rctx.URLParams.Add("permissionID", "permissionid")
+
+			ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
+			ctx = revactx.ContextSetUser(ctx, currentUser)
+
+			driveItemPermission = &libregraph.Permission{}
+
+			getUserMock := gatewayClient.On("GetUser", mock.Anything, mock.Anything)
+			getUserMockResponse = &userpb.GetUserResponse{
+				Status: status.NewOK(ctx),
+				User: &userpb.User{
+					Id:          &userpb.UserId{OpaqueId: "useri"},
+					DisplayName: "Test User",
+				},
+			}
+			getUserMock.Return(getUserMockResponse, nil)
+
+			getShareMock := gatewayClient.On("GetShare",
+				mock.Anything,
+				mock.MatchedBy(func(req *collaboration.GetShareRequest) bool {
+					return req.GetRef().GetId().GetOpaqueId() == "permissionid"
+				}),
+			)
+			share := &collaboration.Share{
+				Id: &collaboration.ShareId{
+					OpaqueId: "permissionid",
+				},
+				ResourceId: &provider.ResourceId{
+					StorageId: "1",
+					SpaceId:   "2",
+					OpaqueId:  "3",
+				},
+				Grantee: &provider.Grantee{
+					Type: provider.GranteeType_GRANTEE_TYPE_USER,
+					Id: &provider.Grantee_UserId{
+						UserId: &user.UserId{
+							OpaqueId: "userid",
+						},
+					},
+				},
+				Permissions: &collaboration.SharePermissions{
+					Permissions: roleconversions.NewViewerRole(true).CS3ResourcePermissions(),
+				},
+			}
+			getShareMockResponse = &collaboration.GetShareResponse{
+				Status: status.NewOK(ctx),
+				Share:  share,
+			}
+			getShareMock.Return(getShareMockResponse, nil)
+
+			updateShareMockResponse = &collaboration.UpdateShareResponse{
+				Status: status.NewOK(ctx),
+				Share:  share,
+			}
+
+			getPublicShareMock := gatewayClient.On("GetPublicShare",
+				mock.Anything,
+				mock.MatchedBy(func(req *link.GetPublicShareRequest) bool {
+					return req.GetRef().GetId().GetOpaqueId() == "permissionid"
+				}),
+			)
+			getPublicShareMockResponse = &link.GetPublicShareResponse{
+				Status: status.NewOK(ctx),
+				Share: &link.PublicShare{
+					Id: &link.PublicShareId{
+						OpaqueId: "permissionid",
+					},
+					ResourceId: &provider.ResourceId{
+						StorageId: "1",
+						SpaceId:   "2",
+						OpaqueId:  "3",
+					},
+					Permissions: &link.PublicSharePermissions{
+						Permissions: roleconversions.NewViewerRole(true).CS3ResourcePermissions(),
+					},
+					Token: "token",
+				},
+			}
+			getPublicShareMock.Return(getPublicShareMockResponse, nil)
+
+		})
+		It("fails when no share is found", func() {
+			getShareMockResponse.Share = nil
+			getShareMockResponse.Status = status.NewNotFound(ctx, "not found")
+			getPublicShareMockResponse.Share = nil
+			getPublicShareMockResponse.Status = status.NewNotFound(ctx, "not found")
+
+			body, err := driveItemPermission.MarshalJSON()
+			Expect(err).To(BeNil())
+			svc.UpdatePermission(
+				rr,
+				httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))).
+					WithContext(ctx),
+			)
+			Expect(rr.Code).To(Equal(http.StatusNotFound))
+		})
+		// Updating a public link will be implemented later
+		It("fails when trying to update a link permission", func() {
+			getShareMockResponse.Share = nil
+			getShareMockResponse.Status = status.NewNotFound(ctx, "not found")
+
+			body, err := driveItemPermission.MarshalJSON()
+			Expect(err).To(BeNil())
+			svc.UpdatePermission(
+				rr,
+				httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))).
+					WithContext(ctx),
+			)
+			Expect(rr.Code).To(Equal(http.StatusNotImplemented))
+		})
+		It("fails updating the id", func() {
+			driveItemPermission.SetId("permissionid")
+			body, err := driveItemPermission.MarshalJSON()
+			Expect(err).To(BeNil())
+			svc.UpdatePermission(
+				rr,
+				httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))).
+					WithContext(ctx),
+			)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+		It("updates the expiration date", func() {
+			expiration := time.Now().Add(time.Hour)
+			updateShareMock := gatewayClient.On("UpdateShare",
+				mock.Anything,
+				mock.MatchedBy(func(req *collaboration.UpdateShareRequest) bool {
+					if req.GetRef().GetId().GetOpaqueId() == "permissionid" {
+						return expiration.Equal(utils.TSToTime(req.GetShare().GetExpiration()))
+					}
+					return false
+				}),
+			)
+			updateShareMockResponse.Share.Expiration = utils.TimeToTS(expiration)
+			updateShareMock.Return(updateShareMockResponse, nil)
+
+			driveItemPermission.SetExpirationDateTime(expiration)
+			body, err := driveItemPermission.MarshalJSON()
+			Expect(err).To(BeNil())
+			svc.UpdatePermission(
+				rr,
+				httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))).
+					WithContext(ctx),
+			)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := libregraph.Permission{}
+
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(res.GetExpirationDateTime().Equal(expiration)).To(BeTrue())
+		})
+		It("deletes the expiration date", func() {
+			updateShareMock := gatewayClient.On("UpdateShare",
+				mock.Anything,
+				mock.MatchedBy(func(req *collaboration.UpdateShareRequest) bool {
+					if req.GetRef().GetId().GetOpaqueId() == "permissionid" {
+						return true
+					}
+					return false
+				}),
+			)
+			updateShareMock.Return(updateShareMockResponse, nil)
+
+			driveItemPermission.SetExpirationDateTimeNil()
+			body, err := driveItemPermission.MarshalJSON()
+			Expect(err).To(BeNil())
+			svc.UpdatePermission(
+				rr,
+				httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))).
+					WithContext(ctx),
+			)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := libregraph.Permission{}
+
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+			_, ok := res.GetExpirationDateTimeOk()
+			Expect(ok).To(BeFalse())
+		})
+		It("updates the share permissions with changing the role", func() {
+			updateShareMock := gatewayClient.On("UpdateShare",
+				mock.Anything,
+				mock.MatchedBy(func(req *collaboration.UpdateShareRequest) bool {
+					return req.GetRef().GetId().GetOpaqueId() == "permissionid"
+				}),
+			)
+			updateShareMock.Return(updateShareMockResponse, nil)
+
+			driveItemPermission.SetRoles([]string{unifiedrole.NewViewerUnifiedRole(true).GetId()})
+			body, err := driveItemPermission.MarshalJSON()
+			Expect(err).To(BeNil())
+			svc.UpdatePermission(
+				rr,
+				httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))).
+					WithContext(ctx),
+			)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := libregraph.Permission{}
+
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+			_, ok := res.GetRolesOk()
+			Expect(ok).To(BeTrue())
+		})
+		It("updates the share permissions when changing the resource permission actions", func() {
+			updateShareMock := gatewayClient.On("UpdateShare",
+				mock.Anything,
+				mock.MatchedBy(func(req *collaboration.UpdateShareRequest) bool {
+					return req.GetRef().GetId().GetOpaqueId() == "permissionid"
+				}),
+			)
+			updateShareMockResponse.Share.Permissions = &collaboration.SharePermissions{
+				Permissions: &provider.ResourcePermissions{
+					GetPath: true,
+				},
+			}
+
+			updateShareMock.Return(updateShareMockResponse, nil)
+
+			driveItemPermission.SetLibreGraphPermissionsActions([]string{unifiedrole.DriveItemPathRead})
+			body, err := driveItemPermission.MarshalJSON()
+			Expect(err).To(BeNil())
+			svc.UpdatePermission(
+				rr,
+				httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))).
+					WithContext(ctx),
+			)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := libregraph.Permission{}
+
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+			_, ok := res.GetRolesOk()
+			Expect(ok).To(BeFalse())
+			_, ok = res.GetLibreGraphPermissionsActionsOk()
+			Expect(ok).To(BeTrue())
+		})
+	})
+
 	Describe("Invite", func() {
 		var (
 			driveItemInvite     *libregraph.DriveItemInvite