Repository: owncloud/ocis
Pull request #7908 (2023-12-07 11:20:59)

Edit public links and setPassword
## Description

- [x] Add https://owncloud.dev/libre-graph-api/#/drives.permissions/UpdatePermission for public shares
- [x] Add https://owncloud.dev/libre-graph-api/#/drives.permissions/SetPermissionPassword

## Related Issue
<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->
- Relates to #6993 

## Motivation and Context

Improve our APIs 🎉 

## How Has This Been Tested?
- manually
- unit tests


## Screenshots (if appropriate):

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Technical debt
- [ ] Tests only (no source changes)

## Checklist:
<!-- Tick the checkboxes when done. -->
<!-- Raise documentation ticket in owncloud.github.io/ -->
- [x] Code changes
- [x] Unit tests added
- [ ] Acceptance tests added
- [ ] Documentation ticket raised: <link> 


update-docs[bot]: Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a [changelog](https://github.com/owncloud/ocis/blob/master/changelog/README.md) item based on your changes.

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=7908)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&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=7908&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&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=7908&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=7908&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&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=7908&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=7908&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7908&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=7908&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=7908&resolved=false&types=CODE_SMELL) [4 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&resolved=false&types=CODE_SMELL)

[![49.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/40-16px.png '49.3%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7908&metric=new_coverage&view=list) [49.3% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7908&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=7908&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7908&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=7908)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&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=7908&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&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=7908&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=7908&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&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=7908&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=7908&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7908&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=7908&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=7908&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&resolved=false&types=CODE_SMELL)

[![1.8%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '1.8%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7908&metric=new_coverage&view=list) [1.8% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7908&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=7908&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7908&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=7908)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&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=7908&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&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=7908&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=7908&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&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=7908&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=7908&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7908&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=7908&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=7908&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&resolved=false&types=CODE_SMELL)

[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7908&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7908&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=7908&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7908&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=7908)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&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=7908&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&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=7908&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=7908&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&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=7908&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=7908&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7908&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=7908&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=7908&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7908&resolved=false&types=CODE_SMELL)

[![34.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/25-16px.png '34.3%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7908&metric=new_coverage&view=list) [34.3% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7908&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=7908&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7908&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/changelog/unreleased/add-edit-public-share.md b/changelog/unreleased/add-edit-public-share.md
new file mode 100644
index 00000000000..3a751b0d633
--- /dev/null
+++ b/changelog/unreleased/add-edit-public-share.md
@@ -0,0 +1,6 @@
+Enhancement: Add edit public share to sharing NG
+
+We added the ability to edit public shares to the sharing NG endpoints.
+
+https://github.com/owncloud/ocis/pull/7908/
+https://github.com/owncloud/ocis/issues/6993
diff --git a/services/graph/pkg/errorcode/cs3.go b/services/graph/pkg/errorcode/cs3.go
index 756cf271a46..12bd7b5c8b3 100644
--- a/services/graph/pkg/errorcode/cs3.go
+++ b/services/graph/pkg/errorcode/cs3.go
@@ -45,7 +45,7 @@ func FromCS3Status(status *cs3rpc.Status, inerr error, ignore ...cs3rpc.Code) *E
 	case code == cs3rpc.Code_CODE_ALREADY_EXISTS:
 		err.errorCode = NameAlreadyExists
 	case code == cs3rpc.Code_CODE_FAILED_PRECONDITION:
-		err.errorCode = PreconditionFailed
+		err.errorCode = InvalidRequest
 	case code == cs3rpc.Code_CODE_OUT_OF_RANGE:
 		err.errorCode = InvalidRange
 	case code == cs3rpc.Code_CODE_UNIMPLEMENTED:
diff --git a/services/graph/pkg/errorcode/errorcode.go b/services/graph/pkg/errorcode/errorcode.go
index a94fa3de597..14db1bb0a5c 100644
--- a/services/graph/pkg/errorcode/errorcode.go
+++ b/services/graph/pkg/errorcode/errorcode.go
@@ -135,6 +135,8 @@ func (e Error) Render(w http.ResponseWriter, r *http.Request) {
 		status = http.StatusMethodNotAllowed
 	case ItemIsLocked:
 		status = http.StatusLocked
+	case PreconditionFailed:
+		status = http.StatusPreconditionFailed
 	default:
 		status = http.StatusInternalServerError
 	}
diff --git a/services/graph/pkg/linktype/linktype.go b/services/graph/pkg/linktype/linktype.go
index e73cef66887..9b36b6cec74 100644
--- a/services/graph/pkg/linktype/linktype.go
+++ b/services/graph/pkg/linktype/linktype.go
@@ -31,6 +31,9 @@ func (l *LinkType) GetPermissions() *provider.ResourcePermissions {
 // SharingLinkTypeFromCS3Permissions creates a libregraph link type
 // It returns a list of libregraph actions when the conversion is not possible
 func SharingLinkTypeFromCS3Permissions(permissions *linkv1beta1.PublicSharePermissions) (*libregraph.SharingLinkType, []string) {
+	if permissions == nil {
+		return nil, nil
+	}
 	linkTypes := GetAvailableLinkTypes()
 	for _, linkType := range linkTypes {
 		if grants.PermissionsEqual(linkType.GetPermissions(), permissions.GetPermissions()) {
diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go
index 83406696f51..4f9432c5580 100644
--- a/services/graph/pkg/service/v0/driveitems.go
+++ b/services/graph/pkg/service/v0/driveitems.go
@@ -654,11 +654,18 @@ func (g Graph) UpdatePermission(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// We don't implement updating link permissions yet
+	// This is a public link
 	if _, ok := oldPermission.GetLinkOk(); ok {
-		errorcode.NotSupported.Render(w, r, http.StatusNotImplemented, "not implemented")
+		updatedPermission, err := g.updatePublicLinkPermission(ctx, permissionID, &itemID, permission)
+		if err != nil {
+			errorcode.RenderError(w, r, err)
+			return
+		}
+		render.Status(r, http.StatusOK)
+		render.JSON(w, r, &updatedPermission)
 		return
 	}
+
 	// This is a user share
 	updatedPermission, err := g.updateUserShare(ctx, permissionID, oldPermission, permission)
 	if err != nil {
diff --git a/services/graph/pkg/service/v0/links.go b/services/graph/pkg/service/v0/links.go
index 035fb1b650f..96b0619ddf1 100644
--- a/services/graph/pkg/service/v0/links.go
+++ b/services/graph/pkg/service/v0/links.go
@@ -14,6 +14,8 @@ import (
 	link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
 	providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
+	"github.com/cs3org/reva/v2/pkg/utils"
+	"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/errorcode"
@@ -55,6 +57,52 @@ func (g Graph) CreateLink(w http.ResponseWriter, r *http.Request) {
 	render.JSON(w, r, *perm)
 }
 
+// SetLinkPassword sets public link password on the cs3 api
+func (g Graph) SetLinkPassword(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	_, 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
+	}
+
+	password := &libregraph.SharingLinkPassword{}
+	if err := StrictJSONUnmarshal(r.Body, password); 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
+	}
+
+	publicShare, err := g.getCS3PublicShareByID(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(publicShare.GetResourceId(), &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
+	}
+
+	newPermission, err := g.updatePublicLinkPassword(ctx, permissionID, password.GetPassword())
+	if err != nil {
+		errorcode.RenderError(w, r, err)
+	}
+
+	render.Status(r, http.StatusOK)
+	render.JSON(w, r, *newPermission)
+}
+
 func (g Graph) createLink(ctx context.Context, driveItemID *providerv1beta1.ResourceId, createLink libregraph.DriveItemCreateLink) (*link.PublicShare, error) {
 	gatewayClient, err := g.gatewaySelector.Next()
 	if err != nil {
@@ -146,6 +194,9 @@ func (g Graph) libreGraphPermissionFromCS3PublicShare(createdLink *link.PublicSh
 	if createdLink.GetExpiration() != nil {
 		perm.SetExpirationDateTime(cs3TimestampToTime(createdLink.GetExpiration()).UTC())
 	}
+
+	perm.SetHasPassword(createdLink.GetPasswordProtected())
+
 	return perm, nil
 }
 
@@ -165,3 +216,129 @@ func parseAndFillUpTime(t *time.Time) *types.Timestamp {
 		Nanos:   uint32(final % 1000000000),
 	}
 }
+
+func (g Graph) updatePublicLinkPassword(ctx context.Context, permissionID string, password string) (*libregraph.Permission, error) {
+	gatewayClient, err := g.gatewaySelector.Next()
+	if err != nil {
+		return nil, err
+	}
+
+	changeLinkRes, err := gatewayClient.UpdatePublicShare(ctx, &link.UpdatePublicShareRequest{
+		Update: &link.UpdatePublicShareRequest_Update{
+			Type: link.UpdatePublicShareRequest_Update_TYPE_PASSWORD,
+			Grant: &link.Grant{
+				Password: password,
+			},
+		},
+		Ref: &link.PublicShareReference{
+			Spec: &link.PublicShareReference_Id{
+				Id: &link.PublicShareId{
+					OpaqueId: permissionID,
+				},
+			},
+		},
+	})
+	if errCode := errorcode.FromCS3Status(changeLinkRes.GetStatus(), err); errCode != nil {
+		return nil, *errCode
+	}
+	permission, err := g.libreGraphPermissionFromCS3PublicShare(changeLinkRes.GetShare())
+	if err != nil {
+		return nil, err
+	}
+	return permission, nil
+}
+
+func (g Graph) updatePublicLinkPermission(ctx context.Context, permissionID string, itemID *providerv1beta1.ResourceId, newPermission *libregraph.Permission) (perm *libregraph.Permission, err error) {
+	gatewayClient, err := g.gatewaySelector.Next()
+	if err != nil {
+		g.logger.Error().Err(err).Msg("could not select next gateway client")
+		return nil, errorcode.New(errorcode.GeneralException, err.Error())
+	}
+
+	statResp, err := gatewayClient.Stat(
+		ctx,
+		&providerv1beta1.StatRequest{
+			Ref: &providerv1beta1.Reference{
+				ResourceId: itemID,
+				Path:       ".",
+			},
+		})
+
+	if errCode := errorcode.FromCS3Status(statResp.GetStatus(), err); errCode != nil {
+		return nil, *errCode
+	}
+
+	if newPermission.HasExpirationDateTime() {
+		expirationDate := newPermission.GetExpirationDateTime()
+		update := &link.UpdatePublicShareRequest_Update{
+			Type:  link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION,
+			Grant: &link.Grant{Expiration: parseAndFillUpTime(&expirationDate)},
+		}
+		perm, err = g.updatePublicLink(ctx, permissionID, update)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if newPermission.HasLink() && newPermission.Link.HasLibreGraphDisplayName() {
+		changedLink := newPermission.GetLink()
+		update := &link.UpdatePublicShareRequest_Update{
+			Type:        link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME,
+			DisplayName: changedLink.GetLibreGraphDisplayName(),
+		}
+		perm, err = g.updatePublicLink(ctx, permissionID, update)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if newPermission.HasLink() && newPermission.Link.HasType() {
+		changedLink := newPermission.Link.GetType()
+		permissions, err := linktype.CS3ResourcePermissionsFromSharingLink(
+			libregraph.DriveItemCreateLink{
+				Type: &changedLink,
+			},
+			statResp.GetInfo().GetType(),
+		)
+		update := &link.UpdatePublicShareRequest_Update{
+			Type: link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS,
+			Grant: &link.Grant{
+				Permissions: &link.PublicSharePermissions{Permissions: permissions},
+			},
+		}
+		perm, err = g.updatePublicLink(ctx, permissionID, update)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return perm, err
+}
+
+func (g Graph) updatePublicLink(ctx context.Context, permissionID string, update *link.UpdatePublicShareRequest_Update) (*libregraph.Permission, error) {
+	gatewayClient, err := g.gatewaySelector.Next()
+	if err != nil {
+		return nil, err
+	}
+
+	changeLinkRes, err := gatewayClient.UpdatePublicShare(ctx, &link.UpdatePublicShareRequest{
+		Update: update,
+		Ref: &link.PublicShareReference{
+			Spec: &link.PublicShareReference_Id{
+				Id: &link.PublicShareId{
+					OpaqueId: permissionID,
+				},
+			},
+		},
+	})
+
+	if errCode := errorcode.FromCS3Status(changeLinkRes.GetStatus(), err); errCode != nil {
+		return nil, *errCode
+	}
+
+	permission, err := g.libreGraphPermissionFromCS3PublicShare(changeLinkRes.GetShare())
+	if err != nil {
+		return nil, err
+	}
+	return permission, nil
+}
diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go
index fd0679bd588..b662ce3bd84 100644
--- a/services/graph/pkg/service/v0/service.go
+++ b/services/graph/pkg/service/v0/service.go
@@ -110,6 +110,7 @@ type Service interface {
 	GetDriveItem(w http.ResponseWriter, r *http.Request)
 	GetDriveItemChildren(w http.ResponseWriter, r *http.Request)
 	CreateLink(w http.ResponseWriter, r *http.Request)
+	SetLinkPassword(writer http.ResponseWriter, request *http.Request)
 
 	Invite(w http.ResponseWriter, r *http.Request)
 	ListPermissions(w http.ResponseWriter, r *http.Request)
@@ -218,6 +219,7 @@ func NewService(opts ...Option) (Graph, error) {
 					r.Route("/{permissionID}", func(r chi.Router) {
 						r.Delete("/", svc.DeletePermission)
 						r.Patch("/", svc.UpdatePermission)
+						r.Post("/setPassword", svc.SetLinkPassword)
 					})
 				})
 				r.Post("/createLink", svc.CreateLink)
diff --git a/services/graph/pkg/validate/libregraph.go b/services/graph/pkg/validate/libregraph.go
index 3cd523d0785..ebfec9724a9 100644
--- a/services/graph/pkg/validate/libregraph.go
+++ b/services/graph/pkg/validate/libregraph.go
@@ -47,6 +47,10 @@ func permission(v *validator.Validate) {
 			sl.ReportError(permission.Id, "Id", "Id", "readonly", "")
 		}
 
+		if _, ok := permission.GetHasPasswordOk(); ok {
+			sl.ReportError(permission.HasPassword, "hasPassword", "HasPassword", "readonly", "")
+		}
+
 		rolesAndActions(sl, permission.Roles, permission.LibreGraphPermissionsActions, true)
 	}, s)
 }


Test patch:
diff --git a/services/graph/pkg/errorcode/cs3_test.go b/services/graph/pkg/errorcode/cs3_test.go
index 22616342e59..13184531ecb 100644
--- a/services/graph/pkg/errorcode/cs3_test.go
+++ b/services/graph/pkg/errorcode/cs3_test.go
@@ -29,7 +29,7 @@ func TestFromCS3Status(t *testing.T) {
 		{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNAUTHENTICATED, Message: "msg"}, nil, nil, conversions.ToPointer(errorcode.New(errorcode.Unauthenticated, "msg"))},
 		{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INVALID_ARGUMENT, Message: "msg"}, nil, nil, conversions.ToPointer(errorcode.New(errorcode.InvalidRequest, "msg"))},
 		{&cs3rpc.Status{Code: cs3rpc.Code_CODE_ALREADY_EXISTS, Message: "msg"}, nil, nil, conversions.ToPointer(errorcode.New(errorcode.NameAlreadyExists, "msg"))},
-		{&cs3rpc.Status{Code: cs3rpc.Code_CODE_FAILED_PRECONDITION, Message: "msg"}, nil, nil, conversions.ToPointer(errorcode.New(errorcode.PreconditionFailed, "msg"))},
+		{&cs3rpc.Status{Code: cs3rpc.Code_CODE_FAILED_PRECONDITION, Message: "msg"}, nil, nil, conversions.ToPointer(errorcode.New(errorcode.InvalidRequest, "msg"))},
 		{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNIMPLEMENTED, Message: "msg"}, nil, nil, conversions.ToPointer(errorcode.New(errorcode.NotSupported, "msg"))},
 		{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INVALID, Message: "msg"}, nil, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
 		{&cs3rpc.Status{Code: cs3rpc.Code_CODE_CANCELLED, Message: "msg"}, nil, nil, conversions.ToPointer(errorcode.New(errorcode.GeneralException, "msg"))},
diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go
index 909d6ed3ff8..0fbca67f9d9 100644
--- a/services/graph/pkg/service/v0/driveitems_test.go
+++ b/services/graph/pkg/service/v0/driveitems_test.go
@@ -22,6 +22,8 @@ import (
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	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/linktype"
 	"github.com/stretchr/testify/mock"
 	"github.com/tidwall/gjson"
 	"google.golang.org/grpc"
@@ -250,12 +252,14 @@ var _ = Describe("Driveitems", func() {
 
 	Describe("UpdatePermission", func() {
 		var (
-			driveItemPermission        *libregraph.Permission
-			getShareMockResponse       *collaboration.GetShareResponse
-			getPublicShareMockResponse *link.GetPublicShareResponse
-			getUserMockResponse        *user.GetUserResponse
-			updateShareMockResponse    *collaboration.UpdateShareResponse
+			driveItemPermission           *libregraph.Permission
+			getShareMockResponse          *collaboration.GetShareResponse
+			getPublicShareMockResponse    *link.GetPublicShareResponse
+			getUserMockResponse           *user.GetUserResponse
+			updateShareMockResponse       *collaboration.UpdateShareResponse
+			updatePublicShareMockResponse *link.UpdatePublicShareResponse
 		)
+		const TestLinkName = "Test Link"
 		BeforeEach(func() {
 			rr = httptest.NewRecorder()
 
@@ -317,6 +321,11 @@ var _ = Describe("Driveitems", func() {
 				Share:  share,
 			}
 
+			updatePublicShareMockResponse = &link.UpdatePublicShareResponse{
+				Status: status.NewOK(ctx),
+				Share:  &link.PublicShare{DisplayName: TestLinkName},
+			}
+
 			getPublicShareMock := gatewayClient.On("GetPublicShare",
 				mock.Anything,
 				mock.MatchedBy(func(req *link.GetPublicShareRequest) bool {
@@ -335,13 +344,39 @@ var _ = Describe("Driveitems", func() {
 						OpaqueId:  "3",
 					},
 					Permissions: &link.PublicSharePermissions{
-						Permissions: roleconversions.NewViewerRole(true).CS3ResourcePermissions(),
+						Permissions: linktype.NewViewLinkPermissionSet().GetPermissions(),
 					},
 					Token: "token",
 				},
 			}
 			getPublicShareMock.Return(getPublicShareMockResponse, nil)
 
+			statMock := gatewayClient.On("Stat",
+				mock.Anything,
+				mock.MatchedBy(func(req *provider.StatRequest) bool {
+					return utils.ResourceIDEqual(
+						req.GetRef().GetResourceId(),
+						&provider.ResourceId{
+							StorageId: "1",
+							SpaceId:   "2",
+							OpaqueId:  "3",
+						},
+					) && req.GetRef().GetPath() == "."
+				}))
+
+			statResponse := &provider.StatResponse{
+				Status: status.NewOK(ctx),
+				Info: &provider.ResourceInfo{
+					Id: &provider.ResourceId{
+						StorageId: "1",
+						SpaceId:   "2",
+						OpaqueId:  "3",
+					},
+					Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER,
+				},
+			}
+
+			statMock.Return(statResponse, nil)
 		})
 		It("fails when no share is found", func() {
 			getShareMockResponse.Share = nil
@@ -358,11 +393,164 @@ var _ = Describe("Driveitems", func() {
 			)
 			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() {
+		It("fails to update permission when no request body is sent", func() {
+			svc.UpdatePermission(
+				rr,
+				httptest.NewRequest(http.MethodPatch, "/", nil).
+					WithContext(ctx),
+			)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+		It("fails to update password when no request body is sent", func() {
+			svc.SetLinkPassword(
+				rr,
+				httptest.NewRequest(http.MethodPatch, "/", nil).
+					WithContext(ctx),
+			)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+		})
+		It("fails to update password when itemID mismatches with the driveID", func() {
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("driveID", "1$2")
+			rctx.URLParams.Add("itemID", "1$4!3")
+			rctx.URLParams.Add("permissionID", "permissionid")
+
+			ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
+			ctx = revactx.ContextSetUser(ctx, currentUser)
+			svc.SetLinkPassword(
+				rr,
+				httptest.NewRequest(http.MethodPatch, "/", nil).
+					WithContext(ctx),
+			)
+			Expect(rr.Code).To(Equal(http.StatusNotFound))
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := libregraph.OdataError{}
+
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(res.Error.Code).To(Equal(errorcode.ItemNotFound.String()))
+			Expect(res.Error.Message).To(Equal("driveID and itemID do not match"))
+		})
+		It("fails to update permission when itemID mismatches with the driveID", func() {
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("driveID", "1$2")
+			rctx.URLParams.Add("itemID", "1$4!3")
+			rctx.URLParams.Add("permissionID", "permissionid")
+
+			ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
+			ctx = revactx.ContextSetUser(ctx, currentUser)
+			svc.UpdatePermission(
+				rr,
+				httptest.NewRequest(http.MethodPatch, "/", nil).
+					WithContext(ctx),
+			)
+			Expect(rr.Code).To(Equal(http.StatusNotFound))
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := libregraph.OdataError{}
+
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(res.Error.Code).To(Equal(errorcode.ItemNotFound.String()))
+			Expect(res.Error.Message).To(Equal("driveID and itemID do not match"))
+		})
+		It("fails to update permission when the resourceID mismatches with the shared resource's id", func() {
 			getShareMockResponse.Share = nil
 			getShareMockResponse.Status = status.NewNotFound(ctx, "not found")
+			getPublicShareMockResponse.Share.ResourceId = &provider.ResourceId{
+				StorageId: "1",
+				SpaceId:   "2",
+				OpaqueId:  "4",
+			}
+
+			driveItemPermission.SetExpirationDateTime(time.Now().Add(time.Hour))
+			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))
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := libregraph.OdataError{}
+
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+		})
+		It("fails to update public link password when the permissionID is not parseable", func() {
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("driveID", "1$2")
+			rctx.URLParams.Add("itemID", "1$2!3")
+			rctx.URLParams.Add("permissionID", "permi%ssionid")
+
+			ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
+			ctx = revactx.ContextSetUser(ctx, currentUser)
+			svc.SetLinkPassword(
+				rr,
+				httptest.NewRequest(http.MethodPatch, "/", nil).
+					WithContext(ctx),
+			)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := libregraph.OdataError{}
+
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(res.Error.Code).To(Equal(errorcode.InvalidRequest.String()))
+			Expect(res.Error.Message).To(Equal("invalid permissionID"))
+		})
+		It("fails to update permission when the permissionID is not parseable", func() {
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("driveID", "1$2")
+			rctx.URLParams.Add("itemID", "1$2!3")
+			rctx.URLParams.Add("permissionID", "permi%ssionid")
+
+			ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
+			ctx = revactx.ContextSetUser(ctx, currentUser)
+			svc.UpdatePermission(
+				rr,
+				httptest.NewRequest(http.MethodPatch, "/", nil).
+					WithContext(ctx),
+			)
+			Expect(rr.Code).To(Equal(http.StatusBadRequest))
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := libregraph.OdataError{}
+
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(res.Error.Code).To(Equal(errorcode.InvalidRequest.String()))
+			Expect(res.Error.Message).To(Equal("invalid permissionID"))
+		})
+		It("succeeds when trying to update a link permission with displayname", func() {
+			getShareMockResponse.Share = nil
+			getShareMockResponse.Status = status.NewNotFound(ctx, "not found")
+
+			updatePublicShareMock := gatewayClient.On("UpdatePublicShare",
+				mock.Anything,
+				mock.MatchedBy(func(req *link.UpdatePublicShareRequest) bool {
+					if req.GetRef().GetId().GetOpaqueId() == "permissionid" {
+						return req.Update.GetDisplayName() == TestLinkName
+					}
+					return false
+				}),
+			)
+
+			updatePublicShareMock.Return(updatePublicShareMockResponse, nil)
 
+			link := libregraph.NewSharingLink()
+			link.SetLibreGraphDisplayName(TestLinkName)
+
+			driveItemPermission.SetLink(*link)
 			body, err := driveItemPermission.MarshalJSON()
 			Expect(err).To(BeNil())
 			svc.UpdatePermission(
@@ -370,7 +558,17 @@ var _ = Describe("Driveitems", func() {
 				httptest.NewRequest(http.MethodPatch, "/", strings.NewReader(string(body))).
 					WithContext(ctx),
 			)
-			Expect(rr.Code).To(Equal(http.StatusNotImplemented))
+			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.Link).ToNot(BeNil())
+			Expect(res.Link.GetLibreGraphDisplayName() == TestLinkName)
 		})
 		It("fails updating the id", func() {
 			driveItemPermission.SetId("permissionid")
@@ -383,6 +581,17 @@ var _ = Describe("Driveitems", func() {
 			)
 			Expect(rr.Code).To(Equal(http.StatusBadRequest))
 		})
+		It("fails updating the password flag", func() {
+			driveItemPermission.SetHasPassword(true)
+			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",
@@ -510,6 +719,153 @@ var _ = Describe("Driveitems", func() {
 			_, ok = res.GetLibreGraphPermissionsActionsOk()
 			Expect(ok).To(BeTrue())
 		})
+		It("updates the expiration date on a public share", func() {
+			getShareMockResponse.Share = nil
+			getShareMockResponse.Status = status.NewNotFound(ctx, "not found")
+
+			expiration := time.Now().UTC().Add(time.Hour)
+			updatePublicShareMock := gatewayClient.On("UpdatePublicShare",
+				mock.Anything,
+				mock.MatchedBy(func(req *link.UpdatePublicShareRequest) bool {
+					return req.GetRef().GetId().GetOpaqueId() == "permissionid"
+				}),
+			)
+
+			updatePublicShareMockResponse.Share.Expiration = utils.TimeToTS(expiration)
+			updatePublicShareMock.Return(updatePublicShareMockResponse, 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("updates the permissions on a public share", func() {
+			getShareMockResponse.Share = nil
+			getShareMockResponse.Status = status.NewNotFound(ctx, "not found")
+
+			newLink := libregraph.NewSharingLink()
+			newLinkType, err := libregraph.NewSharingLinkTypeFromValue("edit")
+			Expect(err).ToNot(HaveOccurred())
+			newLink.SetType(*newLinkType)
+
+			updatePublicShareMock := gatewayClient.On("UpdatePublicShare",
+				mock.Anything,
+				mock.MatchedBy(func(req *link.UpdatePublicShareRequest) bool {
+					return req.GetRef().GetId().GetOpaqueId() == "permissionid"
+				}),
+			)
+
+			updatePublicShareMockResponse.Share.Permissions = &link.PublicSharePermissions{
+				Permissions: linktype.NewFolderEditLinkPermissionSet().Permissions,
+			}
+			updatePublicShareMock.Return(updatePublicShareMockResponse, nil)
+
+			driveItemPermission.SetLink(*newLink)
+			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())
+			linkType := res.Link.GetType()
+			Expect(string(linkType)).To(Equal("edit"))
+		})
+		It("updates the password on a public share", func() {
+			getShareMockResponse.Share = nil
+			getShareMockResponse.Status = status.NewNotFound(ctx, "not found")
+
+			newLinkPassword := libregraph.NewSharingLinkPassword()
+			newLinkPassword.SetPassword("OC123!")
+
+			updatePublicShareMock := gatewayClient.On("UpdatePublicShare",
+				mock.Anything,
+				mock.MatchedBy(func(req *link.UpdatePublicShareRequest) bool {
+					return req.GetRef().GetId().GetOpaqueId() == "permissionid"
+				}),
+			)
+
+			updatePublicShareMockResponse.Share.Permissions = &link.PublicSharePermissions{
+				Permissions: linktype.NewViewLinkPermissionSet().Permissions,
+			}
+			updatePublicShareMockResponse.Share.PasswordProtected = true
+			updatePublicShareMock.Return(updatePublicShareMockResponse, nil)
+
+			body, err := newLinkPassword.MarshalJSON()
+			Expect(err).To(BeNil())
+			svc.SetLinkPassword(
+				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())
+			linkType := res.Link.GetType()
+			Expect(string(linkType)).To(Equal("view"))
+			Expect(*res.HasPassword).To(BeTrue())
+		})
+		It("fails when updating the expiration date on a public share", func() {
+			getShareMockResponse.Share = nil
+			getShareMockResponse.Status = status.NewNotFound(ctx, "not found")
+
+			expiration := time.Now().UTC().AddDate(0, 0, -1)
+			updatePublicShareMock := gatewayClient.On("UpdatePublicShare",
+				mock.Anything,
+				mock.MatchedBy(func(req *link.UpdatePublicShareRequest) bool {
+					return req.GetRef().GetId().GetOpaqueId() == "permissionid"
+				}),
+			)
+
+			updatePublicShareMockResponse.Share = nil
+			updatePublicShareMockResponse.Status = status.NewFailedPrecondition(ctx, nil, "expiration date is in the past")
+			updatePublicShareMock.Return(updatePublicShareMockResponse, 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.StatusBadRequest))
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := libregraph.OdataError{}
+
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(res.GetError().Code).To(Equal(errorcode.InvalidRequest.String()))
+			Expect(res.GetError().Message).To(Equal("expiration date is in the past"))
+		})
 	})
 
 	Describe("Invite", func() {