mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-10-08 20:03:05 -04:00
Merge pull request #3674 from aduffeck/guest-id-canonical-ascii
Use the stored guest id when returning an invite permission
This commit is contained in:
20 files changed
+242
-40
No files matched your search
@@ -66,7 +66,7 @@ require (
|
||||
github.com/open-policy-agent/opa v1.21.1
|
||||
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8
|
||||
github.com/opencloud-eu/reva/v2 v2.51.1-0.20261006112042-7d9dd4c07424
|
||||
github.com/opencloud-eu/reva/v2 v2.51.1-0.20261007142032-7148a82e0e45
|
||||
github.com/opensearch-project/opensearch-go/v4 v4.7.3
|
||||
github.com/orcaman/concurrent-map v1.0.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
|
||||
@@ -761,8 +761,8 @@ github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+l
|
||||
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8 h1:VTkFLkLNna+T4RN6JUZnVQ1bqVAfPtMeORXbFzEYGKs=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU=
|
||||
github.com/opencloud-eu/reva/v2 v2.51.1-0.20261006112042-7d9dd4c07424 h1:rvopdGQTsI0PT7P3MRamrDsO1yYIKJBOxJAbbYmc4P8=
|
||||
github.com/opencloud-eu/reva/v2 v2.51.1-0.20261006112042-7d9dd4c07424/go.mod h1:ngLsDakKnt8zCFCeULhuSZFESGy5NIuSq5/JGeOhPdU=
|
||||
github.com/opencloud-eu/reva/v2 v2.51.1-0.20261007142032-7148a82e0e45 h1:rrZBLsznQDL6YFjtbZF+Z8WCHbGSWn0RsD0ddwaVY8c=
|
||||
github.com/opencloud-eu/reva/v2 v2.51.1-0.20261007142032-7148a82e0e45/go.mod h1:rAwuI3PX8n3RxBXHfpMtAKqfwxTIKhZol1Bi3Qj39p8=
|
||||
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI=
|
||||
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
|
||||
@@ -182,7 +182,8 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto
|
||||
cTime = createShareResponse.GetShare().GetCtime()
|
||||
expiration = createShareResponse.GetShare().GetExpiration()
|
||||
|
||||
guestIdentity, err := cs3UserIdToIdentity(ctx, s.identityCache, createShareRequest.GetGrant().GetGrantee().GetUserId())
|
||||
// the share provider stores the guest's mail address in its canonical form
|
||||
guestIdentity, err := cs3UserIdToIdentity(ctx, s.identityCache, createShareResponse.GetShare().GetGrantee().GetUserId())
|
||||
if err != nil {
|
||||
s.logger.Debug().Err(err).Msg("failed to convert guest user id to identity")
|
||||
return libregraph.Permission{}, err
|
||||
|
||||
@@ -176,7 +176,8 @@ var _ = Describe("DriveItemPermissionsService", func() {
|
||||
{Email: libregraph.PtrString("guest@example.com")},
|
||||
}
|
||||
createShareResponse.Share = &collaboration.Share{
|
||||
Id: &collaboration.ShareId{OpaqueId: "guest123"},
|
||||
Id: &collaboration.ShareId{OpaqueId: "guest123"},
|
||||
Grantee: guestGrantee("guest@example.com"),
|
||||
}
|
||||
|
||||
permission, err := driveItemPermissionsService.Invite(ctx, driveItemId, driveItemInvite)
|
||||
@@ -187,6 +188,23 @@ var _ = Describe("DriveItemPermissionsService", func() {
|
||||
Expect(permission.GrantedToV2.User.GetLibreGraphUserType()).To(Equal("Guest"))
|
||||
})
|
||||
|
||||
It("returns the guest id as stored by the share provider", func() {
|
||||
cfg.Commons = &shared.Commons{EnableGuestLinks: true}
|
||||
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
|
||||
gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil)
|
||||
driveItemInvite.Recipients = []libregraph.DriveRecipient{
|
||||
{Email: libregraph.PtrString("Guest@\u0130nfocorp.com")},
|
||||
}
|
||||
createShareResponse.Share = &collaboration.Share{
|
||||
Id: &collaboration.ShareId{OpaqueId: "guest123"},
|
||||
Grantee: guestGrantee("guest@xn--infocorp-o0e.com"),
|
||||
}
|
||||
|
||||
permission, err := driveItemPermissionsService.Invite(ctx, driveItemId, driveItemInvite)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(permission.GrantedToV2.User.GetId()).To(Equal("guest@xn--infocorp-o0e.com"))
|
||||
})
|
||||
|
||||
It("rejects guest shares when guest invites are disabled by default", func() {
|
||||
driveItemInvite.Recipients = []libregraph.DriveRecipient{
|
||||
{Email: libregraph.PtrString("guest@example.com")},
|
||||
@@ -1477,3 +1495,12 @@ var _ = Describe("DriveItemPermissionsApi", func() {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func guestGrantee(mail string) *provider.Grantee {
|
||||
return &provider.Grantee{
|
||||
Type: provider.GranteeType_GRANTEE_TYPE_USER,
|
||||
Id: &provider.Grantee_UserId{
|
||||
UserId: &userpb.UserId{Type: userpb.UserType_USER_TYPE_GUEST, OpaqueId: mail},
|
||||
},
|
||||
}
|
||||
}
|
||||
vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/usershareprovider/usershareprovider.go
Generated
Vendored
+6
-5
@@ -20,7 +20,6 @@ package usershareprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/mail"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
@@ -184,13 +183,15 @@ func (s *service) CreateShare(ctx context.Context, req *collaboration.CreateShar
|
||||
|
||||
if req.GetGrant().GetGrantee().GetUserId().GetType() == userpb.UserType_USER_TYPE_GUEST {
|
||||
// guests are identified by their mail address, lets be strict here and only accept bare mail addresses
|
||||
// and reject the "Mailbox"-Format that also contains a Display name
|
||||
addr, err := mail.ParseAddress(req.GetGrant().GetGrantee().GetUserId().GetOpaqueId())
|
||||
if err != nil || addr.Name != "" {
|
||||
// and store them in their canonical form
|
||||
guestID := req.GetGrant().GetGrantee().GetUserId()
|
||||
canonical, err := utils.CanonicalMail(guestID.GetOpaqueId())
|
||||
if err != nil {
|
||||
return &collaboration.CreateShareResponse{
|
||||
Status: status.NewInvalidArg(ctx, "invalid mail address for guest grantee"),
|
||||
Status: status.NewInvalidArg(ctx, "invalid mail address for guest grantee: "+err.Error()),
|
||||
}, nil
|
||||
}
|
||||
guestID.OpaqueId = canonical
|
||||
}
|
||||
}
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
|
||||
+4
@@ -271,6 +271,10 @@ func guestUserFromShare(s *collaboration.Share) (*userpb.User, error) {
|
||||
return nil, errors.New("guestlinks: grantee is not a guest user")
|
||||
}
|
||||
|
||||
if !utils.IsASCII(uid.GetOpaqueId()) {
|
||||
return nil, errors.New("guestlinks: grantee opaque id is not ASCII")
|
||||
}
|
||||
|
||||
addr, err := mail.ParseAddress(uid.GetOpaqueId())
|
||||
if err != nil || addr.Name != "" || addr.Address != uid.GetOpaqueId() {
|
||||
return nil, errors.New("guestlinks: grantee opaque id is not a bare email address")
|
||||
|
||||
+15
-6
@@ -183,12 +183,21 @@ type UploadReady struct {
|
||||
SpaceOwner *user.UserId
|
||||
ExecutingUser *user.User
|
||||
ImpersonatingUser *user.User
|
||||
FileRef *provider.Reference
|
||||
ParentID *provider.ResourceId
|
||||
Timestamp *types.Timestamp
|
||||
Failed bool
|
||||
IsVersion bool
|
||||
// add reference here? We could use it to inform client pp is finished
|
||||
// FileRef is a legacy reference to the uploaded file and its shape depends on
|
||||
// the producer: decomposedfs anchors it at the space root
|
||||
// (ResourceId.OpaqueId == SpaceId, with a Path relative to that anchor), while
|
||||
// the posix assimilation emits the file node ID with an empty Path. Neither
|
||||
// OpaqueId nor Path can be relied on universally, so treat Path as optional
|
||||
// and use ResourceID to identify the file.
|
||||
FileRef *provider.Reference
|
||||
ParentID *provider.ResourceId
|
||||
// ResourceID identifies the uploaded file: OpaqueId is the node ID. It mirrors
|
||||
// BytesReceived and is the only file reference every producer sets the same
|
||||
// way, so id based consumers (Graph API, fileid lookups) should use it.
|
||||
ResourceID *provider.ResourceId
|
||||
Timestamp *types.Timestamp
|
||||
Failed bool
|
||||
IsVersion bool
|
||||
}
|
||||
|
||||
// Unmarshal to fulfill umarshaller interface
|
||||
|
||||
+4
@@ -46,6 +46,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/aspects"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/node"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/permissions"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/upload"
|
||||
@@ -105,6 +106,9 @@ func New(o *options.Options, stream events.Stream, cache, historyCache *idcache.
|
||||
if o.FileMetadataCache.Store == "noop" {
|
||||
return nil, fmt.Errorf("the posix driver requires a file metadata cache")
|
||||
}
|
||||
if err := prefixes.SetOcPrefix(o.MetadataPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var err error
|
||||
if log == nil {
|
||||
|
||||
+13
-11
@@ -67,7 +67,7 @@ type queueItem struct {
|
||||
timer *time.Timer
|
||||
}
|
||||
|
||||
const dirtyFlag = prefixes.OcPrefix + "dirty"
|
||||
func dirtyFlag() string { return prefixes.OcPrefix + "dirty" }
|
||||
|
||||
type assimilationNode struct {
|
||||
path string
|
||||
@@ -601,10 +601,11 @@ func (t *Tree) assimilate(item scanItem) error {
|
||||
})
|
||||
} else {
|
||||
t.PublishEvent(events.UploadReady{
|
||||
FileRef: ref,
|
||||
ParentID: parentResourceID,
|
||||
Timestamp: utils.TSNow(),
|
||||
IsVersion: true,
|
||||
FileRef: ref,
|
||||
ParentID: parentResourceID,
|
||||
ResourceID: ref.ResourceId,
|
||||
Timestamp: utils.TSNow(),
|
||||
IsVersion: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -671,10 +672,11 @@ func (t *Tree) assimilate(item scanItem) error {
|
||||
})
|
||||
} else {
|
||||
t.PublishEvent(events.UploadReady{
|
||||
FileRef: ref,
|
||||
ParentID: parentId,
|
||||
Timestamp: utils.TSNow(),
|
||||
IsVersion: false,
|
||||
FileRef: ref,
|
||||
ParentID: parentId,
|
||||
ResourceID: ref.ResourceId,
|
||||
Timestamp: utils.TSNow(),
|
||||
IsVersion: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1088,11 +1090,11 @@ func (t *Tree) propagateSizeDiff(n *node.Node, size int64) error {
|
||||
}
|
||||
|
||||
func (t *Tree) setDirty(path string, dirty bool) error {
|
||||
return xattr.Set(path, dirtyFlag, []byte(fmt.Sprintf("%t", dirty)))
|
||||
return xattr.Set(path, dirtyFlag(), []byte(fmt.Sprintf("%t", dirty)))
|
||||
}
|
||||
|
||||
func (t *Tree) isDirty(path string) (bool, error) {
|
||||
dirtyAttr, err := xattr.Get(path, dirtyFlag)
|
||||
dirtyAttr, err := xattr.Get(path, dirtyFlag())
|
||||
if err != nil {
|
||||
if metadata.IsAttrUnset(err) {
|
||||
return true, nil
|
||||
|
||||
Generated
Vendored
+10
@@ -51,6 +51,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/aspects"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/lookup"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/node"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/options"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/permissions"
|
||||
@@ -147,6 +148,10 @@ func NewDefault(m map[string]interface{}, bs node.Blobstore, es events.Stream, l
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := prefixes.SetOcPrefix(o.MetadataPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lu *lookup.Lookup
|
||||
switch o.MetadataBackend {
|
||||
case "xattrs":
|
||||
@@ -415,6 +420,11 @@ func (fs *Decomposedfs) handlePostprocessingEvent(ctx context.Context, event eve
|
||||
},
|
||||
Path: utils.MakeRelativePath(filepath.Join(session.Dir(), session.Filename())),
|
||||
},
|
||||
ResourceID: &provider.ResourceId{
|
||||
StorageId: session.ProviderID(),
|
||||
SpaceId: session.SpaceID(),
|
||||
OpaqueId: session.NodeID(),
|
||||
},
|
||||
ParentID: parentId,
|
||||
Timestamp: utils.TimeToTS(now),
|
||||
SpaceOwner: n.SpaceOwnerOrManager(ctx),
|
||||
|
||||
Generated
Vendored
+6
-6
@@ -21,7 +21,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes"
|
||||
)
|
||||
|
||||
var _metadataOffloadedAttr = prefixes.OcPrefix + "metadata_offloaded"
|
||||
func metadataOffloadedAttr() string { return prefixes.OcPrefix + "metadata_offloaded" }
|
||||
|
||||
type MetadataPathFunc func(MetadataNode) string
|
||||
|
||||
@@ -72,7 +72,7 @@ func (b HybridBackend) Get(ctx context.Context, n MetadataNode, key string) ([]b
|
||||
|
||||
if isOffloadingAttribute(key) {
|
||||
// check if key is offloaded
|
||||
offloaded, err := xattr.Get(n.InternalPath(), _metadataOffloadedAttr)
|
||||
offloaded, err := xattr.Get(n.InternalPath(), metadataOffloadedAttr())
|
||||
if err == nil && string(offloaded) == "1" {
|
||||
msgpackAttribs := map[string][]byte{}
|
||||
msgBytes, err := os.ReadFile(b.MetadataPath(n))
|
||||
@@ -174,7 +174,7 @@ func (b HybridBackend) getAll(ctx context.Context, n MetadataNode, skipOffloaded
|
||||
}
|
||||
|
||||
// merge the attributes from the offload file
|
||||
offloaded, err := xattr.Get(path, _metadataOffloadedAttr)
|
||||
offloaded, err := xattr.Get(path, metadataOffloadedAttr())
|
||||
if err != nil && !IsAttrUnset(err) {
|
||||
return nil, err
|
||||
}
|
||||
@@ -217,7 +217,7 @@ func (b HybridBackend) SetMultiple(ctx context.Context, n MetadataNode, attribs
|
||||
defer func() { _ = unlock() }()
|
||||
}
|
||||
|
||||
offloadAttr, err := xattr.Get(path, _metadataOffloadedAttr)
|
||||
offloadAttr, err := xattr.Get(path, metadataOffloadedAttr())
|
||||
offloaded := err == nil && string(offloadAttr) == "1"
|
||||
|
||||
// offload if the offloading metadata size exceeds the limit
|
||||
@@ -346,7 +346,7 @@ func (b HybridBackend) offloadMetadata(ctx context.Context, n MetadataNode) erro
|
||||
}
|
||||
|
||||
// set the metadata offloaded attribute
|
||||
err = xattr.Set(path, _metadataOffloadedAttr, []byte("1"))
|
||||
err = xattr.Set(path, metadataOffloadedAttr(), []byte("1"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -376,7 +376,7 @@ func (b HybridBackend) Remove(ctx context.Context, n MetadataNode, key string) e
|
||||
}
|
||||
|
||||
if isOffloadingAttribute(key) {
|
||||
offloadAttr, err := xattr.Get(path, _metadataOffloadedAttr)
|
||||
offloadAttr, err := xattr.Get(path, metadataOffloadedAttr())
|
||||
offloaded := err == nil && string(offloadAttr) == "1"
|
||||
if offloaded {
|
||||
// remove from offloaded metadata
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -24,6 +24,6 @@ package prefixes
|
||||
// manipulate the user. namespace, which is what is used to store decomposedfs
|
||||
// specific metadata. To prevent name collisions with other apps, we are going
|
||||
// to introduce a sub namespace "user.oc."
|
||||
const (
|
||||
var (
|
||||
OcPrefix string = "user.oc."
|
||||
)
|
||||
Generated
Vendored
+1
-1
@@ -23,6 +23,6 @@ package prefixes
|
||||
// On FreeBSD the `user` namespace is implied through a separate syscall argument
|
||||
// and will fail with invalid argument when you try to start an xattr name with user. or system.
|
||||
// For that reason we drop the superfluous user. prefix for FreeBSD specifically.
|
||||
const (
|
||||
var (
|
||||
OcPrefix string = "oc."
|
||||
)
|
||||
Generated
Vendored
+77
-1
@@ -19,6 +19,9 @@
|
||||
package prefixes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/ace"
|
||||
)
|
||||
@@ -31,7 +34,7 @@ import (
|
||||
// we will use to store decomposedfs specific metadata. To prevent name
|
||||
// collisions with other apps We are going to introduce a sub namespace
|
||||
// "user.oc." in the xattrs_prefix*.go files.
|
||||
const (
|
||||
var (
|
||||
TypeAttr string = OcPrefix + "type"
|
||||
IDAttr string = OcPrefix + "id"
|
||||
ParentidAttr string = OcPrefix + "parentid"
|
||||
@@ -111,3 +114,76 @@ func FavoriteKey(uid *userpb.UserId) string {
|
||||
// the favorite flag is specific to the user, so we need to incorporate the userid
|
||||
return FavPrefix + uid.OpaqueId
|
||||
}
|
||||
|
||||
var (
|
||||
defaultOcPrefix = OcPrefix
|
||||
|
||||
mu sync.Mutex
|
||||
fixed bool
|
||||
)
|
||||
|
||||
// SetOcPrefix sets the key prefix for the whole process, empty means the
|
||||
// default. The first call fixes it, a later call with another prefix fails.
|
||||
// It has to run before any key is used.
|
||||
func SetOcPrefix(prefix string) error {
|
||||
if prefix == "" {
|
||||
prefix = defaultOcPrefix
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if fixed {
|
||||
if prefix != OcPrefix {
|
||||
return fmt.Errorf("metadata prefix is already set to %q, cannot change it to %q", OcPrefix, prefix)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fixed = true
|
||||
if prefix != OcPrefix {
|
||||
setKeys(prefix)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setKeys(p string) {
|
||||
OcPrefix = p
|
||||
TypeAttr = p + "type"
|
||||
IDAttr = p + "id"
|
||||
ParentidAttr = p + "parentid"
|
||||
OwnerIDAttr = p + "owner.id"
|
||||
OwnerIDPAttr = p + "owner.idp"
|
||||
OwnerTypeAttr = p + "owner.type"
|
||||
NameAttr = p + "name"
|
||||
BlobIDAttr = p + "blobid"
|
||||
BlobsizeAttr = p + "blobsize"
|
||||
StatusPrefix = p + "nodestatus"
|
||||
ScanStatusPrefix = p + "scanstatus"
|
||||
ScanDatePrefix = p + "scandate"
|
||||
GrantPrefix = p + "grant."
|
||||
GrantUserAcePrefix = p + "grant." + ace.UserAcePrefix
|
||||
GrantGroupAcePrefix = p + "grant." + ace.GroupAcePrefix
|
||||
GrantMailAcePrefix = p + "grant." + ace.MailAcePrefix
|
||||
MetadataPrefix = p + "md."
|
||||
FavPrefix = p + "fav."
|
||||
TmpEtagAttr = p + "tmp.etag"
|
||||
ReferenceAttr = p + "cs3.ref"
|
||||
ChecksumPrefix = p + "cs."
|
||||
TrashOriginAttr = p + "trash.origin"
|
||||
PropagationAttr = p + "propagation"
|
||||
MTimeAttr = p + "mtime"
|
||||
TreeMTimeAttr = p + "tmtime"
|
||||
DTimeAttr = p + "dtime"
|
||||
TreesizeAttr = p + "treesize"
|
||||
QuotaAttr = p + "quota"
|
||||
SpaceIDAttr = p + "space.id"
|
||||
SpaceNameAttr = p + "space.name"
|
||||
SpaceTypeAttr = p + "space.type"
|
||||
SpaceDescriptionAttr = p + "space.description"
|
||||
SpaceReadmeAttr = p + "space.readme"
|
||||
SpaceImageAttr = p + "space.image"
|
||||
SpaceAliasAttr = p + "space.alias"
|
||||
SpaceTenantIDAttr = p + "space.tenantid"
|
||||
SpaceContentTypeAttr = p + "space.contenttype"
|
||||
}
|
||||
Generated
Vendored
+3
@@ -42,6 +42,9 @@ type Options struct {
|
||||
// the metadata backend to use, currently supports `xattr` or `ini`
|
||||
MetadataBackend string `mapstructure:"metadata_backend"`
|
||||
|
||||
// the attribute key prefix of the metadata on disk, process wide, see prefixes.SetOcPrefix
|
||||
MetadataPrefix string `mapstructure:"metadata_prefix"`
|
||||
|
||||
// the propagator to use for this fs. currently only `sync` is fully supported, `async` is available as an experimental feature
|
||||
Propagator string `mapstructure:"propagator"`
|
||||
// Options specific to the async propagator
|
||||
|
||||
Generated
Vendored
+5
@@ -404,6 +404,11 @@ func (fs *Decomposedfs) handlePostprocessingEvent(ctx context.Context, event eve
|
||||
},
|
||||
Path: utils.MakeRelativePath(filepath.Join(session.Dir(), session.Filename())),
|
||||
},
|
||||
ResourceID: &provider.ResourceId{
|
||||
StorageId: session.ProviderID(),
|
||||
SpaceId: session.SpaceID(),
|
||||
OpaqueId: session.NodeID(),
|
||||
},
|
||||
Timestamp: utils.TimeToTS(now),
|
||||
SpaceOwner: n.SpaceOwnerOrManager(ctx),
|
||||
IsVersion: isVersion,
|
||||
|
||||
+1
-2
@@ -2,7 +2,6 @@ package utils
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
@@ -28,7 +27,7 @@ func (id FSSafeUserID) SafeFilename() string {
|
||||
// it lowercase: RFC 5321 does specify that email address local-parts
|
||||
// are case sensitive but, in practice, it's a de-facto standard that
|
||||
// email providers consider them to be case insensitive:
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(strings.ToLower(opaqueID)))
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(LowerASCII(opaqueID)))
|
||||
}
|
||||
return opaqueID
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright 2026 OpenCloud GmbH <mail@opencloud.eu>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
// CanonicalMail returns the canonical form of an email address. The result
|
||||
// is always plain ASCII, so it can't contain look-alike characters, and two
|
||||
// addresses that mail delivery treats the same end up equal:
|
||||
// - only a plain address is accepted (no display name, no quoting)
|
||||
// - the local part must be ASCII
|
||||
// - international domains are converted to punycode
|
||||
// - the result is lowercased
|
||||
func CanonicalMail(email string) (string, error) {
|
||||
addr, err := mail.ParseAddress(email)
|
||||
if err != nil || addr.Name != "" || addr.Address != email {
|
||||
return "", errors.New("invalid email address")
|
||||
}
|
||||
at := strings.LastIndex(email, "@")
|
||||
local, domain := email[:at], email[at+1:]
|
||||
if !IsASCII(local) {
|
||||
return "", errors.New("email addresses with non-ASCII characters before the @ are not supported")
|
||||
}
|
||||
if !IsASCII(domain) {
|
||||
if domain, err = idna.Lookup.ToASCII(domain); err != nil {
|
||||
return "", errors.New("invalid email domain")
|
||||
}
|
||||
}
|
||||
return strings.ToLower(local + "@" + domain), nil
|
||||
}
|
||||
+25
-1
@@ -32,6 +32,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
@@ -240,11 +241,34 @@ func CanonicalUserID(id *userpb.UserId) string {
|
||||
// comparisons: those are email addresses, and while RFC 5321 states
|
||||
// that the local-part is case sensitive, in practice, it's a de facto
|
||||
// standard that email providers consider them to be case insensitive.
|
||||
return strings.ToLower(id.GetOpaqueId())
|
||||
return LowerASCII(id.GetOpaqueId())
|
||||
}
|
||||
return id.GetOpaqueId()
|
||||
}
|
||||
|
||||
// LowerASCII lowercases only the ASCII letters A-Z in s and leaves all other
|
||||
// characters untouched. Unlike strings.ToLower it never maps a non-ASCII
|
||||
// character to an ASCII one (e.g. 'İ' -> 'i' or the Kelvin sign 'K' -> 'k'),
|
||||
// so two different email addresses can't end up with the same canonical form.
|
||||
func LowerASCII(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return r + ('a' - 'A')
|
||||
}
|
||||
return r
|
||||
}, s)
|
||||
}
|
||||
|
||||
// IsASCII returns true if s contains only ASCII characters.
|
||||
func IsASCII(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] >= utf8.RuneSelf {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GroupEqual returns whether two groups have the same field values.
|
||||
func GroupEqual(u, v *grouppb.GroupId) bool {
|
||||
return u != nil && v != nil && u.Idp == v.Idp && u.OpaqueId == v.OpaqueId
|
||||
|
||||
Vendored
+1
-1
@@ -1360,7 +1360,7 @@ github.com/opencloud-eu/icap-client
|
||||
# github.com/opencloud-eu/libre-graph-api-go v1.0.8
|
||||
## explicit; go 1.23
|
||||
github.com/opencloud-eu/libre-graph-api-go
|
||||
# github.com/opencloud-eu/reva/v2 v2.51.1-0.20261006112042-7d9dd4c07424
|
||||
# github.com/opencloud-eu/reva/v2 v2.51.1-0.20261007142032-7148a82e0e45
|
||||
## explicit; go 1.26.0
|
||||
github.com/opencloud-eu/reva/v2/cmd/revad/internal/grace
|
||||
github.com/opencloud-eu/reva/v2/cmd/revad/runtime
|
||||
|
||||
Reference in new issue
Block a user