Merge pull request #2760 from opencloud-eu/feature/guest-links
Persist space memberships in share manager
This commit is contained in:
@@ -64,7 +64,7 @@ require (
|
||||
github.com/open-policy-agent/opa v1.15.2
|
||||
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d
|
||||
github.com/opencloud-eu/reva/v2 v2.44.0
|
||||
github.com/opencloud-eu/reva/v2 v2.44.1-0.20260513122109-583a11875721
|
||||
github.com/opensearch-project/opensearch-go/v4 v4.6.0
|
||||
github.com/orcaman/concurrent-map v1.0.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
@@ -103,7 +103,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.43.0
|
||||
go.opentelemetry.io/otel/trace v1.43.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||
golang.org/x/image v0.38.0
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
@@ -179,7 +179,7 @@ require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/crewjam/httperr v0.2.0 // indirect
|
||||
github.com/crewjam/saml v0.4.14 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.5.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deckarep/golang-set v1.8.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
@@ -204,8 +204,8 @@ require (
|
||||
github.com/go-acme/lego/v4 v4.4.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.18.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.19.0 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
@@ -258,7 +258,7 @@ require (
|
||||
github.com/juliangruber/go-intersect v1.1.0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
|
||||
github.com/kovidgoyal/go-shm v1.0.0 // indirect
|
||||
@@ -321,7 +321,7 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.15 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/pquerna/cachecontrol v0.2.0 // indirect
|
||||
|
||||
@@ -267,8 +267,8 @@ github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1n
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20260424072047-8d9ef7076ae9 h1:8WtMb7TKKx1AJM3j+uR+H25y4v+b8o8GIQg/2ooCyRo=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20260424072047-8d9ef7076ae9/go.mod h1:DedpcqXl193qF/08Y04IO0PpxyyMu8+GrkD6kWK2MEQ=
|
||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -382,12 +382,12 @@ github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
|
||||
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
|
||||
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM=
|
||||
github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
||||
github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc=
|
||||
github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -722,8 +722,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
github.com/kobergj/gowebdav v0.0.0-20250102091030-aa65266db202 h1:A1xJ2NKgiYFiaHiLl9B5yw/gUBACSs9crDykTS3GuQI=
|
||||
@@ -952,8 +952,8 @@ github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9 h1:dIft
|
||||
github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9/go.mod h1:JWyDC6H+5oZRdUJUgKuaye+8Ph5hEs6HVzVoPKzWSGI=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d h1:JcqGDiyrcaQwVyV861TUyQgO7uEmsjkhfm7aQd84dOw=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
|
||||
github.com/opencloud-eu/reva/v2 v2.44.0 h1:5SZRIjFzyc4e20dCJQbeZ15Lo7COvrSQ1jgjFVQWmRE=
|
||||
github.com/opencloud-eu/reva/v2 v2.44.0/go.mod h1:ZYjHYcg3rLInH81yNnC5GgS4I0JS9KRFVwxJptw1/CM=
|
||||
github.com/opencloud-eu/reva/v2 v2.44.1-0.20260513122109-583a11875721 h1:PH9Ia0HwdvpfaThYCid2atc5y+uwn4EJxvJU6L9wU6M=
|
||||
github.com/opencloud-eu/reva/v2 v2.44.1-0.20260513122109-583a11875721/go.mod h1:tUL2X47YxLHrnBDArHrIP73UJliMI0PaY/3tPs31dTM=
|
||||
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=
|
||||
@@ -988,8 +988,8 @@ github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJ
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
|
||||
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -1374,8 +1374,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
|
||||
|
||||
@@ -85,12 +85,16 @@ func cleanup(_ *cobra.Command, cfg *config.Config) error {
|
||||
return configlog.ReturnError(errors.New("cleanup is only implemented for the jsoncs3 share manager"))
|
||||
}
|
||||
|
||||
l := logger()
|
||||
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
|
||||
rcfg := revaShareConfig(cfg.Sharing)
|
||||
f, ok := registry.NewFuncs[driver]
|
||||
if !ok {
|
||||
return configlog.ReturnError(errors.New("Unknown share manager type '" + driver + "'"))
|
||||
}
|
||||
mgr, err := f(rcfg[driver].(map[string]any))
|
||||
mgr, err := f(rcfg[driver].(map[string]any), l)
|
||||
if err != nil {
|
||||
return configlog.ReturnError(err)
|
||||
}
|
||||
@@ -115,10 +119,6 @@ func cleanup(_ *cobra.Command, cfg *config.Config) error {
|
||||
if err != nil {
|
||||
return configlog.ReturnError(err)
|
||||
}
|
||||
|
||||
l := logger()
|
||||
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
serviceUserCtx = l.WithContext(serviceUserCtx)
|
||||
|
||||
mgr.(*jsoncs3.Manager).CleanupStaleShares(serviceUserCtx)
|
||||
|
||||
@@ -272,6 +272,9 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
|
||||
Activitylog: Activitylog{
|
||||
ServiceAccount: serviceAccount,
|
||||
},
|
||||
Sharing: Sharing{
|
||||
ServiceAccount: serviceAccount,
|
||||
},
|
||||
}
|
||||
|
||||
if insecure {
|
||||
|
||||
@@ -204,7 +204,8 @@ type SettingsService struct {
|
||||
|
||||
// Sharing is the configuration for the sharing service
|
||||
type Sharing struct {
|
||||
Events Events
|
||||
Events Events
|
||||
ServiceAccount ServiceAccount `yaml:"service_account"`
|
||||
}
|
||||
|
||||
// StorageRegistry is the configuration for the storage registry
|
||||
|
||||
@@ -51,7 +51,8 @@ func GatewayConfigFromStruct(cfg *config.Config, logger log.Logger) map[string]a
|
||||
"ocminvitemanagersvc": cfg.OCMEndpoint,
|
||||
"ocmproviderauthorizersvc": cfg.OCMEndpoint,
|
||||
"ocmcoresvc": cfg.OCMEndpoint,
|
||||
"commit_share_to_storage_grant": cfg.CommitShareToStorageGrant,
|
||||
"use_common_space_root_share_logic": true,
|
||||
"commit_share_to_storage_grant": cfg.CommitShareToStorageGrant,
|
||||
"share_folder": cfg.ShareFolder, // ShareFolder is the location where to create shares in the recipient's storage provider.
|
||||
// other
|
||||
"disable_home_creation_on_login": cfg.DisableHomeCreationOnLogin,
|
||||
|
||||
@@ -251,10 +251,6 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto
|
||||
|
||||
if shareid != "" {
|
||||
permission.Id = conversions.ToPointer(shareid)
|
||||
} else if IsSpaceRoot(statResponse.GetInfo().GetId()) {
|
||||
// permissions on a space root are not handled by a share manager so
|
||||
// they don't get a share-id
|
||||
permission.SetId(identitySetToSpacePermissionID(permission.GetGrantedToV2()))
|
||||
}
|
||||
|
||||
if expiration != nil {
|
||||
@@ -420,12 +416,12 @@ func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID
|
||||
var permissionsCount int
|
||||
|
||||
if IsSpaceRoot(statResponse.GetInfo().GetId()) {
|
||||
var permissions []libregraph.Permission
|
||||
permissions, permissionsCount, err = s.getSpaceRootPermissions(ctx, statResponse.GetInfo().GetSpace().GetId(), queryOptions.NoValues)
|
||||
driveItems, err = s.listSpaceRootUserShares(ctx, []*collaboration.Filter{
|
||||
share.ResourceIDFilter(itemID),
|
||||
}, driveItems)
|
||||
if err != nil {
|
||||
return collectionOfPermissions, err
|
||||
}
|
||||
collectionOfPermissions.Value = permissions
|
||||
} else {
|
||||
// "normal" driveItem, populate user permissions via share providers
|
||||
driveItems, err = s.listUserShares(ctx, []*collaboration.Filter{
|
||||
@@ -541,12 +537,10 @@ func (s DriveItemPermissionsService) DeletePermission(ctx context.Context, itemI
|
||||
}
|
||||
|
||||
switch permissionType {
|
||||
case User:
|
||||
case User, Space:
|
||||
return s.removeUserShare(ctx, permissionID)
|
||||
case Public:
|
||||
return s.removePublicShare(ctx, permissionID)
|
||||
case Space:
|
||||
return s.removeSpacePermission(ctx, permissionID, sharedResourceID)
|
||||
case OCM:
|
||||
return s.removeOCMPermission(ctx, permissionID)
|
||||
default:
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/tidwall/gjson"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
roleconversions "github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
@@ -391,6 +390,25 @@ var _ = Describe("DriveItemPermissionsService", func() {
|
||||
Expect(len(permissions.LibreGraphPermissionsActionsAllowedValues)).ToNot(BeZero())
|
||||
Expect(len(permissions.LibreGraphPermissionsRolesAllowedValues)).ToNot(BeZero())
|
||||
})
|
||||
It("sends SpaceRootFilter(false) when listing shares for a non-space-root item", func() {
|
||||
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
|
||||
gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(listPublicSharesResponse, nil)
|
||||
gatewayClient.On("ListShares",
|
||||
mock.Anything,
|
||||
mock.MatchedBy(func(req *collaboration.ListSharesRequest) bool {
|
||||
for _, f := range req.Filters {
|
||||
if f.Type == collaboration.Filter_TYPE_SPACE_ROOT && !f.GetSpaceRoot() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}),
|
||||
).Return(listSharesResponse, nil)
|
||||
|
||||
_, err := driveItemPermissionsService.ListPermissions(context.Background(), itemID, svc.ListPermissionsQueryOptions{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
gatewayClient.AssertExpectations(GinkgoT())
|
||||
})
|
||||
It("returns one permission per share", func() {
|
||||
statResponse.Info.PermissionSet = roleconversions.NewEditorRole().CS3ResourcePermissions()
|
||||
listSharesResponse.Shares = []*collaboration.Share{
|
||||
@@ -442,6 +460,8 @@ var _ = Describe("DriveItemPermissionsService", func() {
|
||||
})
|
||||
It("returns role denied", func() {
|
||||
statResponse.Info.PermissionSet = roleconversions.NewManagerRole().CS3ResourcePermissions()
|
||||
statResponse.Info.Type = provider.ResourceType_RESOURCE_TYPE_CONTAINER
|
||||
|
||||
listSharesResponse.Shares = []*collaboration.Share{
|
||||
{
|
||||
Id: &collaboration.ShareId{OpaqueId: "1"},
|
||||
@@ -666,6 +686,7 @@ var _ = Describe("DriveItemPermissionsService", func() {
|
||||
}
|
||||
|
||||
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listSpacesResponse, nil)
|
||||
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(&collaboration.ListSharesResponse{Status: status.NewOK(ctx)}, nil)
|
||||
gatewayClient.On("ListPublicShares", mock.Anything, mock.Anything).Return(listPublicSharesResponse, nil)
|
||||
statResponse.Info.Id = listSpacesResponse.StorageSpaces[0].Root
|
||||
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
|
||||
@@ -765,12 +786,10 @@ var _ = Describe("DriveItemPermissionsService", func() {
|
||||
|
||||
gatewayClient.On("RemoveShare",
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
).Return(func(ctx context.Context, in *collaboration.RemoveShareRequest, opts ...grpc.CallOption) (*collaboration.RemoveShareResponse, error) {
|
||||
Expect(in.Ref.GetKey()).ToNot(BeNil())
|
||||
Expect(in.Ref.GetKey().GetGrantee().GetUserId().GetOpaqueId()).To(Equal("userid"))
|
||||
return &collaboration.RemoveShareResponse{Status: status.NewOK(ctx)}, nil
|
||||
})
|
||||
mock.MatchedBy(func(req *collaboration.RemoveShareRequest) bool {
|
||||
return req.GetRef().GetId().GetOpaqueId() == "shareid"
|
||||
}),
|
||||
).Return(&collaboration.RemoveShareResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
err := driveItemPermissionsService.DeletePermission(context.Background(),
|
||||
&provider.ResourceId{
|
||||
@@ -778,7 +797,7 @@ var _ = Describe("DriveItemPermissionsService", func() {
|
||||
SpaceId: "2",
|
||||
OpaqueId: "2",
|
||||
},
|
||||
"u:userid",
|
||||
"shareid",
|
||||
)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
@@ -1045,24 +1064,40 @@ var _ = Describe("DriveItemPermissionsService", func() {
|
||||
Expect(res).To(BeZero())
|
||||
})
|
||||
It("fails to update the space permissions for a space share when setting a file specific role", func() {
|
||||
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
|
||||
getPublicShareMockResponse.Share = nil
|
||||
getPublicShareMockResponse.Status = status.NewNotFound(ctx, "not found")
|
||||
gatewayClient.On("GetPublicShare", mock.Anything, mock.Anything).Return(getPublicShareMockResponse, nil)
|
||||
|
||||
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listSpacesResponse, nil)
|
||||
|
||||
statResponse.Info.Id = listSpacesResponse.StorageSpaces[0].Root
|
||||
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
|
||||
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
|
||||
|
||||
driveItemPermission.SetRoles([]string{unifiedrole.UnifiedRoleFileEditorID})
|
||||
spaceId := &provider.ResourceId{
|
||||
StorageId: "1",
|
||||
SpaceId: "2",
|
||||
OpaqueId: "2",
|
||||
}
|
||||
res, err := driveItemPermissionsService.UpdatePermission(ctx, spaceId, "u:userid", driveItemPermission)
|
||||
statResponse.Info.Id = spaceId
|
||||
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
|
||||
|
||||
gatewayClient.On("ListShares", mock.Anything, mock.Anything).Return(&collaboration.ListSharesResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Shares: []*collaboration.Share{
|
||||
{
|
||||
Id: &collaboration.ShareId{OpaqueId: "spaceShareId"},
|
||||
ResourceId: spaceId,
|
||||
Grantee: &provider.Grantee{
|
||||
Type: provider.GranteeType_GRANTEE_TYPE_USER,
|
||||
Id: &provider.Grantee_UserId{
|
||||
UserId: &userpb.UserId{OpaqueId: "userid"},
|
||||
},
|
||||
},
|
||||
Permissions: &collaboration.SharePermissions{
|
||||
Permissions: roleconversions.NewSpaceViewerRole().CS3ResourcePermissions(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
|
||||
|
||||
driveItemPermission.SetRoles([]string{unifiedrole.UnifiedRoleFileEditorID})
|
||||
res, err := driveItemPermissionsService.UpdatePermission(ctx, spaceId, "spaceShareId", driveItemPermission)
|
||||
Expect(err).To(MatchError(errorcode.New(errorcode.InvalidRequest, "role not applicable to this resource")))
|
||||
Expect(res).To(BeZero())
|
||||
})
|
||||
|
||||
@@ -52,22 +52,6 @@ type BaseGraphService struct {
|
||||
publicBaseURL *url.URL
|
||||
}
|
||||
|
||||
func (g BaseGraphService) getSpaceRootPermissions(ctx context.Context, spaceID *storageprovider.StorageSpaceId, countOnly bool) ([]libregraph.Permission, int, error) {
|
||||
gatewayClient, err := g.gatewaySelector.Next()
|
||||
|
||||
if err != nil {
|
||||
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
|
||||
return nil, 0, err
|
||||
}
|
||||
space, err := utils.GetSpace(ctx, spaceID.GetOpaqueId(), gatewayClient)
|
||||
if err != nil {
|
||||
return nil, 0, errorcode.FromUtilsStatusCodeError(err)
|
||||
}
|
||||
|
||||
perm, count := g.cs3SpacePermissionsToLibreGraph(ctx, space, countOnly, APIVersion_1_Beta_1)
|
||||
return perm, count, nil
|
||||
}
|
||||
|
||||
func (g BaseGraphService) getDriveItem(ctx context.Context, ref *storageprovider.Reference) (*libregraph.DriveItem, error) {
|
||||
gatewayClient, err := g.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
@@ -270,6 +254,17 @@ func (g BaseGraphService) libreGraphPermissionFromCS3PublicShare(createdLink *li
|
||||
}
|
||||
|
||||
func (g BaseGraphService) listUserShares(ctx context.Context, filters []*collaboration.Filter, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
|
||||
return g.listSharesWithSpaceRootFilter(ctx, false, filters, driveItems)
|
||||
}
|
||||
|
||||
// listSpaceRootUserShares lists user/group shares whose resource is a space root (i.e. space
|
||||
// memberships). It uses SpaceRootFilter(true) so only space-root shares are returned, mirroring
|
||||
// how listUserShares works for regular file/folder shares.
|
||||
func (g BaseGraphService) listSpaceRootUserShares(ctx context.Context, filters []*collaboration.Filter, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
|
||||
return g.listSharesWithSpaceRootFilter(ctx, true, filters, driveItems)
|
||||
}
|
||||
|
||||
func (g BaseGraphService) listSharesWithSpaceRootFilter(ctx context.Context, spaceRoot bool, filters []*collaboration.Filter, driveItems driveItemsByResourceID) (driveItemsByResourceID, error) {
|
||||
gatewayClient, err := g.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
g.logger.Error().Err(err).Msg("could not select next gateway client")
|
||||
@@ -279,6 +274,7 @@ func (g BaseGraphService) listUserShares(ctx context.Context, filters []*collabo
|
||||
concreteFilters := []*collaboration.Filter{
|
||||
share.UserGranteeFilter(),
|
||||
share.GroupGranteeFilter(),
|
||||
share.SpaceRootFilter(spaceRoot),
|
||||
}
|
||||
concreteFilters = append(concreteFilters, filters...)
|
||||
|
||||
@@ -563,9 +559,7 @@ func (g BaseGraphService) cs3OCMSharesToDriveItems(ctx context.Context, shares [
|
||||
func (g BaseGraphService) cs3UserShareToPermission(ctx context.Context, share *collaboration.Share, roleCondition string) (*libregraph.Permission, error) {
|
||||
perm := libregraph.Permission{}
|
||||
perm.SetRoles([]string{})
|
||||
if roleCondition != unifiedrole.UnifiedRoleConditionDrive {
|
||||
perm.SetId(share.GetId().GetOpaqueId())
|
||||
}
|
||||
perm.SetId(share.GetId().GetOpaqueId())
|
||||
grantedTo := libregraph.SharePointIdentitySet{}
|
||||
switch share.GetGrantee().GetType() {
|
||||
case storageprovider.GranteeType_GRANTEE_TYPE_USER:
|
||||
@@ -579,9 +573,6 @@ func (g BaseGraphService) cs3UserShareToPermission(ctx context.Context, share *c
|
||||
return nil, errorcode.New(errorcode.GeneralException, err.Error())
|
||||
default:
|
||||
grantedTo.SetUser(user)
|
||||
if roleCondition == unifiedrole.UnifiedRoleConditionDrive {
|
||||
perm.SetId("u:" + user.GetId())
|
||||
}
|
||||
}
|
||||
case storageprovider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
group, err := groupIdToIdentity(ctx, g.identityCache, share.Grantee.GetGroupId().GetOpaqueId())
|
||||
@@ -594,9 +585,6 @@ func (g BaseGraphService) cs3UserShareToPermission(ctx context.Context, share *c
|
||||
return nil, errorcode.New(errorcode.GeneralException, err.Error())
|
||||
default:
|
||||
grantedTo.SetGroup(group)
|
||||
if roleCondition == unifiedrole.UnifiedRoleConditionDrive {
|
||||
perm.SetId("g:" + group.GetId())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,35 +915,6 @@ func (g BaseGraphService) removeUserShare(ctx context.Context, permissionID stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g BaseGraphService) removeSpacePermission(ctx context.Context, permissionID string, resourceId *storageprovider.ResourceId) error {
|
||||
grantee, err := spacePermissionIdToCS3Grantee(permissionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gatewayClient, err := g.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
g.logger.Debug().Err(err).Msg("selecting gatewaySelector failed")
|
||||
return err
|
||||
}
|
||||
removeShareResp, err := gatewayClient.RemoveShare(ctx, &collaboration.RemoveShareRequest{
|
||||
Ref: &collaboration.ShareReference{
|
||||
Spec: &collaboration.ShareReference_Key{
|
||||
Key: &collaboration.ShareKey{
|
||||
ResourceId: resourceId,
|
||||
Grantee: &grantee,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err := errorcode.FromCS3Status(removeShareResp.GetStatus(), err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We need to return an untyped nil here otherwise the error==nil check won't work
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g BaseGraphService) getOCMPermissionResourceID(ctx context.Context, permissionID string) (*storageprovider.ResourceId, error) {
|
||||
cs3Share, err := g.getCS3OCMShareByID(ctx, permissionID)
|
||||
if err != nil {
|
||||
@@ -1071,20 +1030,19 @@ func (g BaseGraphService) getPermissionByID(ctx context.Context, permissionID st
|
||||
}
|
||||
return permission, publicShare.GetResourceId(), nil
|
||||
case IsSpaceRoot(itemID):
|
||||
// itemID is referencing a spaceroot this is a space permission. Handle
|
||||
// that here and get space id
|
||||
resourceInfo, err := utils.GetResourceByID(ctx, itemID, gatewayClient)
|
||||
// itemID is referencing a space root — use the share manager to look up the permission
|
||||
driveItems := make(driveItemsByResourceID)
|
||||
driveItems, err = g.listSpaceRootUserShares(ctx, []*collaboration.Filter{
|
||||
share.ResourceIDFilter(itemID),
|
||||
}, driveItems)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
perms, _, err := g.getSpaceRootPermissions(ctx, resourceInfo.GetSpace().GetId(), false)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, p := range perms {
|
||||
if p.GetId() == permissionID {
|
||||
return &p, itemID, nil
|
||||
for _, item := range driveItems {
|
||||
for i := range item.Permissions {
|
||||
if item.Permissions[i].GetId() == permissionID {
|
||||
return &item.Permissions[i], itemID, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
case errors.As(err, &errcode) && errcode.GetCode() == errorcode.ItemNotFound:
|
||||
@@ -1239,29 +1197,10 @@ func (g BaseGraphService) updateUserShare(ctx context.Context, permissionID stri
|
||||
}
|
||||
|
||||
var cs3UpdateShareReq collaboration.UpdateShareRequest
|
||||
// When updating a space root we need to reference the share by resourceId and grantee
|
||||
if IsSpaceRoot(itemID) {
|
||||
grantee, err := spacePermissionIdToCS3Grantee(permissionID)
|
||||
if err != nil {
|
||||
g.logger.Debug().Err(err).Str("permissionid", permissionID).Msg("failed to parse space permission id")
|
||||
return nil, err
|
||||
}
|
||||
cs3UpdateShareReq.Share = &collaboration.Share{
|
||||
ResourceId: itemID,
|
||||
Grantee: &grantee,
|
||||
}
|
||||
cs3UpdateShareReq.Opaque = &types.Opaque{
|
||||
Map: map[string]*types.OpaqueEntry{
|
||||
"spacegrant": {},
|
||||
},
|
||||
}
|
||||
cs3UpdateShareReq.Opaque = utils.AppendPlainToOpaque(cs3UpdateShareReq.Opaque, "spacetype", _spaceTypeProject)
|
||||
} else {
|
||||
cs3UpdateShareReq.Share = &collaboration.Share{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: permissionID,
|
||||
},
|
||||
}
|
||||
cs3UpdateShareReq.Share = &collaboration.Share{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: permissionID,
|
||||
},
|
||||
}
|
||||
fieldmask := []string{}
|
||||
if expiration, ok := newPermission.GetExpirationDateTimeOk(); ok {
|
||||
|
||||
@@ -15,8 +15,6 @@ import (
|
||||
"time"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
@@ -349,35 +347,6 @@ func (g Graph) GetDriveItemChildren(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, &ListResponse{Value: files})
|
||||
}
|
||||
|
||||
func spacePermissionIdToCS3Grantee(permissionID string) (storageprovider.Grantee, error) {
|
||||
// the permission ID for space permission is made of two parts
|
||||
// the grantee type ('u' or user, 'g' for group) and the user or group id
|
||||
var grantee storageprovider.Grantee
|
||||
parts := strings.SplitN(permissionID, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return grantee, errorcode.New(errorcode.InvalidRequest, "invalid space permission id")
|
||||
}
|
||||
switch parts[0] {
|
||||
case "u":
|
||||
grantee.Type = storageprovider.GranteeType_GRANTEE_TYPE_USER
|
||||
grantee.Id = &storageprovider.Grantee_UserId{
|
||||
UserId: &userpb.UserId{
|
||||
OpaqueId: parts[1],
|
||||
},
|
||||
}
|
||||
case "g":
|
||||
grantee.Type = storageprovider.GranteeType_GRANTEE_TYPE_GROUP
|
||||
grantee.Id = &storageprovider.Grantee_GroupId{
|
||||
GroupId: &grouppb.GroupId{
|
||||
OpaqueId: parts[1],
|
||||
},
|
||||
}
|
||||
default:
|
||||
return grantee, errorcode.New(errorcode.InvalidRequest, "invalid space permission id")
|
||||
}
|
||||
return grantee, nil
|
||||
}
|
||||
|
||||
func (g Graph) getRemoteItem(ctx context.Context, root *storageprovider.ResourceId, baseURL *url.URL) (*libregraph.RemoteItem, error) {
|
||||
gatewayClient, err := g.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
@@ -465,14 +434,22 @@ func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *sto
|
||||
parentRef.SetPath(path.Dir(res.GetPath()))
|
||||
driveItem.ParentReference = parentRef
|
||||
}
|
||||
if res.GetType() == storageprovider.ResourceType_RESOURCE_TYPE_FILE && res.GetMimeType() != "" {
|
||||
switch res.GetType() {
|
||||
case storageprovider.ResourceType_RESOURCE_TYPE_FILE:
|
||||
mimeType := res.GetMimeType()
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
// We cannot use a libregraph.File here because the openapi codegenerator autodetects 'File' as a go type ...
|
||||
driveItem.File = &libregraph.OpenGraphFile{
|
||||
MimeType: &res.MimeType,
|
||||
MimeType: &mimeType,
|
||||
}
|
||||
case storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER:
|
||||
if IsSpaceRoot(res.GetId()) {
|
||||
driveItem.SetRoot(map[string]any{})
|
||||
} else {
|
||||
driveItem.SetFolder(libregraph.Folder{})
|
||||
}
|
||||
}
|
||||
if res.GetType() == storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
driveItem.Folder = &libregraph.Folder{}
|
||||
}
|
||||
|
||||
if res.GetArbitraryMetadata() != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
|
||||
"github.com/go-chi/render"
|
||||
libregraph "github.com/opencloud-eu/libre-graph-api-go"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
|
||||
"github.com/opencloud-eu/opencloud/services/thumbnails/pkg/thumbnail"
|
||||
@@ -41,7 +42,11 @@ func (g Graph) listSharedWithMe(ctx context.Context, expandThumbnails bool) ([]l
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listReceivedSharesResponse, err := gatewayClient.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{})
|
||||
listReceivedSharesResponse, err := gatewayClient.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{
|
||||
Filters: []*collaboration.Filter{
|
||||
share.SpaceRootFilter(false),
|
||||
},
|
||||
})
|
||||
if err := errorcode.FromCS3Status(listReceivedSharesResponse.GetStatus(), err); err != nil {
|
||||
g.logger.Error().Err(err).Msg("listing shares failed")
|
||||
return nil, err
|
||||
|
||||
@@ -18,7 +18,8 @@ type Config struct {
|
||||
Reva *shared.Reva `yaml:"reva"`
|
||||
Events Events `yaml:"events"`
|
||||
|
||||
SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"SHARING_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the loading of user's group memberships from the reva access token." introductionVersion:"1.0.0"`
|
||||
ServiceAccount ServiceAccount `yaml:"service_account"`
|
||||
SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"SHARING_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the loading of user's group memberships from the reva access token." introductionVersion:"1.0.0"`
|
||||
|
||||
UserSharingDriver string `yaml:"user_sharing_driver" env:"SHARING_USER_DRIVER" desc:"Driver to be used to persist shares. Supported values are 'jsoncs3', 'json', 'cs3' (deprecated) and 'owncloudsql'." introductionVersion:"1.0.0"`
|
||||
UserSharingDrivers UserSharingDrivers `yaml:"user_sharing_drivers"`
|
||||
@@ -100,6 +101,13 @@ type UserSharingJSONCS3Driver struct {
|
||||
CacheTTL int `yaml:"cache_ttl" env:"SHARING_USER_JSONCS3_CACHE_TTL" desc:"TTL for the internal caches in seconds." introductionVersion:"1.0.0"`
|
||||
MaxConcurrency int `yaml:"max_concurrency" env:"OC_MAX_CONCURRENCY;SHARING_USER_JSONCS3_MAX_CONCURRENCY" desc:"Maximum number of concurrent go-routines. Higher values can potentially get work done faster but will also cause more load on the system. Values of 0 or below will be ignored and the default value will be used." introductionVersion:"1.0.0"`
|
||||
}
|
||||
|
||||
// ServiceAccount is the configuration for the used service account
|
||||
type ServiceAccount struct {
|
||||
ServiceAccountID string `yaml:"service_account_id" env:"OC_SERVICE_ACCOUNT_ID;SHARING_SERVICE_ACCOUNT" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details." introductionVersion:"%%NEXT%%"`
|
||||
ServiceAccountSecret string `yaml:"service_account_secret" env:"OC_SERVICE_ACCOUNT_SECRET;SHARING_SERVICE_ACCOUNT_SECRET" desc:"The service account secret." introductionVersion:"%%NEXT%%"`
|
||||
}
|
||||
|
||||
type PublicSharingDrivers struct {
|
||||
JSON PublicSharingJSONDriver `yaml:"json"`
|
||||
JSONCS3 PublicSharingJSONCS3Driver `yaml:"jsoncs3"`
|
||||
|
||||
@@ -58,5 +58,13 @@ func Validate(cfg *config.Config) error {
|
||||
return shared.MissingSystemUserID(cfg.Service.Name)
|
||||
}
|
||||
|
||||
if cfg.ServiceAccount.ServiceAccountID == "" {
|
||||
return shared.MissingServiceAccountID(cfg.Service.Name)
|
||||
}
|
||||
|
||||
if cfg.ServiceAccount.ServiceAccountSecret == "" {
|
||||
return shared.MissingServiceAccountSecret(cfg.Service.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -71,13 +71,15 @@ func SharingConfigFromStruct(cfg *config.Config, logger log.Logger) (map[string]
|
||||
"machine_auth_apikey": cfg.UserSharingDrivers.CS3.SystemUserAPIKey,
|
||||
},
|
||||
"jsoncs3": map[string]any{
|
||||
"gateway_addr": cfg.Reva.Address,
|
||||
"provider_addr": cfg.UserSharingDrivers.JSONCS3.ProviderAddr,
|
||||
"service_user_id": cfg.UserSharingDrivers.JSONCS3.SystemUserID,
|
||||
"service_user_idp": cfg.UserSharingDrivers.JSONCS3.SystemUserIDP,
|
||||
"machine_auth_apikey": cfg.UserSharingDrivers.JSONCS3.SystemUserAPIKey,
|
||||
"ttl": cfg.UserSharingDrivers.JSONCS3.CacheTTL,
|
||||
"max_concurrency": cfg.UserSharingDrivers.JSONCS3.MaxConcurrency,
|
||||
"gateway_addr": cfg.Reva.Address,
|
||||
"provider_addr": cfg.UserSharingDrivers.JSONCS3.ProviderAddr,
|
||||
"system_user_id": cfg.UserSharingDrivers.JSONCS3.SystemUserID,
|
||||
"system_user_idp": cfg.UserSharingDrivers.JSONCS3.SystemUserIDP,
|
||||
"machine_auth_apikey": cfg.UserSharingDrivers.JSONCS3.SystemUserAPIKey,
|
||||
"ttl": cfg.UserSharingDrivers.JSONCS3.CacheTTL,
|
||||
"max_concurrency": cfg.UserSharingDrivers.JSONCS3.MaxConcurrency,
|
||||
"service_account_id": cfg.ServiceAccount.ServiceAccountID,
|
||||
"service_account_secret": cfg.ServiceAccount.ServiceAccountSecret,
|
||||
"events": map[string]any{
|
||||
"natsaddress": cfg.Events.Addr,
|
||||
"natsclusterid": cfg.Events.ClusterID,
|
||||
|
||||
@@ -420,7 +420,7 @@ class SharingNgContext implements Context {
|
||||
$expirationDateTime = (\array_key_exists('expirationDateTime', $rows))
|
||||
? \date(DATE_RFC3339, \strtotime($rows['expirationDateTime'])) : null;
|
||||
|
||||
return GraphHelper::sendSharingInvitationForDrive(
|
||||
$response = GraphHelper::sendSharingInvitationForDrive(
|
||||
$this->featureContext->getBaseUrl(),
|
||||
$this->featureContext->getStepLineRef(),
|
||||
$user,
|
||||
@@ -432,6 +432,10 @@ class SharingNgContext implements Context {
|
||||
$permissionsAction,
|
||||
$expirationDateTime
|
||||
);
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$this->featureContext->shareNgAddToCreatedUserGroupShares($response);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -651,13 +655,7 @@ class SharingNgContext implements Context {
|
||||
string $sharee,
|
||||
TableNode $table
|
||||
) {
|
||||
$permissionID = "";
|
||||
if ($shareType === "user") {
|
||||
$permissionID = "u:" . $this->featureContext->getAttributeOfCreatedUser($sharee, 'id');
|
||||
} elseif ($shareType === "group") {
|
||||
$permissionID = "g:" . $this->featureContext->getAttributeOfCreatedGroup($sharee, 'id');
|
||||
}
|
||||
|
||||
$permissionID = $this->featureContext->shareNgGetLastCreatedUserGroupShareID();
|
||||
$this->featureContext->setResponse(
|
||||
$this->updateResourceShare(
|
||||
$user,
|
||||
@@ -974,7 +972,6 @@ class SharingNgContext implements Context {
|
||||
* @param string $shareType (user|group|link)
|
||||
* @param string $space
|
||||
* @param string|null $resource
|
||||
* @param string|null $recipient
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @throws GuzzleException
|
||||
@@ -984,29 +981,15 @@ class SharingNgContext implements Context {
|
||||
string $sharer,
|
||||
string $shareType,
|
||||
string $space,
|
||||
?string $resource = null,
|
||||
?string $recipient = null
|
||||
?string $resource = null
|
||||
): ResponseInterface {
|
||||
$spaceId = ($this->spacesContext->getSpaceByName($sharer, $space))["id"];
|
||||
$itemId = (isset($resource)) ? $this->spacesContext->getResourceId($sharer, $space, $resource)
|
||||
: $this->spacesContext->getResourceId($sharer, $space, $space);
|
||||
|
||||
$permissionID = "";
|
||||
|
||||
// if resource is not provided then it indicates a space share
|
||||
// and the space shares are not stored
|
||||
// so build the permission-id using the user or group id
|
||||
if ($resource === null) {
|
||||
if ($shareType === "user") {
|
||||
$permissionID = "u:" . $this->featureContext->getAttributeOfCreatedUser($recipient, 'id');
|
||||
} elseif ($shareType === "group") {
|
||||
$permissionID = "g:" . $this->featureContext->getAttributeOfCreatedGroup($recipient, 'id');
|
||||
}
|
||||
} else {
|
||||
$permissionID = ($shareType === 'link')
|
||||
$permissionID = ($shareType === 'link')
|
||||
? $this->featureContext->shareNgGetLastCreatedLinkShareID()
|
||||
: $this->featureContext->shareNgGetLastCreatedUserGroupShareID();
|
||||
}
|
||||
|
||||
return
|
||||
GraphHelper::removeAccessToSpaceItem(
|
||||
@@ -1040,12 +1023,18 @@ class SharingNgContext implements Context {
|
||||
): ResponseInterface {
|
||||
$spaceId = ($this->spacesContext->getSpaceByName($sharer, $space))["id"];
|
||||
|
||||
$permissionID = match ($shareType) {
|
||||
'link' => $this->featureContext->shareNgGetLastCreatedLinkShareID(),
|
||||
'user' => 'u:' . $this->featureContext->getAttributeOfCreatedUser($recipient, 'id'),
|
||||
'group' => 'g:' . $this->featureContext->getAttributeOfCreatedGroup($recipient, 'id'),
|
||||
default => throw new Exception("shareType '$shareType' does not match user|group|link "),
|
||||
};
|
||||
// if recipient is not provided, it means user tries to remove own access, then we need to get the user permission id
|
||||
if ($shareType == 'user' && !isset($recipient)) {
|
||||
$this->featureContext->shareNgAddToCreatedUserGroupShares($this->getDrivePermissionsList($sharer, $space));
|
||||
$permissionID = $this->featureContext->shareNgGetLastCreatedUserGroupShareID();
|
||||
} else {
|
||||
$permissionID = match ($shareType) {
|
||||
'link' => $this->featureContext->shareNgGetLastCreatedLinkShareID(),
|
||||
'user' => $this->featureContext->shareNgGetLastCreatedUserGroupShareID(),
|
||||
'group' => $this->featureContext->shareNgGetLastCreatedUserGroupShareID(),
|
||||
default => throw new Exception("shareType '$shareType' does not match user|group|link "),
|
||||
};
|
||||
}
|
||||
|
||||
return
|
||||
GraphHelper::removeAccessToSpace(
|
||||
@@ -1078,7 +1067,7 @@ class SharingNgContext implements Context {
|
||||
string $resource,
|
||||
string $space
|
||||
): void {
|
||||
$response = $this->removeAccessToSpaceItem($sharer, $recipientType, $space, $resource);
|
||||
$response = $this->removeAccessToSpaceItem($sharer, $recipientType, $space, $resource, $recipient);
|
||||
$this->featureContext->theHTTPStatusCodeShouldBe(204, "", $response);
|
||||
}
|
||||
|
||||
@@ -1195,6 +1184,44 @@ class SharingNgContext implements Context {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^user "([^"]*)" removes own access from space "([^"]*)" using root endpoint of the Graph API$/
|
||||
* @When /^user "([^"]*)" tries to remove own access from space "([^"]*)" using root endpoint of the Graph API$/
|
||||
*
|
||||
* @param string $user
|
||||
* @param string $space
|
||||
*
|
||||
* @return void
|
||||
* @throws JsonException
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function userRemovesOwnAccessFromSpaceUsingGraphAPI(
|
||||
string $user,
|
||||
string $space
|
||||
): void {
|
||||
$this->featureContext->setResponse(
|
||||
$this->removeAccessToSpace($user, 'user', $space)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^user "([^"]*)" has removed own access from space "([^"]*)"$/
|
||||
*
|
||||
* @param string $user
|
||||
* @param string $space
|
||||
*
|
||||
* @return void
|
||||
* @throws JsonException
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function userHasRemovedOwnAccessFromSpace(
|
||||
string $user,
|
||||
string $space
|
||||
): void {
|
||||
$response = $this->removeAccessToSpace($user, 'user', $space);
|
||||
$this->featureContext->theHTTPStatusCodeShouldBe(204, "", $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^user "([^"]*)" removes the link from space "([^"]*)" using root endpoint of the Graph API$/
|
||||
*
|
||||
@@ -1650,11 +1677,7 @@ class SharingNgContext implements Context {
|
||||
TableNode $table
|
||||
): void {
|
||||
$bodyRows = $table->getRowsHash();
|
||||
$permissionID = match ($bodyRows['shareType']) {
|
||||
'user' => 'u:' . $this->featureContext->getAttributeOfCreatedUser($bodyRows['sharee'], 'id'),
|
||||
'group' => 'g:' . $this->featureContext->getAttributeOfCreatedGroup($bodyRows['sharee'], 'id'),
|
||||
default => throw new Exception("shareType {$bodyRows['shareType']} does not match user|group "),
|
||||
};
|
||||
$permissionID = $this->featureContext->shareNgGetLastCreatedUserGroupShareID();
|
||||
$space = $bodyRows['space'];
|
||||
$spaceId = ($this->spacesContext->getSpaceByName($user, $space))["id"];
|
||||
$body = [];
|
||||
|
||||
@@ -345,6 +345,10 @@ _ocdav: api compatibility, return correct status code_
|
||||
- [coreApiWebdavPreviews/previews.feature:264](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L264)
|
||||
- [coreApiWebdavPreviews/previews.feature:265](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/coreApiWebdavPreviews/previews.feature#L265)
|
||||
|
||||
### TODO: adjust test
|
||||
|
||||
- [apiSharingNg1/removeAccessToDrive.feature:145](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSharingNg1/removeAccessToDrive.feature#L145)
|
||||
|
||||
|
||||
### Won't fix
|
||||
|
||||
|
||||
@@ -414,7 +414,7 @@ Feature: List a sharing permissions
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^u:%user_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -456,7 +456,7 @@ Feature: List a sharing permissions
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^u:%user_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -1428,7 +1428,7 @@ Feature: List a sharing permissions
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^u:%user_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -1470,7 +1470,7 @@ Feature: List a sharing permissions
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^u:%user_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -2769,4 +2769,4 @@ Feature: List a sharing permissions
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
"""
|
||||
|
||||
@@ -137,7 +137,7 @@ Feature: Remove access to a drive
|
||||
Scenario: user cannot remove himself from the project space if he is the last manager using root endpoint
|
||||
Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
|
||||
And user "Alice" has created a space "NewSpace" with the default quota using the Graph API
|
||||
When user "Alice" tries to remove the access of user "Alice" from space "NewSpace" using root endpoint of the Graph API
|
||||
When user "Alice" tries to remove own access from space "NewSpace" using root endpoint of the Graph API
|
||||
Then the HTTP status code should be "403"
|
||||
And the user "Alice" should have a space called "NewSpace"
|
||||
|
||||
@@ -152,7 +152,7 @@ Feature: Remove access to a drive
|
||||
| sharee | group1 |
|
||||
| shareType | group |
|
||||
| permissionsRole | Manager |
|
||||
And user "Alice" has removed the access of user "Alice" from space "NewSpace"
|
||||
And user "Alice" has removed own access from space "NewSpace"
|
||||
When user "Brian" tries to remove the access of group "group1" from space "NewSpace" using root endpoint of the Graph API
|
||||
Then the HTTP status code should be "403"
|
||||
And the user "Brian" should have a space called "NewSpace"
|
||||
|
||||
@@ -3407,7 +3407,7 @@ Feature: resources shared by user
|
||||
"name",
|
||||
"id",
|
||||
"eTag",
|
||||
"folder",
|
||||
"root",
|
||||
"lastModifiedDateTime",
|
||||
"size"
|
||||
],
|
||||
@@ -3415,7 +3415,7 @@ Feature: resources shared by user
|
||||
"name": {
|
||||
"const": "."
|
||||
},
|
||||
"folder": {
|
||||
"root": {
|
||||
"const": {}
|
||||
},
|
||||
"id": {
|
||||
|
||||
@@ -1777,7 +1777,7 @@ Feature: Send a sharing invitations
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^g:%group_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
|
||||
+12
-12
@@ -278,7 +278,7 @@ Feature: Update permission of a share
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^u:%user_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -437,7 +437,7 @@ Feature: Update permission of a share
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^g:%group_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -517,7 +517,7 @@ Feature: Update permission of a share
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^g:%group_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -602,7 +602,7 @@ Feature: Update permission of a share
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^g:%group_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -680,7 +680,7 @@ Feature: Update permission of a share
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^g:%group_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -766,7 +766,7 @@ Feature: Update permission of a share
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^g:%group_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -842,7 +842,7 @@ Feature: Update permission of a share
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^u:%user_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -918,7 +918,7 @@ Feature: Update permission of a share
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^u:%user_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -1000,7 +1000,7 @@ Feature: Update permission of a share
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^u:%user_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -1077,7 +1077,7 @@ Feature: Update permission of a share
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^u:%user_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -1159,7 +1159,7 @@ Feature: Update permission of a share
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^u:%user_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
@@ -1417,7 +1417,7 @@ Feature: Update permission of a share
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"pattern": "^u:%user_id_pattern%$"
|
||||
"pattern": "^%permissions_id_pattern%$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -9,6 +9,10 @@
|
||||
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
build-tags:
|
||||
- libpathrs
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- asasalint
|
||||
|
||||
+52
-2
@@ -4,7 +4,54 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [Unreleased 0.5.z] ##
|
||||
## [Unreleased] ##
|
||||
|
||||
## [0.6.1] - 2025-11-19 ##
|
||||
|
||||
> At last up jumped the cunning spider, and fiercely held her fast.
|
||||
|
||||
### Fixed ###
|
||||
- Our logic for deciding whether to use `openat2(2)` or fallback to an `O_PATH`
|
||||
resolver would cache the result to avoid doing needless test runs of
|
||||
`openat2(2)`. However, this causes issues when `pathrs-lite` is being used by
|
||||
a program that applies new seccomp-bpf filters onto itself -- if the filter
|
||||
denies `openat2(2)` then we would return that error rather than falling back
|
||||
to the `O_PATH` resolver. To resolve this issue, we no longer cache the
|
||||
result if `openat2(2)` was successful, only if there was an error.
|
||||
- A file descriptor leak in our `openat2` wrapper (when doing the necessary
|
||||
`dup` for `RESOLVE_IN_ROOT`) has been removed.
|
||||
|
||||
## [0.5.2] - 2025-11-19 ##
|
||||
|
||||
> "Will you walk into my parlour?" said a spider to a fly.
|
||||
|
||||
### Fixed ###
|
||||
- Our logic for deciding whether to use `openat2(2)` or fallback to an `O_PATH`
|
||||
resolver would cache the result to avoid doing needless test runs of
|
||||
`openat2(2)`. However, this causes issues when `pathrs-lite` is being used by
|
||||
a program that applies new seccomp-bpf filters onto itself -- if the filter
|
||||
denies `openat2(2)` then we would return that error rather than falling back
|
||||
to the `O_PATH` resolver. To resolve this issue, we no longer cache the
|
||||
result if `openat2(2)` was successful, only if there was an error.
|
||||
- A file descriptor leak in our `openat2` wrapper (when doing the necessary
|
||||
`dup` for `RESOLVE_IN_ROOT`) has been removed.
|
||||
|
||||
## [0.6.0] - 2025-11-03 ##
|
||||
|
||||
> By the Power of Greyskull!
|
||||
|
||||
### Breaking ###
|
||||
- The deprecated `MkdirAll`, `MkdirAllHandle`, `OpenInRoot`, `OpenatInRoot` and
|
||||
`Reopen` wrappers have been removed. Please switch to using `pathrs-lite`
|
||||
directly.
|
||||
|
||||
### Added ###
|
||||
- `pathrs-lite` now has support for using libpathrs as a backend. This is
|
||||
opt-in and can be enabled at build time with the `libpathrs` build tag. The
|
||||
intention is to allow for downstream libraries and other projects to make use
|
||||
of the pure-Go `github.com/cyphar/filepath-securejoin/pathrs-lite` package
|
||||
and distributors can then opt-in to using `libpathrs` for the entire binary
|
||||
if they wish.
|
||||
|
||||
## [0.5.1] - 2025-10-31 ##
|
||||
|
||||
@@ -383,7 +430,10 @@ This is our first release of `github.com/cyphar/filepath-securejoin`,
|
||||
containing a full implementation with a coverage of 93.5% (the only missing
|
||||
cases are the error cases, which are hard to mocktest at the moment).
|
||||
|
||||
[Unreleased 0.5.z]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.1...release-0.5
|
||||
[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.6.1...HEAD
|
||||
[0.6.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.6.0...v0.6.1
|
||||
[0.6.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...v0.6.0
|
||||
[0.5.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.1...v0.5.2
|
||||
[0.5.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...v0.5.1
|
||||
[0.5.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...v0.5.0
|
||||
[0.4.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.0...v0.4.1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
0.5.1
|
||||
0.6.1
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package securejoin
|
||||
|
||||
import (
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite"
|
||||
)
|
||||
|
||||
var (
|
||||
// MkdirAll is a wrapper around [pathrs.MkdirAll].
|
||||
//
|
||||
// Deprecated: You should use [pathrs.MkdirAll] directly instead. This
|
||||
// wrapper will be removed in filepath-securejoin v0.6.
|
||||
MkdirAll = pathrs.MkdirAll
|
||||
|
||||
// MkdirAllHandle is a wrapper around [pathrs.MkdirAllHandle].
|
||||
//
|
||||
// Deprecated: You should use [pathrs.MkdirAllHandle] directly instead.
|
||||
// This wrapper will be removed in filepath-securejoin v0.6.
|
||||
MkdirAllHandle = pathrs.MkdirAllHandle
|
||||
|
||||
// OpenInRoot is a wrapper around [pathrs.OpenInRoot].
|
||||
//
|
||||
// Deprecated: You should use [pathrs.OpenInRoot] directly instead. This
|
||||
// wrapper will be removed in filepath-securejoin v0.6.
|
||||
OpenInRoot = pathrs.OpenInRoot
|
||||
|
||||
// OpenatInRoot is a wrapper around [pathrs.OpenatInRoot].
|
||||
//
|
||||
// Deprecated: You should use [pathrs.OpenatInRoot] directly instead. This
|
||||
// wrapper will be removed in filepath-securejoin v0.6.
|
||||
OpenatInRoot = pathrs.OpenatInRoot
|
||||
|
||||
// Reopen is a wrapper around [pathrs.Reopen].
|
||||
//
|
||||
// Deprecated: You should use [pathrs.Reopen] directly instead. This
|
||||
// wrapper will be removed in filepath-securejoin v0.6.
|
||||
Reopen = pathrs.Reopen
|
||||
)
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
## `pathrs-lite` ##
|
||||
|
||||
`github.com/cyphar/filepath-securejoin/pathrs-lite` provides a minimal **pure
|
||||
Go** implementation of the core bits of [libpathrs][]. This is not intended to
|
||||
be a complete replacement for libpathrs, instead it is mainly intended to be
|
||||
useful as a transition tool for existing Go projects.
|
||||
|
||||
The long-term plan for `pathrs-lite` is to provide a build tag that will cause
|
||||
all `pathrs-lite` operations to call into libpathrs directly, thus removing
|
||||
code duplication for projects that wish to make use of libpathrs (and providing
|
||||
the ability for software packagers to opt-in to libpathrs support without
|
||||
needing to patch upstream).
|
||||
|
||||
[libpathrs]: https://github.com/cyphar/libpathrs
|
||||
|
||||
### License ###
|
||||
|
||||
Most of this subpackage is licensed under the Mozilla Public License (version
|
||||
2.0). For more information, see the top-level [COPYING.md][] and
|
||||
[LICENSE.MPL-2.0][] files, as well as the individual license headers for each
|
||||
file.
|
||||
|
||||
```
|
||||
Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
Copyright (C) 2024-2025 SUSE LLC
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
```
|
||||
|
||||
[COPYING.md]: ../COPYING.md
|
||||
[LICENSE.MPL-2.0]: ../LICENSE.MPL-2.0
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Package pathrs (pathrs-lite) is a less complete pure Go implementation of
|
||||
// some of the APIs provided by [libpathrs].
|
||||
package pathrs
|
||||
Generated
Vendored
-30
@@ -1,30 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Package assert provides some basic assertion helpers for Go.
|
||||
package assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Assert panics if the predicate is false with the provided argument.
|
||||
func Assert(predicate bool, msg any) {
|
||||
if !predicate {
|
||||
panic(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Assertf panics if the predicate is false and formats the message using the
|
||||
// same formatting as [fmt.Printf].
|
||||
//
|
||||
// [fmt.Printf]: https://pkg.go.dev/fmt#Printf
|
||||
func Assertf(predicate bool, fmtMsg string, args ...any) {
|
||||
Assert(predicate, fmt.Sprintf(fmtMsg, args...))
|
||||
}
|
||||
Generated
Vendored
-41
@@ -1,41 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Package internal contains unexported common code for filepath-securejoin.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type xdevErrorish struct {
|
||||
description string
|
||||
}
|
||||
|
||||
func (err xdevErrorish) Error() string { return err.description }
|
||||
func (err xdevErrorish) Is(target error) bool { return target == unix.EXDEV }
|
||||
|
||||
var (
|
||||
// ErrPossibleAttack indicates that some attack was detected.
|
||||
ErrPossibleAttack error = xdevErrorish{"possible attack detected"}
|
||||
|
||||
// ErrPossibleBreakout indicates that during an operation we ended up in a
|
||||
// state that could be a breakout but we detected it.
|
||||
ErrPossibleBreakout error = xdevErrorish{"possible breakout detected"}
|
||||
|
||||
// ErrInvalidDirectory indicates an unlinked directory.
|
||||
ErrInvalidDirectory = errors.New("wandered into deleted directory")
|
||||
|
||||
// ErrDeletedInode indicates an unlinked file (non-directory).
|
||||
ErrDeletedInode = errors.New("cannot verify path of deleted inode")
|
||||
)
|
||||
-148
@@ -1,148 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
||||
)
|
||||
|
||||
// prepareAtWith returns -EBADF (an invalid fd) if dir is nil, otherwise using
|
||||
// the dir.Fd(). We use -EBADF because in filepath-securejoin we generally
|
||||
// don't want to allow relative-to-cwd paths. The returned path is an
|
||||
// *informational* string that describes a reasonable pathname for the given
|
||||
// *at(2) arguments. You must not use the full path for any actual filesystem
|
||||
// operations.
|
||||
func prepareAt(dir Fd, path string) (dirFd int, unsafeUnmaskedPath string) {
|
||||
dirFd, dirPath := -int(unix.EBADF), "."
|
||||
if dir != nil {
|
||||
dirFd, dirPath = int(dir.Fd()), dir.Name()
|
||||
}
|
||||
if !filepath.IsAbs(path) {
|
||||
// only prepend the dirfd path for relative paths
|
||||
path = dirPath + "/" + path
|
||||
}
|
||||
// NOTE: If path is "." or "", the returned path won't be filepath.Clean,
|
||||
// but that's okay since this path is either used for errors (in which case
|
||||
// a trailing "/" or "/." is important information) or will be
|
||||
// filepath.Clean'd later (in the case of fd.Openat).
|
||||
return dirFd, path
|
||||
}
|
||||
|
||||
// Openat is an [Fd]-based wrapper around unix.Openat.
|
||||
func Openat(dir Fd, path string, flags int, mode int) (*os.File, error) { //nolint:unparam // wrapper func
|
||||
dirFd, fullPath := prepareAt(dir, path)
|
||||
// Make sure we always set O_CLOEXEC.
|
||||
flags |= unix.O_CLOEXEC
|
||||
fd, err := unix.Openat(dirFd, path, flags, uint32(mode))
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "openat", Path: fullPath, Err: err}
|
||||
}
|
||||
runtime.KeepAlive(dir)
|
||||
// openat is only used with lexically-safe paths so we can use
|
||||
// filepath.Clean here, and also the path itself is not going to be used
|
||||
// for actual path operations.
|
||||
fullPath = filepath.Clean(fullPath)
|
||||
return os.NewFile(uintptr(fd), fullPath), nil
|
||||
}
|
||||
|
||||
// Fstatat is an [Fd]-based wrapper around unix.Fstatat.
|
||||
func Fstatat(dir Fd, path string, flags int) (unix.Stat_t, error) {
|
||||
dirFd, fullPath := prepareAt(dir, path)
|
||||
var stat unix.Stat_t
|
||||
if err := unix.Fstatat(dirFd, path, &stat, flags); err != nil {
|
||||
return stat, &os.PathError{Op: "fstatat", Path: fullPath, Err: err}
|
||||
}
|
||||
runtime.KeepAlive(dir)
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
// Faccessat is an [Fd]-based wrapper around unix.Faccessat.
|
||||
func Faccessat(dir Fd, path string, mode uint32, flags int) error {
|
||||
dirFd, fullPath := prepareAt(dir, path)
|
||||
err := unix.Faccessat(dirFd, path, mode, flags)
|
||||
if err != nil {
|
||||
err = &os.PathError{Op: "faccessat", Path: fullPath, Err: err}
|
||||
}
|
||||
runtime.KeepAlive(dir)
|
||||
return err
|
||||
}
|
||||
|
||||
// Readlinkat is an [Fd]-based wrapper around unix.Readlinkat.
|
||||
func Readlinkat(dir Fd, path string) (string, error) {
|
||||
dirFd, fullPath := prepareAt(dir, path)
|
||||
size := 4096
|
||||
for {
|
||||
linkBuf := make([]byte, size)
|
||||
n, err := unix.Readlinkat(dirFd, path, linkBuf)
|
||||
if err != nil {
|
||||
return "", &os.PathError{Op: "readlinkat", Path: fullPath, Err: err}
|
||||
}
|
||||
runtime.KeepAlive(dir)
|
||||
if n != size {
|
||||
return string(linkBuf[:n]), nil
|
||||
}
|
||||
// Possible truncation, resize the buffer.
|
||||
size *= 2
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// STATX_MNT_ID_UNIQUE is provided in golang.org/x/sys@v0.20.0, but in order to
|
||||
// avoid bumping the requirement for a single constant we can just define it
|
||||
// ourselves.
|
||||
_STATX_MNT_ID_UNIQUE = 0x4000 //nolint:revive // unix.* name
|
||||
|
||||
// We don't care which mount ID we get. The kernel will give us the unique
|
||||
// one if it is supported. If the kernel doesn't support
|
||||
// STATX_MNT_ID_UNIQUE, the bit is ignored and the returned request mask
|
||||
// will only contain STATX_MNT_ID (if supported).
|
||||
wantStatxMntMask = _STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID
|
||||
)
|
||||
|
||||
var hasStatxMountID = gocompat.SyncOnceValue(func() bool {
|
||||
var stx unix.Statx_t
|
||||
err := unix.Statx(-int(unix.EBADF), "/", 0, wantStatxMntMask, &stx)
|
||||
return err == nil && stx.Mask&wantStatxMntMask != 0
|
||||
})
|
||||
|
||||
// GetMountID gets the mount identifier associated with the fd and path
|
||||
// combination. It is effectively a wrapper around fetching
|
||||
// STATX_MNT_ID{,_UNIQUE} with unix.Statx, but with a fallback to 0 if the
|
||||
// kernel doesn't support the feature.
|
||||
func GetMountID(dir Fd, path string) (uint64, error) {
|
||||
// If we don't have statx(STATX_MNT_ID*) support, we can't do anything.
|
||||
if !hasStatxMountID() {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
dirFd, fullPath := prepareAt(dir, path)
|
||||
|
||||
var stx unix.Statx_t
|
||||
err := unix.Statx(dirFd, path, unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, wantStatxMntMask, &stx)
|
||||
if stx.Mask&wantStatxMntMask == 0 {
|
||||
// It's not a kernel limitation, for some reason we couldn't get a
|
||||
// mount ID. Assume it's some kind of attack.
|
||||
err = fmt.Errorf("could not get mount id: %w", err)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, &os.PathError{Op: "statx(STATX_MNT_ID_...)", Path: fullPath, Err: err}
|
||||
}
|
||||
runtime.KeepAlive(dir)
|
||||
return stx.Mnt_id, nil
|
||||
}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Package fd provides a drop-in interface-based replacement of [*os.File] that
|
||||
// allows for things like noop-Close wrappers to be used.
|
||||
//
|
||||
// [*os.File]: https://pkg.go.dev/os#File
|
||||
package fd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Fd is an interface that mirrors most of the API of [*os.File], allowing you
|
||||
// to create wrappers that can be used in place of [*os.File].
|
||||
//
|
||||
// [*os.File]: https://pkg.go.dev/os#File
|
||||
type Fd interface {
|
||||
io.Closer
|
||||
Name() string
|
||||
Fd() uintptr
|
||||
}
|
||||
|
||||
// Compile-time interface checks.
|
||||
var (
|
||||
_ Fd = (*os.File)(nil)
|
||||
_ Fd = noClose{}
|
||||
)
|
||||
|
||||
type noClose struct{ inner Fd }
|
||||
|
||||
func (f noClose) Name() string { return f.inner.Name() }
|
||||
func (f noClose) Fd() uintptr { return f.inner.Fd() }
|
||||
|
||||
func (f noClose) Close() error { return nil }
|
||||
|
||||
// NopCloser returns an [*os.File]-like object where the [Close] method is now
|
||||
// a no-op.
|
||||
//
|
||||
// Note that for [*os.File] and similar objects, the Go garbage collector will
|
||||
// still call [Close] on the underlying file unless you use
|
||||
// [runtime.SetFinalizer] to disable this behaviour. This is up to the caller
|
||||
// to do (if necessary).
|
||||
//
|
||||
// [*os.File]: https://pkg.go.dev/os#File
|
||||
// [Close]: https://pkg.go.dev/io#Closer
|
||||
// [runtime.SetFinalizer]: https://pkg.go.dev/runtime#SetFinalizer
|
||||
func NopCloser(f Fd) Fd { return noClose{inner: f} }
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal"
|
||||
)
|
||||
|
||||
// DupWithName creates a new file descriptor referencing the same underlying
|
||||
// file, but with the provided name instead of fd.Name().
|
||||
func DupWithName(fd Fd, name string) (*os.File, error) {
|
||||
fd2, err := unix.FcntlInt(fd.Fd(), unix.F_DUPFD_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, os.NewSyscallError("fcntl(F_DUPFD_CLOEXEC)", err)
|
||||
}
|
||||
runtime.KeepAlive(fd)
|
||||
return os.NewFile(uintptr(fd2), name), nil
|
||||
}
|
||||
|
||||
// Dup creates a new file description referencing the same underlying file.
|
||||
func Dup(fd Fd) (*os.File, error) {
|
||||
return DupWithName(fd, fd.Name())
|
||||
}
|
||||
|
||||
// Fstat is an [Fd]-based wrapper around unix.Fstat.
|
||||
func Fstat(fd Fd) (unix.Stat_t, error) {
|
||||
var stat unix.Stat_t
|
||||
if err := unix.Fstat(int(fd.Fd()), &stat); err != nil {
|
||||
return stat, &os.PathError{Op: "fstat", Path: fd.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(fd)
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
// Fstatfs is an [Fd]-based wrapper around unix.Fstatfs.
|
||||
func Fstatfs(fd Fd) (unix.Statfs_t, error) {
|
||||
var statfs unix.Statfs_t
|
||||
if err := unix.Fstatfs(int(fd.Fd()), &statfs); err != nil {
|
||||
return statfs, &os.PathError{Op: "fstatfs", Path: fd.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(fd)
|
||||
return statfs, nil
|
||||
}
|
||||
|
||||
// IsDeadInode detects whether the file has been unlinked from a filesystem and
|
||||
// is thus a "dead inode" from the kernel's perspective.
|
||||
func IsDeadInode(file Fd) error {
|
||||
// If the nlink of a file drops to 0, there is an attacker deleting
|
||||
// directories during our walk, which could result in weird /proc values.
|
||||
// It's better to error out in this case.
|
||||
stat, err := Fstat(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check for dead inode: %w", err)
|
||||
}
|
||||
if stat.Nlink == 0 {
|
||||
err := internal.ErrDeletedInode
|
||||
if stat.Mode&unix.S_IFMT == unix.S_IFDIR {
|
||||
err = internal.ErrInvalidDirectory
|
||||
}
|
||||
return fmt.Errorf("%w %q", err, file.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Generated
Vendored
-54
@@ -1,54 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Fsopen is an [Fd]-based wrapper around unix.Fsopen.
|
||||
func Fsopen(fsName string, flags int) (*os.File, error) {
|
||||
// Make sure we always set O_CLOEXEC.
|
||||
flags |= unix.FSOPEN_CLOEXEC
|
||||
fd, err := unix.Fsopen(fsName, flags)
|
||||
if err != nil {
|
||||
return nil, os.NewSyscallError("fsopen "+fsName, err)
|
||||
}
|
||||
return os.NewFile(uintptr(fd), "fscontext:"+fsName), nil
|
||||
}
|
||||
|
||||
// Fsmount is an [Fd]-based wrapper around unix.Fsmount.
|
||||
func Fsmount(ctx Fd, flags, mountAttrs int) (*os.File, error) {
|
||||
// Make sure we always set O_CLOEXEC.
|
||||
flags |= unix.FSMOUNT_CLOEXEC
|
||||
fd, err := unix.Fsmount(int(ctx.Fd()), flags, mountAttrs)
|
||||
if err != nil {
|
||||
return nil, os.NewSyscallError("fsmount "+ctx.Name(), err)
|
||||
}
|
||||
return os.NewFile(uintptr(fd), "fsmount:"+ctx.Name()), nil
|
||||
}
|
||||
|
||||
// OpenTree is an [Fd]-based wrapper around unix.OpenTree.
|
||||
func OpenTree(dir Fd, path string, flags uint) (*os.File, error) {
|
||||
dirFd, fullPath := prepareAt(dir, path)
|
||||
// Make sure we always set O_CLOEXEC.
|
||||
flags |= unix.OPEN_TREE_CLOEXEC
|
||||
fd, err := unix.OpenTree(dirFd, path, flags)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "open_tree", Path: fullPath, Err: err}
|
||||
}
|
||||
runtime.KeepAlive(dir)
|
||||
return os.NewFile(uintptr(fd), fullPath), nil
|
||||
}
|
||||
Generated
Vendored
-62
@@ -1,62 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool {
|
||||
// RESOLVE_IN_ROOT (and RESOLVE_BENEATH) can return -EAGAIN if we resolve
|
||||
// ".." while a mount or rename occurs anywhere on the system. This could
|
||||
// happen spuriously, or as the result of an attacker trying to mess with
|
||||
// us during lookup.
|
||||
//
|
||||
// In addition, scoped lookups have a "safety check" at the end of
|
||||
// complete_walk which will return -EXDEV if the final path is not in the
|
||||
// root.
|
||||
return how.Resolve&(unix.RESOLVE_IN_ROOT|unix.RESOLVE_BENEATH) != 0 &&
|
||||
(errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EXDEV))
|
||||
}
|
||||
|
||||
// This is a fairly arbitrary limit we have just to avoid an attacker being
|
||||
// able to make us spin in an infinite retry loop -- callers can choose to
|
||||
// retry on EAGAIN if they prefer.
|
||||
const scopedLookupMaxRetries = 128
|
||||
|
||||
// Openat2 is an [Fd]-based wrapper around unix.Openat2, but with some retry
|
||||
// logic in case of EAGAIN errors.
|
||||
func Openat2(dir Fd, path string, how *unix.OpenHow) (*os.File, error) {
|
||||
dirFd, fullPath := prepareAt(dir, path)
|
||||
// Make sure we always set O_CLOEXEC.
|
||||
how.Flags |= unix.O_CLOEXEC
|
||||
var tries int
|
||||
for {
|
||||
fd, err := unix.Openat2(dirFd, path, how)
|
||||
if err != nil {
|
||||
if scopedLookupShouldRetry(how, err) && tries < scopedLookupMaxRetries {
|
||||
// We retry a couple of times to avoid the spurious errors, and
|
||||
// if we are being attacked then returning -EAGAIN is the best
|
||||
// we can do.
|
||||
tries++
|
||||
continue
|
||||
}
|
||||
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err}
|
||||
}
|
||||
runtime.KeepAlive(dir)
|
||||
return os.NewFile(uintptr(fd), fullPath), nil
|
||||
}
|
||||
}
|
||||
Generated
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
## gocompat ##
|
||||
|
||||
This directory contains backports of stdlib functions from later Go versions so
|
||||
the filepath-securejoin can continue to be used by projects that are stuck with
|
||||
Go 1.18 support. Note that often filepath-securejoin is added in security
|
||||
patches for old releases, so avoiding the need to bump Go compiler requirements
|
||||
is a huge plus to downstreams.
|
||||
|
||||
The source code is licensed under the same license as the Go stdlib. See the
|
||||
source files for the precise license information.
|
||||
Generated
Vendored
-13
@@ -1,13 +0,0 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
//go:build linux && go1.20
|
||||
|
||||
// Copyright (C) 2025 SUSE LLC. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package gocompat includes compatibility shims (backported from future Go
|
||||
// stdlib versions) to permit filepath-securejoin to be used with older Go
|
||||
// versions (often filepath-securejoin is added in security patches for old
|
||||
// releases, so avoiding the need to bump Go compiler requirements is a huge
|
||||
// plus to downstreams).
|
||||
package gocompat
|
||||
Generated
Vendored
-19
@@ -1,19 +0,0 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
//go:build linux && go1.20
|
||||
|
||||
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gocompat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// WrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
|
||||
// that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap)
|
||||
// is only guaranteed to give you baseErr.
|
||||
func WrapBaseError(baseErr, extraErr error) error {
|
||||
return fmt.Errorf("%w: %w", extraErr, baseErr)
|
||||
}
|
||||
Generated
Vendored
-40
@@ -1,40 +0,0 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && !go1.20
|
||||
|
||||
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gocompat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type wrappedError struct {
|
||||
inner error
|
||||
isError error
|
||||
}
|
||||
|
||||
func (err wrappedError) Is(target error) bool {
|
||||
return err.isError == target
|
||||
}
|
||||
|
||||
func (err wrappedError) Unwrap() error {
|
||||
return err.inner
|
||||
}
|
||||
|
||||
func (err wrappedError) Error() string {
|
||||
return fmt.Sprintf("%v: %v", err.isError, err.inner)
|
||||
}
|
||||
|
||||
// WrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
|
||||
// that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap)
|
||||
// is only guaranteed to give you baseErr.
|
||||
func WrapBaseError(baseErr, extraErr error) error {
|
||||
return wrappedError{
|
||||
inner: baseErr,
|
||||
isError: extraErr,
|
||||
}
|
||||
}
|
||||
Generated
Vendored
-53
@@ -1,53 +0,0 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && go1.21
|
||||
|
||||
// Copyright (C) 2024-2025 SUSE LLC. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gocompat
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"slices"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// SlicesDeleteFunc is equivalent to Go 1.21's slices.DeleteFunc.
|
||||
func SlicesDeleteFunc[S ~[]E, E any](slice S, delFn func(E) bool) S {
|
||||
return slices.DeleteFunc(slice, delFn)
|
||||
}
|
||||
|
||||
// SlicesContains is equivalent to Go 1.21's slices.Contains.
|
||||
func SlicesContains[S ~[]E, E comparable](slice S, val E) bool {
|
||||
return slices.Contains(slice, val)
|
||||
}
|
||||
|
||||
// SlicesClone is equivalent to Go 1.21's slices.Clone.
|
||||
func SlicesClone[S ~[]E, E any](slice S) S {
|
||||
return slices.Clone(slice)
|
||||
}
|
||||
|
||||
// SyncOnceValue is equivalent to Go 1.21's sync.OnceValue.
|
||||
func SyncOnceValue[T any](f func() T) func() T {
|
||||
return sync.OnceValue(f)
|
||||
}
|
||||
|
||||
// SyncOnceValues is equivalent to Go 1.21's sync.OnceValues.
|
||||
func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
|
||||
return sync.OnceValues(f)
|
||||
}
|
||||
|
||||
// CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition.
|
||||
type CmpOrdered = cmp.Ordered
|
||||
|
||||
// CmpCompare is equivalent to Go 1.21's cmp.Compare.
|
||||
func CmpCompare[T CmpOrdered](x, y T) int {
|
||||
return cmp.Compare(x, y)
|
||||
}
|
||||
|
||||
// Max2 is equivalent to Go 1.21's max builtin (but only for two parameters).
|
||||
func Max2[T CmpOrdered](x, y T) T {
|
||||
return max(x, y)
|
||||
}
|
||||
Generated
Vendored
-187
@@ -1,187 +0,0 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && !go1.21
|
||||
|
||||
// Copyright (C) 2021, 2022 The Go Authors. All rights reserved.
|
||||
// Copyright (C) 2024-2025 SUSE LLC. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.BSD file.
|
||||
|
||||
package gocompat
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// These are very minimal implementations of functions that appear in Go 1.21's
|
||||
// stdlib, included so that we can build on older Go versions. Most are
|
||||
// borrowed directly from the stdlib, and a few are modified to be "obviously
|
||||
// correct" without needing to copy too many other helpers.
|
||||
|
||||
// clearSlice is equivalent to Go 1.21's builtin clear.
|
||||
// Copied from the Go 1.24 stdlib implementation.
|
||||
func clearSlice[S ~[]E, E any](slice S) {
|
||||
var zero E
|
||||
for i := range slice {
|
||||
slice[i] = zero
|
||||
}
|
||||
}
|
||||
|
||||
// slicesIndexFunc is equivalent to Go 1.21's slices.IndexFunc.
|
||||
// Copied from the Go 1.24 stdlib implementation.
|
||||
func slicesIndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
|
||||
for i := range s {
|
||||
if f(s[i]) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// SlicesDeleteFunc is equivalent to Go 1.21's slices.DeleteFunc.
|
||||
// Copied from the Go 1.24 stdlib implementation.
|
||||
func SlicesDeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
|
||||
i := slicesIndexFunc(s, del)
|
||||
if i == -1 {
|
||||
return s
|
||||
}
|
||||
// Don't start copying elements until we find one to delete.
|
||||
for j := i + 1; j < len(s); j++ {
|
||||
if v := s[j]; !del(v) {
|
||||
s[i] = v
|
||||
i++
|
||||
}
|
||||
}
|
||||
clearSlice(s[i:]) // zero/nil out the obsolete elements, for GC
|
||||
return s[:i]
|
||||
}
|
||||
|
||||
// SlicesContains is equivalent to Go 1.21's slices.Contains.
|
||||
// Similar to the stdlib slices.Contains, except that we don't have
|
||||
// slices.Index so we need to use slices.IndexFunc for this non-Func helper.
|
||||
func SlicesContains[S ~[]E, E comparable](s S, v E) bool {
|
||||
return slicesIndexFunc(s, func(e E) bool { return e == v }) >= 0
|
||||
}
|
||||
|
||||
// SlicesClone is equivalent to Go 1.21's slices.Clone.
|
||||
// Copied from the Go 1.24 stdlib implementation.
|
||||
func SlicesClone[S ~[]E, E any](s S) S {
|
||||
// Preserve nil in case it matters.
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return append(S([]E{}), s...)
|
||||
}
|
||||
|
||||
// SyncOnceValue is equivalent to Go 1.21's sync.OnceValue.
|
||||
// Copied from the Go 1.25 stdlib implementation.
|
||||
func SyncOnceValue[T any](f func() T) func() T {
|
||||
// Use a struct so that there's a single heap allocation.
|
||||
d := struct {
|
||||
f func() T
|
||||
once sync.Once
|
||||
valid bool
|
||||
p any
|
||||
result T
|
||||
}{
|
||||
f: f,
|
||||
}
|
||||
return func() T {
|
||||
d.once.Do(func() {
|
||||
defer func() {
|
||||
d.f = nil
|
||||
d.p = recover()
|
||||
if !d.valid {
|
||||
panic(d.p)
|
||||
}
|
||||
}()
|
||||
d.result = d.f()
|
||||
d.valid = true
|
||||
})
|
||||
if !d.valid {
|
||||
panic(d.p)
|
||||
}
|
||||
return d.result
|
||||
}
|
||||
}
|
||||
|
||||
// SyncOnceValues is equivalent to Go 1.21's sync.OnceValues.
|
||||
// Copied from the Go 1.25 stdlib implementation.
|
||||
func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
|
||||
// Use a struct so that there's a single heap allocation.
|
||||
d := struct {
|
||||
f func() (T1, T2)
|
||||
once sync.Once
|
||||
valid bool
|
||||
p any
|
||||
r1 T1
|
||||
r2 T2
|
||||
}{
|
||||
f: f,
|
||||
}
|
||||
return func() (T1, T2) {
|
||||
d.once.Do(func() {
|
||||
defer func() {
|
||||
d.f = nil
|
||||
d.p = recover()
|
||||
if !d.valid {
|
||||
panic(d.p)
|
||||
}
|
||||
}()
|
||||
d.r1, d.r2 = d.f()
|
||||
d.valid = true
|
||||
})
|
||||
if !d.valid {
|
||||
panic(d.p)
|
||||
}
|
||||
return d.r1, d.r2
|
||||
}
|
||||
}
|
||||
|
||||
// CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition.
|
||||
// Copied from the Go 1.25 stdlib implementation.
|
||||
type CmpOrdered interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 |
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
|
||||
~float32 | ~float64 |
|
||||
~string
|
||||
}
|
||||
|
||||
// isNaN reports whether x is a NaN without requiring the math package.
|
||||
// This will always return false if T is not floating-point.
|
||||
// Copied from the Go 1.25 stdlib implementation.
|
||||
func isNaN[T CmpOrdered](x T) bool {
|
||||
return x != x
|
||||
}
|
||||
|
||||
// CmpCompare is equivalent to Go 1.21's cmp.Compare.
|
||||
// Copied from the Go 1.25 stdlib implementation.
|
||||
func CmpCompare[T CmpOrdered](x, y T) int {
|
||||
xNaN := isNaN(x)
|
||||
yNaN := isNaN(y)
|
||||
if xNaN {
|
||||
if yNaN {
|
||||
return 0
|
||||
}
|
||||
return -1
|
||||
}
|
||||
if yNaN {
|
||||
return +1
|
||||
}
|
||||
if x < y {
|
||||
return -1
|
||||
}
|
||||
if x > y {
|
||||
return +1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Max2 is equivalent to Go 1.21's max builtin for two parameters.
|
||||
func Max2[T CmpOrdered](x, y T) T {
|
||||
m := x
|
||||
if y > m {
|
||||
m = y
|
||||
}
|
||||
return m
|
||||
}
|
||||
Generated
Vendored
-123
@@ -1,123 +0,0 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Copyright (C) 2022 The Go Authors. All rights reserved.
|
||||
// Copyright (C) 2025 SUSE LLC. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.BSD file.
|
||||
|
||||
// The parsing logic is very loosely based on the Go stdlib's
|
||||
// src/internal/syscall/unix/kernel_version_linux.go but with an API that looks
|
||||
// a bit like runc's libcontainer/system/kernelversion.
|
||||
//
|
||||
// TODO(cyphar): This API has been copied around to a lot of different projects
|
||||
// (Docker, containerd, runc, and now filepath-securejoin) -- maybe we should
|
||||
// put it in a separate project?
|
||||
|
||||
// Package kernelversion provides a simple mechanism for checking whether the
|
||||
// running kernel is at least as new as some baseline kernel version. This is
|
||||
// often useful when checking for features that would be too complicated to
|
||||
// test support for (or in cases where we know that some kernel features in
|
||||
// backport-heavy kernels are broken and need to be avoided).
|
||||
package kernelversion
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
||||
)
|
||||
|
||||
// KernelVersion is a numeric representation of the key numerical elements of a
|
||||
// kernel version (for instance, "4.1.2-default-1" would be represented as
|
||||
// KernelVersion{4, 1, 2}).
|
||||
type KernelVersion []uint64
|
||||
|
||||
func (kver KernelVersion) String() string {
|
||||
var str strings.Builder
|
||||
for idx, elem := range kver {
|
||||
if idx != 0 {
|
||||
_, _ = str.WriteRune('.')
|
||||
}
|
||||
_, _ = str.WriteString(strconv.FormatUint(elem, 10))
|
||||
}
|
||||
return str.String()
|
||||
}
|
||||
|
||||
var errInvalidKernelVersion = errors.New("invalid kernel version")
|
||||
|
||||
// parseKernelVersion parses a string and creates a KernelVersion based on it.
|
||||
func parseKernelVersion(kverStr string) (KernelVersion, error) {
|
||||
kver := make(KernelVersion, 1, 3)
|
||||
for idx, ch := range kverStr {
|
||||
if '0' <= ch && ch <= '9' {
|
||||
v := &kver[len(kver)-1]
|
||||
*v = (*v * 10) + uint64(ch-'0')
|
||||
} else {
|
||||
if idx == 0 || kverStr[idx-1] < '0' || '9' < kverStr[idx-1] {
|
||||
// "." must be preceded by a digit while in version section
|
||||
return nil, fmt.Errorf("%w %q: kernel version has dot(s) followed by non-digit in version section", errInvalidKernelVersion, kverStr)
|
||||
}
|
||||
if ch != '.' {
|
||||
break
|
||||
}
|
||||
kver = append(kver, 0)
|
||||
}
|
||||
}
|
||||
if len(kver) < 2 {
|
||||
return nil, fmt.Errorf("%w %q: kernel versions must contain at least two components", errInvalidKernelVersion, kverStr)
|
||||
}
|
||||
return kver, nil
|
||||
}
|
||||
|
||||
// getKernelVersion gets the current kernel version.
|
||||
var getKernelVersion = gocompat.SyncOnceValues(func() (KernelVersion, error) {
|
||||
var uts unix.Utsname
|
||||
if err := unix.Uname(&uts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Remove the \x00 from the release.
|
||||
release := uts.Release[:]
|
||||
return parseKernelVersion(string(release[:bytes.IndexByte(release, 0)]))
|
||||
})
|
||||
|
||||
// GreaterEqualThan returns true if the the host kernel version is greater than
|
||||
// or equal to the provided [KernelVersion]. When doing this comparison, any
|
||||
// non-numerical suffixes of the host kernel version are ignored.
|
||||
//
|
||||
// If the number of components provided is not equal to the number of numerical
|
||||
// components of the host kernel version, any missing components are treated as
|
||||
// 0. This means that GreaterEqualThan(KernelVersion{4}) will be treated the
|
||||
// same as GreaterEqualThan(KernelVersion{4, 0, 0, ..., 0, 0}), and that if the
|
||||
// host kernel version is "4" then GreaterEqualThan(KernelVersion{4, 1}) will
|
||||
// return false (because the host version will be treated as "4.0").
|
||||
func GreaterEqualThan(wantKver KernelVersion) (bool, error) {
|
||||
hostKver, err := getKernelVersion()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Pad out the kernel version lengths to match one another.
|
||||
cmpLen := gocompat.Max2(len(hostKver), len(wantKver))
|
||||
hostKver = append(hostKver, make(KernelVersion, cmpLen-len(hostKver))...)
|
||||
wantKver = append(wantKver, make(KernelVersion, cmpLen-len(wantKver))...)
|
||||
|
||||
for i := 0; i < cmpLen; i++ {
|
||||
switch gocompat.CmpCompare(hostKver[i], wantKver[i]) {
|
||||
case -1:
|
||||
// host < want
|
||||
return false, nil
|
||||
case +1:
|
||||
// host > want
|
||||
return true, nil
|
||||
case 0:
|
||||
continue
|
||||
}
|
||||
}
|
||||
// equal version values
|
||||
return true, nil
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Package linux returns information about what features are supported on the
|
||||
// running kernel.
|
||||
package linux
|
||||
Generated
Vendored
-47
@@ -1,47 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package linux
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/kernelversion"
|
||||
)
|
||||
|
||||
// HasNewMountAPI returns whether the new fsopen(2) mount API is supported on
|
||||
// the running kernel.
|
||||
var HasNewMountAPI = gocompat.SyncOnceValue(func() bool {
|
||||
// All of the pieces of the new mount API we use (fsopen, fsconfig,
|
||||
// fsmount, open_tree) were added together in Linux 5.2[1,2], so we can
|
||||
// just check for one of the syscalls and the others should also be
|
||||
// available.
|
||||
//
|
||||
// Just try to use open_tree(2) to open a file without OPEN_TREE_CLONE.
|
||||
// This is equivalent to openat(2), but tells us if open_tree is
|
||||
// available (and thus all of the other basic new mount API syscalls).
|
||||
// open_tree(2) is most light-weight syscall to test here.
|
||||
//
|
||||
// [1]: merge commit 400913252d09
|
||||
// [2]: <https://lore.kernel.org/lkml/153754740781.17872.7869536526927736855.stgit@warthog.procyon.org.uk/>
|
||||
fd, err := unix.OpenTree(-int(unix.EBADF), "/", unix.OPEN_TREE_CLOEXEC)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = unix.Close(fd)
|
||||
|
||||
// RHEL 8 has a backport of fsopen(2) that appears to have some very
|
||||
// difficult to debug performance pathology. As such, it seems prudent to
|
||||
// simply reject pre-5.2 kernels.
|
||||
isNotBackport, _ := kernelversion.GreaterEqualThan(kernelversion.KernelVersion{5, 2})
|
||||
return isNotBackport
|
||||
})
|
||||
Generated
Vendored
-31
@@ -1,31 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package linux
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
||||
)
|
||||
|
||||
// HasOpenat2 returns whether openat2(2) is supported on the running kernel.
|
||||
var HasOpenat2 = gocompat.SyncOnceValue(func() bool {
|
||||
fd, err := unix.Openat2(unix.AT_FDCWD, ".", &unix.OpenHow{
|
||||
Flags: unix.O_PATH | unix.O_CLOEXEC,
|
||||
Resolve: unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_IN_ROOT,
|
||||
})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = unix.Close(fd)
|
||||
return true
|
||||
})
|
||||
Generated
Vendored
-544
@@ -1,544 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Package procfs provides a safe API for operating on /proc on Linux. Note
|
||||
// that this is the *internal* procfs API, mainy needed due to Go's
|
||||
// restrictions on cyclic dependencies and its incredibly minimal visibility
|
||||
// system without making a separate internal/ package.
|
||||
package procfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux"
|
||||
)
|
||||
|
||||
// The kernel guarantees that the root inode of a procfs mount has an
|
||||
// f_type of PROC_SUPER_MAGIC and st_ino of PROC_ROOT_INO.
|
||||
const (
|
||||
procSuperMagic = 0x9fa0 // PROC_SUPER_MAGIC
|
||||
procRootIno = 1 // PROC_ROOT_INO
|
||||
)
|
||||
|
||||
// verifyProcHandle checks that the handle is from a procfs filesystem.
|
||||
// Contrast this to [verifyProcRoot], which also verifies that the handle is
|
||||
// the root of a procfs mount.
|
||||
func verifyProcHandle(procHandle fd.Fd) error {
|
||||
if statfs, err := fd.Fstatfs(procHandle); err != nil {
|
||||
return err
|
||||
} else if statfs.Type != procSuperMagic {
|
||||
return fmt.Errorf("%w: incorrect procfs root filesystem type 0x%x", errUnsafeProcfs, statfs.Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyProcRoot verifies that the handle is the root of a procfs filesystem.
|
||||
// Contrast this to [verifyProcHandle], which only verifies if the handle is
|
||||
// some file on procfs (regardless of what file it is).
|
||||
func verifyProcRoot(procRoot fd.Fd) error {
|
||||
if err := verifyProcHandle(procRoot); err != nil {
|
||||
return err
|
||||
}
|
||||
if stat, err := fd.Fstat(procRoot); err != nil {
|
||||
return err
|
||||
} else if stat.Ino != procRootIno {
|
||||
return fmt.Errorf("%w: incorrect procfs root inode number %d", errUnsafeProcfs, stat.Ino)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type procfsFeatures struct {
|
||||
// hasSubsetPid was added in Linux 5.8, along with hidepid=ptraceable (and
|
||||
// string-based hidepid= values). Before this patchset, it was not really
|
||||
// safe to try to modify procfs superblock flags because the superblock was
|
||||
// shared -- so if this feature is not available, **you should not set any
|
||||
// superblock flags**.
|
||||
//
|
||||
// 6814ef2d992a ("proc: add option to mount only a pids subset")
|
||||
// fa10fed30f25 ("proc: allow to mount many instances of proc in one pid namespace")
|
||||
// 24a71ce5c47f ("proc: instantiate only pids that we can ptrace on 'hidepid=4' mount option")
|
||||
// 1c6c4d112e81 ("proc: use human-readable values for hidepid")
|
||||
// 9ff7258575d5 ("Merge branch 'proc-linus' of git://git.kernel.org/pub/scm/linux/kernel/git/ebiederm/user-namespace")
|
||||
hasSubsetPid bool
|
||||
}
|
||||
|
||||
var getProcfsFeatures = gocompat.SyncOnceValue(func() procfsFeatures {
|
||||
if !linux.HasNewMountAPI() {
|
||||
return procfsFeatures{}
|
||||
}
|
||||
procfsCtx, err := fd.Fsopen("proc", unix.FSOPEN_CLOEXEC)
|
||||
if err != nil {
|
||||
return procfsFeatures{}
|
||||
}
|
||||
defer procfsCtx.Close() //nolint:errcheck // close failures aren't critical here
|
||||
|
||||
return procfsFeatures{
|
||||
hasSubsetPid: unix.FsconfigSetString(int(procfsCtx.Fd()), "subset", "pid") == nil,
|
||||
}
|
||||
})
|
||||
|
||||
func newPrivateProcMount(subset bool) (_ *Handle, Err error) {
|
||||
procfsCtx, err := fd.Fsopen("proc", unix.FSOPEN_CLOEXEC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer procfsCtx.Close() //nolint:errcheck // close failures aren't critical here
|
||||
|
||||
if subset && getProcfsFeatures().hasSubsetPid {
|
||||
// Try to configure hidepid=ptraceable,subset=pid if possible, but
|
||||
// ignore errors.
|
||||
_ = unix.FsconfigSetString(int(procfsCtx.Fd()), "hidepid", "ptraceable")
|
||||
_ = unix.FsconfigSetString(int(procfsCtx.Fd()), "subset", "pid")
|
||||
}
|
||||
|
||||
// Get an actual handle.
|
||||
if err := unix.FsconfigCreate(int(procfsCtx.Fd())); err != nil {
|
||||
return nil, os.NewSyscallError("fsconfig create procfs", err)
|
||||
}
|
||||
// TODO: Output any information from the fscontext log to debug logs.
|
||||
procRoot, err := fd.Fsmount(procfsCtx, unix.FSMOUNT_CLOEXEC, unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_NOSUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if Err != nil {
|
||||
_ = procRoot.Close()
|
||||
}
|
||||
}()
|
||||
return newHandle(procRoot)
|
||||
}
|
||||
|
||||
func clonePrivateProcMount() (_ *Handle, Err error) {
|
||||
// Try to make a clone without using AT_RECURSIVE if we can. If this works,
|
||||
// we can be sure there are no over-mounts and so if the root is valid then
|
||||
// we're golden. Otherwise, we have to deal with over-mounts.
|
||||
procRoot, err := fd.OpenTree(nil, "/proc", unix.OPEN_TREE_CLONE)
|
||||
if err != nil || hookForcePrivateProcRootOpenTreeAtRecursive(procRoot) {
|
||||
procRoot, err = fd.OpenTree(nil, "/proc", unix.OPEN_TREE_CLONE|unix.AT_RECURSIVE)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating a detached procfs clone: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if Err != nil {
|
||||
_ = procRoot.Close()
|
||||
}
|
||||
}()
|
||||
return newHandle(procRoot)
|
||||
}
|
||||
|
||||
func privateProcRoot(subset bool) (*Handle, error) {
|
||||
if !linux.HasNewMountAPI() || hookForceGetProcRootUnsafe() {
|
||||
return nil, fmt.Errorf("new mount api: %w", unix.ENOTSUP)
|
||||
}
|
||||
// Try to create a new procfs mount from scratch if we can. This ensures we
|
||||
// can get a procfs mount even if /proc is fake (for whatever reason).
|
||||
procRoot, err := newPrivateProcMount(subset)
|
||||
if err != nil || hookForcePrivateProcRootOpenTree(procRoot) {
|
||||
// Try to clone /proc then...
|
||||
procRoot, err = clonePrivateProcMount()
|
||||
}
|
||||
return procRoot, err
|
||||
}
|
||||
|
||||
func unsafeHostProcRoot() (_ *Handle, Err error) {
|
||||
procRoot, err := os.OpenFile("/proc", unix.O_PATH|unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if Err != nil {
|
||||
_ = procRoot.Close()
|
||||
}
|
||||
}()
|
||||
return newHandle(procRoot)
|
||||
}
|
||||
|
||||
// Handle is a wrapper around an *os.File handle to "/proc", which can be used
|
||||
// to do further procfs-related operations in a safe way.
|
||||
type Handle struct {
|
||||
Inner fd.Fd
|
||||
// Does this handle have subset=pid set?
|
||||
isSubset bool
|
||||
}
|
||||
|
||||
func newHandle(procRoot fd.Fd) (*Handle, error) {
|
||||
if err := verifyProcRoot(procRoot); err != nil {
|
||||
// This is only used in methods that
|
||||
_ = procRoot.Close()
|
||||
return nil, err
|
||||
}
|
||||
proc := &Handle{Inner: procRoot}
|
||||
// With subset=pid we can be sure that /proc/uptime will not exist.
|
||||
if err := fd.Faccessat(proc.Inner, "uptime", unix.F_OK, unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
||||
proc.isSubset = errors.Is(err, os.ErrNotExist)
|
||||
}
|
||||
return proc, nil
|
||||
}
|
||||
|
||||
// Close closes the underlying file for the Handle.
|
||||
func (proc *Handle) Close() error { return proc.Inner.Close() }
|
||||
|
||||
var getCachedProcRoot = gocompat.SyncOnceValue(func() *Handle {
|
||||
procRoot, err := getProcRoot(true)
|
||||
if err != nil {
|
||||
return nil // just don't cache if we see an error
|
||||
}
|
||||
if !procRoot.isSubset {
|
||||
return nil // we only cache verified subset=pid handles
|
||||
}
|
||||
|
||||
// Disarm (*Handle).Close() to stop someone from accidentally closing
|
||||
// the global handle.
|
||||
procRoot.Inner = fd.NopCloser(procRoot.Inner)
|
||||
return procRoot
|
||||
})
|
||||
|
||||
// OpenProcRoot tries to open a "safer" handle to "/proc".
|
||||
func OpenProcRoot() (*Handle, error) {
|
||||
if proc := getCachedProcRoot(); proc != nil {
|
||||
return proc, nil
|
||||
}
|
||||
return getProcRoot(true)
|
||||
}
|
||||
|
||||
// OpenUnsafeProcRoot opens a handle to "/proc" without any overmounts or
|
||||
// masked paths (but also without "subset=pid").
|
||||
func OpenUnsafeProcRoot() (*Handle, error) { return getProcRoot(false) }
|
||||
|
||||
func getProcRoot(subset bool) (*Handle, error) {
|
||||
proc, err := privateProcRoot(subset)
|
||||
if err != nil {
|
||||
// Fall back to using a /proc handle if making a private mount failed.
|
||||
// If we have openat2, at least we can avoid some kinds of over-mount
|
||||
// attacks, but without openat2 there's not much we can do.
|
||||
proc, err = unsafeHostProcRoot()
|
||||
}
|
||||
return proc, err
|
||||
}
|
||||
|
||||
var hasProcThreadSelf = gocompat.SyncOnceValue(func() bool {
|
||||
return unix.Access("/proc/thread-self/", unix.F_OK) == nil
|
||||
})
|
||||
|
||||
var errUnsafeProcfs = errors.New("unsafe procfs detected")
|
||||
|
||||
// lookup is a very minimal wrapper around [procfsLookupInRoot] which is
|
||||
// intended to be called from the external API.
|
||||
func (proc *Handle) lookup(subpath string) (*os.File, error) {
|
||||
handle, err := procfsLookupInRoot(proc.Inner, subpath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
// procfsBase is an enum indicating the prefix of a subpath in operations
|
||||
// involving [Handle]s.
|
||||
type procfsBase string
|
||||
|
||||
const (
|
||||
// ProcRoot refers to the root of the procfs (i.e., "/proc/<subpath>").
|
||||
ProcRoot procfsBase = "/proc"
|
||||
// ProcSelf refers to the current process' subdirectory (i.e.,
|
||||
// "/proc/self/<subpath>").
|
||||
ProcSelf procfsBase = "/proc/self"
|
||||
// ProcThreadSelf refers to the current thread's subdirectory (i.e.,
|
||||
// "/proc/thread-self/<subpath>"). In multi-threaded programs (i.e., all Go
|
||||
// programs) where one thread has a different CLONE_FS, it is possible for
|
||||
// "/proc/self" to point the wrong thread and so "/proc/thread-self" may be
|
||||
// necessary. Note that on pre-3.17 kernels, "/proc/thread-self" doesn't
|
||||
// exist and so a fallback will be used in that case.
|
||||
ProcThreadSelf procfsBase = "/proc/thread-self"
|
||||
// TODO: Switch to an interface setup so we can have a more type-safe
|
||||
// version of ProcPid and remove the need to worry about invalid string
|
||||
// values.
|
||||
)
|
||||
|
||||
// prefix returns a prefix that can be used with the given [Handle].
|
||||
func (base procfsBase) prefix(proc *Handle) (string, error) {
|
||||
switch base {
|
||||
case ProcRoot:
|
||||
return ".", nil
|
||||
case ProcSelf:
|
||||
return "self", nil
|
||||
case ProcThreadSelf:
|
||||
threadSelf := "thread-self"
|
||||
if !hasProcThreadSelf() || hookForceProcSelfTask() {
|
||||
// Pre-3.17 kernels don't have /proc/thread-self, so do it
|
||||
// manually.
|
||||
threadSelf = "self/task/" + strconv.Itoa(unix.Gettid())
|
||||
if err := fd.Faccessat(proc.Inner, threadSelf, unix.F_OK, unix.AT_SYMLINK_NOFOLLOW); err != nil || hookForceProcSelf() {
|
||||
// In this case, we running in a pid namespace that doesn't
|
||||
// match the /proc mount we have. This can happen inside runc.
|
||||
//
|
||||
// Unfortunately, there is no nice way to get the correct TID
|
||||
// to use here because of the age of the kernel, so we have to
|
||||
// just use /proc/self and hope that it works.
|
||||
threadSelf = "self"
|
||||
}
|
||||
}
|
||||
return threadSelf, nil
|
||||
}
|
||||
return "", fmt.Errorf("invalid procfs base %q", base)
|
||||
}
|
||||
|
||||
// ProcThreadSelfCloser is a callback that needs to be called when you are done
|
||||
// operating on an [os.File] fetched using [ProcThreadSelf].
|
||||
//
|
||||
// [os.File]: https://pkg.go.dev/os#File
|
||||
type ProcThreadSelfCloser func()
|
||||
|
||||
// open is the core lookup operation for [Handle]. It returns a handle to
|
||||
// "/proc/<base>/<subpath>". If the returned [ProcThreadSelfCloser] is non-nil,
|
||||
// you should call it after you are done interacting with the returned handle.
|
||||
//
|
||||
// In general you should use prefer to use the other helpers, as they remove
|
||||
// the need to interact with [procfsBase] and do not return a nil
|
||||
// [ProcThreadSelfCloser] for [procfsBase] values other than [ProcThreadSelf]
|
||||
// where it is necessary.
|
||||
func (proc *Handle) open(base procfsBase, subpath string) (_ *os.File, closer ProcThreadSelfCloser, Err error) {
|
||||
prefix, err := base.prefix(proc)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
subpath = prefix + "/" + subpath
|
||||
|
||||
switch base {
|
||||
case ProcRoot:
|
||||
file, err := proc.lookup(subpath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// The Handle handle in use might be a subset=pid one, which will
|
||||
// result in spurious errors. In this case, just open a temporary
|
||||
// unmasked procfs handle for this operation.
|
||||
proc, err2 := OpenUnsafeProcRoot() // !subset=pid
|
||||
if err2 != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer proc.Close() //nolint:errcheck // close failures aren't critical here
|
||||
|
||||
file, err = proc.lookup(subpath)
|
||||
}
|
||||
return file, nil, err
|
||||
|
||||
case ProcSelf:
|
||||
file, err := proc.lookup(subpath)
|
||||
return file, nil, err
|
||||
|
||||
case ProcThreadSelf:
|
||||
// We need to lock our thread until the caller is done with the handle
|
||||
// because between getting the handle and using it we could get
|
||||
// interrupted by the Go runtime and hit the case where the underlying
|
||||
// thread is swapped out and the original thread is killed, resulting
|
||||
// in pull-your-hair-out-hard-to-debug issues in the caller.
|
||||
runtime.LockOSThread()
|
||||
defer func() {
|
||||
if Err != nil {
|
||||
runtime.UnlockOSThread()
|
||||
closer = nil
|
||||
}
|
||||
}()
|
||||
|
||||
file, err := proc.lookup(subpath)
|
||||
return file, runtime.UnlockOSThread, err
|
||||
}
|
||||
// should never be reached
|
||||
return nil, nil, fmt.Errorf("[internal error] invalid procfs base %q", base)
|
||||
}
|
||||
|
||||
// OpenThreadSelf returns a handle to "/proc/thread-self/<subpath>" (or an
|
||||
// equivalent handle on older kernels where "/proc/thread-self" doesn't exist).
|
||||
// Once finished with the handle, you must call the returned closer function
|
||||
// (runtime.UnlockOSThread). You must not pass the returned *os.File to other
|
||||
// Go threads or use the handle after calling the closer.
|
||||
func (proc *Handle) OpenThreadSelf(subpath string) (_ *os.File, _ ProcThreadSelfCloser, Err error) {
|
||||
return proc.open(ProcThreadSelf, subpath)
|
||||
}
|
||||
|
||||
// OpenSelf returns a handle to /proc/self/<subpath>.
|
||||
func (proc *Handle) OpenSelf(subpath string) (*os.File, error) {
|
||||
file, closer, err := proc.open(ProcSelf, subpath)
|
||||
assert.Assert(closer == nil, "closer for ProcSelf must be nil")
|
||||
return file, err
|
||||
}
|
||||
|
||||
// OpenRoot returns a handle to /proc/<subpath>.
|
||||
func (proc *Handle) OpenRoot(subpath string) (*os.File, error) {
|
||||
file, closer, err := proc.open(ProcRoot, subpath)
|
||||
assert.Assert(closer == nil, "closer for ProcRoot must be nil")
|
||||
return file, err
|
||||
}
|
||||
|
||||
// OpenPid returns a handle to /proc/$pid/<subpath> (pid can be a pid or tid).
|
||||
// This is mainly intended for usage when operating on other processes.
|
||||
func (proc *Handle) OpenPid(pid int, subpath string) (*os.File, error) {
|
||||
return proc.OpenRoot(strconv.Itoa(pid) + "/" + subpath)
|
||||
}
|
||||
|
||||
// checkSubpathOvermount checks if the dirfd and path combination is on the
|
||||
// same mount as the given root.
|
||||
func checkSubpathOvermount(root, dir fd.Fd, path string) error {
|
||||
// Get the mntID of our procfs handle.
|
||||
expectedMountID, err := fd.GetMountID(root, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get root mount id: %w", err)
|
||||
}
|
||||
// Get the mntID of the target magic-link.
|
||||
gotMountID, err := fd.GetMountID(dir, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get subpath mount id: %w", err)
|
||||
}
|
||||
// As long as the directory mount is alive, even with wrapping mount IDs,
|
||||
// we would expect to see a different mount ID here. (Of course, if we're
|
||||
// using unsafeHostProcRoot() then an attaker could change this after we
|
||||
// did this check.)
|
||||
if expectedMountID != gotMountID {
|
||||
return fmt.Errorf("%w: subpath %s/%s has an overmount obscuring the real path (mount ids do not match %d != %d)",
|
||||
errUnsafeProcfs, dir.Name(), path, expectedMountID, gotMountID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Readlink performs a readlink operation on "/proc/<base>/<subpath>" in a way
|
||||
// that should be free from race attacks. This is most commonly used to get the
|
||||
// real path of a file by looking at "/proc/self/fd/$n", with the same safety
|
||||
// protections as [Open] (as well as some additional checks against
|
||||
// overmounts).
|
||||
func (proc *Handle) Readlink(base procfsBase, subpath string) (string, error) {
|
||||
link, closer, err := proc.open(base, subpath)
|
||||
if closer != nil {
|
||||
defer closer()
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get safe %s/%s handle: %w", base, subpath, err)
|
||||
}
|
||||
defer link.Close() //nolint:errcheck // close failures aren't critical here
|
||||
|
||||
// Try to detect if there is a mount on top of the magic-link. This should
|
||||
// be safe in general (a mount on top of the path afterwards would not
|
||||
// affect the handle itself) and will definitely be safe if we are using
|
||||
// privateProcRoot() (at least since Linux 5.12[1], when anonymous mount
|
||||
// namespaces were completely isolated from external mounts including mount
|
||||
// propagation events).
|
||||
//
|
||||
// [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts
|
||||
// onto targets that reside on shared mounts").
|
||||
if err := checkSubpathOvermount(proc.Inner, link, ""); err != nil {
|
||||
return "", fmt.Errorf("check safety of %s/%s magiclink: %w", base, subpath, err)
|
||||
}
|
||||
|
||||
// readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See Linux commit
|
||||
// 65cfc6722361 ("readlinkat(), fchownat() and fstatat() with empty
|
||||
// relative pathnames").
|
||||
return fd.Readlinkat(link, "")
|
||||
}
|
||||
|
||||
// ProcSelfFdReadlink gets the real path of the given file by looking at
|
||||
// readlink(/proc/thread-self/fd/$n).
|
||||
//
|
||||
// This is just a wrapper around [Handle.Readlink].
|
||||
func ProcSelfFdReadlink(fd fd.Fd) (string, error) {
|
||||
procRoot, err := OpenProcRoot() // subset=pid
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer procRoot.Close() //nolint:errcheck // close failures aren't critical here
|
||||
|
||||
fdPath := "fd/" + strconv.Itoa(int(fd.Fd()))
|
||||
return procRoot.Readlink(ProcThreadSelf, fdPath)
|
||||
}
|
||||
|
||||
// CheckProcSelfFdPath returns whether the given file handle matches the
|
||||
// expected path. (This is inherently racy.)
|
||||
func CheckProcSelfFdPath(path string, file fd.Fd) error {
|
||||
if err := fd.IsDeadInode(file); err != nil {
|
||||
return err
|
||||
}
|
||||
actualPath, err := ProcSelfFdReadlink(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get path of handle: %w", err)
|
||||
}
|
||||
if actualPath != path {
|
||||
return fmt.Errorf("%w: handle path %q doesn't match expected path %q", internal.ErrPossibleBreakout, actualPath, path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReopenFd takes an existing file descriptor and "re-opens" it through
|
||||
// /proc/thread-self/fd/<fd>. This allows for O_PATH file descriptors to be
|
||||
// upgraded to regular file descriptors, as well as changing the open mode of a
|
||||
// regular file descriptor. Some filesystems have unique handling of open(2)
|
||||
// which make this incredibly useful (such as /dev/ptmx).
|
||||
func ReopenFd(handle fd.Fd, flags int) (*os.File, error) {
|
||||
procRoot, err := OpenProcRoot() // subset=pid
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer procRoot.Close() //nolint:errcheck // close failures aren't critical here
|
||||
|
||||
// We can't operate on /proc/thread-self/fd/$n directly when doing a
|
||||
// re-open, so we need to open /proc/thread-self/fd and then open a single
|
||||
// final component.
|
||||
procFdDir, closer, err := procRoot.OpenThreadSelf("fd/")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get safe /proc/thread-self/fd handle: %w", err)
|
||||
}
|
||||
defer procFdDir.Close() //nolint:errcheck // close failures aren't critical here
|
||||
defer closer()
|
||||
|
||||
// Try to detect if there is a mount on top of the magic-link we are about
|
||||
// to open. If we are using unsafeHostProcRoot(), this could change after
|
||||
// we check it (and there's nothing we can do about that) but for
|
||||
// privateProcRoot() this should be guaranteed to be safe (at least since
|
||||
// Linux 5.12[1], when anonymous mount namespaces were completely isolated
|
||||
// from external mounts including mount propagation events).
|
||||
//
|
||||
// [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts
|
||||
// onto targets that reside on shared mounts").
|
||||
fdStr := strconv.Itoa(int(handle.Fd()))
|
||||
if err := checkSubpathOvermount(procRoot.Inner, procFdDir, fdStr); err != nil {
|
||||
return nil, fmt.Errorf("check safety of /proc/thread-self/fd/%s magiclink: %w", fdStr, err)
|
||||
}
|
||||
|
||||
flags |= unix.O_CLOEXEC
|
||||
// Rather than just wrapping fd.Openat, open-code it so we can copy
|
||||
// handle.Name().
|
||||
reopenFd, err := unix.Openat(int(procFdDir.Fd()), fdStr, flags, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reopen fd %d: %w", handle.Fd(), err)
|
||||
}
|
||||
return os.NewFile(uintptr(reopenFd), handle.Name()), nil
|
||||
}
|
||||
|
||||
// Test hooks used in the procfs tests to verify that the fallback logic works.
|
||||
// See testing_mocks_linux_test.go and procfs_linux_test.go for more details.
|
||||
var (
|
||||
hookForcePrivateProcRootOpenTree = hookDummyFile
|
||||
hookForcePrivateProcRootOpenTreeAtRecursive = hookDummyFile
|
||||
hookForceGetProcRootUnsafe = hookDummy
|
||||
|
||||
hookForceProcSelfTask = hookDummy
|
||||
hookForceProcSelf = hookDummy
|
||||
)
|
||||
|
||||
func hookDummy() bool { return false }
|
||||
func hookDummyFile(_ io.Closer) bool { return false }
|
||||
Generated
Vendored
-222
@@ -1,222 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// This code is adapted to be a minimal version of the libpathrs proc resolver
|
||||
// <https://github.com/opensuse/libpathrs/blob/v0.1.3/src/resolvers/procfs.rs>.
|
||||
// As we only need O_PATH|O_NOFOLLOW support, this is not too much to port.
|
||||
|
||||
package procfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/internal/consts"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux"
|
||||
)
|
||||
|
||||
// procfsLookupInRoot is a stripped down version of completeLookupInRoot,
|
||||
// entirely designed to support the very small set of features necessary to
|
||||
// make procfs handling work. Unlike completeLookupInRoot, we always have
|
||||
// O_PATH|O_NOFOLLOW behaviour for trailing symlinks.
|
||||
//
|
||||
// The main restrictions are:
|
||||
//
|
||||
// - ".." is not supported (as it requires either os.Root-style replays,
|
||||
// which is more bug-prone; or procfs verification, which is not possible
|
||||
// due to re-entrancy issues).
|
||||
// - Absolute symlinks for the same reason (and all absolute symlinks in
|
||||
// procfs are magic-links, which we want to skip anyway).
|
||||
// - If statx is supported (checkSymlinkOvermount), any mount-point crossings
|
||||
// (which is the main attack of concern against /proc).
|
||||
// - Partial lookups are not supported, so the symlink stack is not needed.
|
||||
// - Trailing slash special handling is not necessary in most cases (if we
|
||||
// operating on procfs, it's usually with programmer-controlled strings
|
||||
// that will then be re-opened), so we skip it since whatever re-opens it
|
||||
// can deal with it. It's a creature comfort anyway.
|
||||
//
|
||||
// If the system supports openat2(), this is implemented using equivalent flags
|
||||
// (RESOLVE_BENEATH | RESOLVE_NO_XDEV | RESOLVE_NO_MAGICLINKS).
|
||||
func procfsLookupInRoot(procRoot fd.Fd, unsafePath string) (Handle *os.File, _ error) {
|
||||
unsafePath = filepath.ToSlash(unsafePath) // noop
|
||||
|
||||
// Make sure that an empty unsafe path still returns something sane, even
|
||||
// with openat2 (which doesn't have AT_EMPTY_PATH semantics yet).
|
||||
if unsafePath == "" {
|
||||
unsafePath = "."
|
||||
}
|
||||
|
||||
// This is already checked by getProcRoot, but make sure here since the
|
||||
// core security of this lookup is based on this assumption.
|
||||
if err := verifyProcRoot(procRoot); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if linux.HasOpenat2() {
|
||||
// We prefer being able to use RESOLVE_NO_XDEV if we can, to be
|
||||
// absolutely sure we are operating on a clean /proc handle that
|
||||
// doesn't have any cheeky overmounts that could trick us (including
|
||||
// symlink mounts on top of /proc/thread-self). RESOLVE_BENEATH isn't
|
||||
// strictly needed, but just use it since we have it.
|
||||
//
|
||||
// NOTE: /proc/self is technically a magic-link (the contents of the
|
||||
// symlink are generated dynamically), but it doesn't use
|
||||
// nd_jump_link() so RESOLVE_NO_MAGICLINKS allows it.
|
||||
//
|
||||
// TODO: It would be nice to have RESOLVE_NO_DOTDOT, purely for
|
||||
// self-consistency with the backup O_PATH resolver.
|
||||
handle, err := fd.Openat2(procRoot, unsafePath, &unix.OpenHow{
|
||||
Flags: unix.O_PATH | unix.O_NOFOLLOW | unix.O_CLOEXEC,
|
||||
Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_XDEV | unix.RESOLVE_NO_MAGICLINKS,
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Once we bump the minimum Go version to 1.20, we can use
|
||||
// multiple %w verbs for this wrapping. For now we need to use a
|
||||
// compatibility shim for older Go versions.
|
||||
// err = fmt.Errorf("%w: %w", errUnsafeProcfs, err)
|
||||
return nil, gocompat.WrapBaseError(err, errUnsafeProcfs)
|
||||
}
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
// To mirror openat2(RESOLVE_BENEATH), we need to return an error if the
|
||||
// path is absolute.
|
||||
if path.IsAbs(unsafePath) {
|
||||
return nil, fmt.Errorf("%w: cannot resolve absolute paths in procfs resolver", internal.ErrPossibleBreakout)
|
||||
}
|
||||
|
||||
currentDir, err := fd.Dup(procRoot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("clone root fd: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
// If a handle is not returned, close the internal handle.
|
||||
if Handle == nil {
|
||||
_ = currentDir.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var (
|
||||
linksWalked int
|
||||
currentPath string
|
||||
remainingPath = unsafePath
|
||||
)
|
||||
for remainingPath != "" {
|
||||
// Get the next path component.
|
||||
var part string
|
||||
if i := strings.IndexByte(remainingPath, '/'); i == -1 {
|
||||
part, remainingPath = remainingPath, ""
|
||||
} else {
|
||||
part, remainingPath = remainingPath[:i], remainingPath[i+1:]
|
||||
}
|
||||
if part == "" {
|
||||
// no-op component, but treat it the same as "."
|
||||
part = "."
|
||||
}
|
||||
if part == ".." {
|
||||
// not permitted
|
||||
return nil, fmt.Errorf("%w: cannot walk into '..' in procfs resolver", internal.ErrPossibleBreakout)
|
||||
}
|
||||
|
||||
// Apply the component lexically to the path we are building.
|
||||
// currentPath does not contain any symlinks, and we are lexically
|
||||
// dealing with a single component, so it's okay to do a filepath.Clean
|
||||
// here. (Not to mention that ".." isn't allowed.)
|
||||
nextPath := path.Join("/", currentPath, part)
|
||||
// If we logically hit the root, just clone the root rather than
|
||||
// opening the part and doing all of the other checks.
|
||||
if nextPath == "/" {
|
||||
// Jump to root.
|
||||
rootClone, err := fd.Dup(procRoot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("clone root fd: %w", err)
|
||||
}
|
||||
_ = currentDir.Close()
|
||||
currentDir = rootClone
|
||||
currentPath = nextPath
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to open the next component.
|
||||
nextDir, err := fd.Openat(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure we are still on procfs and haven't crossed mounts.
|
||||
if err := verifyProcHandle(nextDir); err != nil {
|
||||
_ = nextDir.Close()
|
||||
return nil, fmt.Errorf("check %q component is on procfs: %w", part, err)
|
||||
}
|
||||
if err := checkSubpathOvermount(procRoot, nextDir, ""); err != nil {
|
||||
_ = nextDir.Close()
|
||||
return nil, fmt.Errorf("check %q component is not overmounted: %w", part, err)
|
||||
}
|
||||
|
||||
// We are emulating O_PATH|O_NOFOLLOW, so we only need to traverse into
|
||||
// trailing symlinks if we are not the final component. Otherwise we
|
||||
// can just return the currentDir.
|
||||
if remainingPath != "" {
|
||||
st, err := nextDir.Stat()
|
||||
if err != nil {
|
||||
_ = nextDir.Close()
|
||||
return nil, fmt.Errorf("stat component %q: %w", part, err)
|
||||
}
|
||||
|
||||
if st.Mode()&os.ModeType == os.ModeSymlink {
|
||||
// readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See
|
||||
// Linux commit 65cfc6722361 ("readlinkat(), fchownat() and
|
||||
// fstatat() with empty relative pathnames").
|
||||
linkDest, err := fd.Readlinkat(nextDir, "")
|
||||
// We don't need the handle anymore.
|
||||
_ = nextDir.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
linksWalked++
|
||||
if linksWalked > consts.MaxSymlinkLimit {
|
||||
return nil, &os.PathError{Op: "securejoin.procfsLookupInRoot", Path: "/proc/" + unsafePath, Err: unix.ELOOP}
|
||||
}
|
||||
|
||||
// Update our logical remaining path.
|
||||
remainingPath = linkDest + "/" + remainingPath
|
||||
// Absolute symlinks are probably magiclinks, we reject them.
|
||||
if path.IsAbs(linkDest) {
|
||||
return nil, fmt.Errorf("%w: cannot jump to / in procfs resolver -- possible magiclink", internal.ErrPossibleBreakout)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Walk into the next component.
|
||||
_ = currentDir.Close()
|
||||
currentDir = nextDir
|
||||
currentPath = nextPath
|
||||
}
|
||||
|
||||
// One final sanity-check.
|
||||
if err := verifyProcHandle(currentDir); err != nil {
|
||||
return nil, fmt.Errorf("check final handle is on procfs: %w", err)
|
||||
}
|
||||
if err := checkSubpathOvermount(procRoot, currentDir, ""); err != nil {
|
||||
return nil, fmt.Errorf("check final handle is not overmounted: %w", err)
|
||||
}
|
||||
return currentDir, nil
|
||||
}
|
||||
-399
@@ -1,399 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package pathrs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/internal/consts"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs"
|
||||
)
|
||||
|
||||
type symlinkStackEntry struct {
|
||||
// (dir, remainingPath) is what we would've returned if the link didn't
|
||||
// exist. This matches what openat2(RESOLVE_IN_ROOT) would return in
|
||||
// this case.
|
||||
dir *os.File
|
||||
remainingPath string
|
||||
// linkUnwalked is the remaining path components from the original
|
||||
// Readlink which we have yet to walk. When this slice is empty, we
|
||||
// drop the link from the stack.
|
||||
linkUnwalked []string
|
||||
}
|
||||
|
||||
func (se symlinkStackEntry) String() string {
|
||||
return fmt.Sprintf("<%s>/%s [->%s]", se.dir.Name(), se.remainingPath, strings.Join(se.linkUnwalked, "/"))
|
||||
}
|
||||
|
||||
func (se symlinkStackEntry) Close() {
|
||||
_ = se.dir.Close()
|
||||
}
|
||||
|
||||
type symlinkStack []*symlinkStackEntry
|
||||
|
||||
func (s *symlinkStack) IsEmpty() bool {
|
||||
return s == nil || len(*s) == 0
|
||||
}
|
||||
|
||||
func (s *symlinkStack) Close() {
|
||||
if s != nil {
|
||||
for _, link := range *s {
|
||||
link.Close()
|
||||
}
|
||||
// TODO: Switch to clear once we switch to Go 1.21.
|
||||
*s = nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
errEmptyStack = errors.New("[internal] stack is empty")
|
||||
errBrokenSymlinkStack = errors.New("[internal error] broken symlink stack")
|
||||
)
|
||||
|
||||
func (s *symlinkStack) popPart(part string) error {
|
||||
if s == nil || s.IsEmpty() {
|
||||
// If there is nothing in the symlink stack, then the part was from the
|
||||
// real path provided by the user, and this is a no-op.
|
||||
return errEmptyStack
|
||||
}
|
||||
if part == "." {
|
||||
// "." components are no-ops -- we drop them when doing SwapLink.
|
||||
return nil
|
||||
}
|
||||
|
||||
tailEntry := (*s)[len(*s)-1]
|
||||
|
||||
// Double-check that we are popping the component we expect.
|
||||
if len(tailEntry.linkUnwalked) == 0 {
|
||||
return fmt.Errorf("%w: trying to pop component %q of empty stack entry %s", errBrokenSymlinkStack, part, tailEntry)
|
||||
}
|
||||
headPart := tailEntry.linkUnwalked[0]
|
||||
if headPart != part {
|
||||
return fmt.Errorf("%w: trying to pop component %q but the last stack entry is %s (%q)", errBrokenSymlinkStack, part, tailEntry, headPart)
|
||||
}
|
||||
|
||||
// Drop the component, but keep the entry around in case we are dealing
|
||||
// with a "tail-chained" symlink.
|
||||
tailEntry.linkUnwalked = tailEntry.linkUnwalked[1:]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *symlinkStack) PopPart(part string) error {
|
||||
if err := s.popPart(part); err != nil {
|
||||
if errors.Is(err, errEmptyStack) {
|
||||
// Skip empty stacks.
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Clean up any of the trailing stack entries that are empty.
|
||||
for lastGood := len(*s) - 1; lastGood >= 0; lastGood-- {
|
||||
entry := (*s)[lastGood]
|
||||
if len(entry.linkUnwalked) > 0 {
|
||||
break
|
||||
}
|
||||
entry.Close()
|
||||
(*s) = (*s)[:lastGood]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *symlinkStack) push(dir *os.File, remainingPath, linkTarget string) error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
// Split the link target and clean up any "" parts.
|
||||
linkTargetParts := gocompat.SlicesDeleteFunc(
|
||||
strings.Split(linkTarget, "/"),
|
||||
func(part string) bool { return part == "" || part == "." })
|
||||
|
||||
// Copy the directory so the caller doesn't close our copy.
|
||||
dirCopy, err := fd.Dup(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add to the stack.
|
||||
*s = append(*s, &symlinkStackEntry{
|
||||
dir: dirCopy,
|
||||
remainingPath: remainingPath,
|
||||
linkUnwalked: linkTargetParts,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *symlinkStack) SwapLink(linkPart string, dir *os.File, remainingPath, linkTarget string) error {
|
||||
// If we are currently inside a symlink resolution, remove the symlink
|
||||
// component from the last symlink entry, but don't remove the entry even
|
||||
// if it's empty. If we are a "tail-chained" symlink (a trailing symlink we
|
||||
// hit during a symlink resolution) we need to keep the old symlink until
|
||||
// we finish the resolution.
|
||||
if err := s.popPart(linkPart); err != nil {
|
||||
if !errors.Is(err, errEmptyStack) {
|
||||
return err
|
||||
}
|
||||
// Push the component regardless of whether the stack was empty.
|
||||
}
|
||||
return s.push(dir, remainingPath, linkTarget)
|
||||
}
|
||||
|
||||
func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) {
|
||||
if s == nil || s.IsEmpty() {
|
||||
return nil, "", false
|
||||
}
|
||||
tailEntry := (*s)[0]
|
||||
*s = (*s)[1:]
|
||||
return tailEntry.dir, tailEntry.remainingPath, true
|
||||
}
|
||||
|
||||
// partialLookupInRoot tries to lookup as much of the request path as possible
|
||||
// within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing
|
||||
// component of the requested path, returning a file handle to the final
|
||||
// existing component and a string containing the remaining path components.
|
||||
func partialLookupInRoot(root fd.Fd, unsafePath string) (*os.File, string, error) {
|
||||
return lookupInRoot(root, unsafePath, true)
|
||||
}
|
||||
|
||||
func completeLookupInRoot(root fd.Fd, unsafePath string) (*os.File, error) {
|
||||
handle, remainingPath, err := lookupInRoot(root, unsafePath, false)
|
||||
if remainingPath != "" && err == nil {
|
||||
// should never happen
|
||||
err = fmt.Errorf("[bug] non-empty remaining path when doing a non-partial lookup: %q", remainingPath)
|
||||
}
|
||||
// lookupInRoot(partial=false) will always close the handle if an error is
|
||||
// returned, so no need to double-check here.
|
||||
return handle, err
|
||||
}
|
||||
|
||||
func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File, _ string, _ error) {
|
||||
unsafePath = filepath.ToSlash(unsafePath) // noop
|
||||
|
||||
// This is very similar to SecureJoin, except that we operate on the
|
||||
// components using file descriptors. We then return the last component we
|
||||
// managed open, along with the remaining path components not opened.
|
||||
|
||||
// Try to use openat2 if possible.
|
||||
if linux.HasOpenat2() {
|
||||
return lookupOpenat2(root, unsafePath, partial)
|
||||
}
|
||||
|
||||
// Get the "actual" root path from /proc/self/fd. This is necessary if the
|
||||
// root is some magic-link like /proc/$pid/root, in which case we want to
|
||||
// make sure when we do procfs.CheckProcSelfFdPath that we are using the
|
||||
// correct root path.
|
||||
logicalRootPath, err := procfs.ProcSelfFdReadlink(root)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("get real root path: %w", err)
|
||||
}
|
||||
|
||||
currentDir, err := fd.Dup(root)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("clone root fd: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
// If a handle is not returned, close the internal handle.
|
||||
if Handle == nil {
|
||||
_ = currentDir.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// symlinkStack is used to emulate how openat2(RESOLVE_IN_ROOT) treats
|
||||
// dangling symlinks. If we hit a non-existent path while resolving a
|
||||
// symlink, we need to return the (dir, remainingPath) that we had when we
|
||||
// hit the symlink (treating the symlink as though it were a regular file).
|
||||
// The set of (dir, remainingPath) sets is stored within the symlinkStack
|
||||
// and we add and remove parts when we hit symlink and non-symlink
|
||||
// components respectively. We need a stack because of recursive symlinks
|
||||
// (symlinks that contain symlink components in their target).
|
||||
//
|
||||
// Note that the stack is ONLY used for book-keeping. All of the actual
|
||||
// path walking logic is still based on currentPath/remainingPath and
|
||||
// currentDir (as in SecureJoin).
|
||||
var symStack *symlinkStack
|
||||
if partial {
|
||||
symStack = new(symlinkStack)
|
||||
defer symStack.Close()
|
||||
}
|
||||
|
||||
var (
|
||||
linksWalked int
|
||||
currentPath string
|
||||
remainingPath = unsafePath
|
||||
)
|
||||
for remainingPath != "" {
|
||||
// Save the current remaining path so if the part is not real we can
|
||||
// return the path including the component.
|
||||
oldRemainingPath := remainingPath
|
||||
|
||||
// Get the next path component.
|
||||
var part string
|
||||
if i := strings.IndexByte(remainingPath, '/'); i == -1 {
|
||||
part, remainingPath = remainingPath, ""
|
||||
} else {
|
||||
part, remainingPath = remainingPath[:i], remainingPath[i+1:]
|
||||
}
|
||||
// If we hit an empty component, we need to treat it as though it is
|
||||
// "." so that trailing "/" and "//" components on a non-directory
|
||||
// correctly return the right error code.
|
||||
if part == "" {
|
||||
part = "."
|
||||
}
|
||||
|
||||
// Apply the component lexically to the path we are building.
|
||||
// currentPath does not contain any symlinks, and we are lexically
|
||||
// dealing with a single component, so it's okay to do a filepath.Clean
|
||||
// here.
|
||||
nextPath := path.Join("/", currentPath, part)
|
||||
// If we logically hit the root, just clone the root rather than
|
||||
// opening the part and doing all of the other checks.
|
||||
if nextPath == "/" {
|
||||
if err := symStack.PopPart(part); err != nil {
|
||||
return nil, "", fmt.Errorf("walking into root with part %q failed: %w", part, err)
|
||||
}
|
||||
// Jump to root.
|
||||
rootClone, err := fd.Dup(root)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("clone root fd: %w", err)
|
||||
}
|
||||
_ = currentDir.Close()
|
||||
currentDir = rootClone
|
||||
currentPath = nextPath
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to open the next component.
|
||||
nextDir, err := fd.Openat(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
||||
switch err {
|
||||
case nil:
|
||||
st, err := nextDir.Stat()
|
||||
if err != nil {
|
||||
_ = nextDir.Close()
|
||||
return nil, "", fmt.Errorf("stat component %q: %w", part, err)
|
||||
}
|
||||
|
||||
switch st.Mode() & os.ModeType { //nolint:exhaustive // just a glorified if statement
|
||||
case os.ModeSymlink:
|
||||
// readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See
|
||||
// Linux commit 65cfc6722361 ("readlinkat(), fchownat() and
|
||||
// fstatat() with empty relative pathnames").
|
||||
linkDest, err := fd.Readlinkat(nextDir, "")
|
||||
// We don't need the handle anymore.
|
||||
_ = nextDir.Close()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
linksWalked++
|
||||
if linksWalked > consts.MaxSymlinkLimit {
|
||||
return nil, "", &os.PathError{Op: "securejoin.lookupInRoot", Path: logicalRootPath + "/" + unsafePath, Err: unix.ELOOP}
|
||||
}
|
||||
|
||||
// Swap out the symlink's component for the link entry itself.
|
||||
if err := symStack.SwapLink(part, currentDir, oldRemainingPath, linkDest); err != nil {
|
||||
return nil, "", fmt.Errorf("walking into symlink %q failed: push symlink: %w", part, err)
|
||||
}
|
||||
|
||||
// Update our logical remaining path.
|
||||
remainingPath = linkDest + "/" + remainingPath
|
||||
// Absolute symlinks reset any work we've already done.
|
||||
if path.IsAbs(linkDest) {
|
||||
// Jump to root.
|
||||
rootClone, err := fd.Dup(root)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("clone root fd: %w", err)
|
||||
}
|
||||
_ = currentDir.Close()
|
||||
currentDir = rootClone
|
||||
currentPath = "/"
|
||||
}
|
||||
|
||||
default:
|
||||
// If we are dealing with a directory, simply walk into it.
|
||||
_ = currentDir.Close()
|
||||
currentDir = nextDir
|
||||
currentPath = nextPath
|
||||
|
||||
// The part was real, so drop it from the symlink stack.
|
||||
if err := symStack.PopPart(part); err != nil {
|
||||
return nil, "", fmt.Errorf("walking into directory %q failed: %w", part, err)
|
||||
}
|
||||
|
||||
// If we are operating on a .., make sure we haven't escaped.
|
||||
// We only have to check for ".." here because walking down
|
||||
// into a regular component component cannot cause you to
|
||||
// escape. This mirrors the logic in RESOLVE_IN_ROOT, except we
|
||||
// have to check every ".." rather than only checking after a
|
||||
// rename or mount on the system.
|
||||
if part == ".." {
|
||||
// Make sure the root hasn't moved.
|
||||
if err := procfs.CheckProcSelfFdPath(logicalRootPath, root); err != nil {
|
||||
return nil, "", fmt.Errorf("root path moved during lookup: %w", err)
|
||||
}
|
||||
// Make sure the path is what we expect.
|
||||
fullPath := logicalRootPath + nextPath
|
||||
if err := procfs.CheckProcSelfFdPath(fullPath, currentDir); err != nil {
|
||||
return nil, "", fmt.Errorf("walking into %q had unexpected result: %w", part, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
if !partial {
|
||||
return nil, "", err
|
||||
}
|
||||
// If there are any remaining components in the symlink stack, we
|
||||
// are still within a symlink resolution and thus we hit a dangling
|
||||
// symlink. So pretend that the first symlink in the stack we hit
|
||||
// was an ENOENT (to match openat2).
|
||||
if oldDir, remainingPath, ok := symStack.PopTopSymlink(); ok {
|
||||
_ = currentDir.Close()
|
||||
return oldDir, remainingPath, err
|
||||
}
|
||||
// We have hit a final component that doesn't exist, so we have our
|
||||
// partial open result. Note that we have to use the OLD remaining
|
||||
// path, since the lookup failed.
|
||||
return currentDir, oldRemainingPath, err
|
||||
}
|
||||
}
|
||||
|
||||
// If the unsafePath had a trailing slash, we need to make sure we try to
|
||||
// do a relative "." open so that we will correctly return an error when
|
||||
// the final component is a non-directory (to match openat2). In the
|
||||
// context of openat2, a trailing slash and a trailing "/." are completely
|
||||
// equivalent.
|
||||
if strings.HasSuffix(unsafePath, "/") {
|
||||
nextDir, err := fd.Openat(currentDir, ".", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
if !partial {
|
||||
_ = currentDir.Close()
|
||||
currentDir = nil
|
||||
}
|
||||
return currentDir, "", err
|
||||
}
|
||||
_ = currentDir.Close()
|
||||
currentDir = nextDir
|
||||
}
|
||||
|
||||
// All of the components existed!
|
||||
return currentDir, "", nil
|
||||
}
|
||||
-246
@@ -1,246 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package pathrs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux"
|
||||
)
|
||||
|
||||
var errInvalidMode = errors.New("invalid permission mode")
|
||||
|
||||
// modePermExt is like os.ModePerm except that it also includes the set[ug]id
|
||||
// and sticky bits.
|
||||
const modePermExt = os.ModePerm | os.ModeSetuid | os.ModeSetgid | os.ModeSticky
|
||||
|
||||
//nolint:cyclop // this function needs to handle a lot of cases
|
||||
func toUnixMode(mode os.FileMode) (uint32, error) {
|
||||
sysMode := uint32(mode.Perm())
|
||||
if mode&os.ModeSetuid != 0 {
|
||||
sysMode |= unix.S_ISUID
|
||||
}
|
||||
if mode&os.ModeSetgid != 0 {
|
||||
sysMode |= unix.S_ISGID
|
||||
}
|
||||
if mode&os.ModeSticky != 0 {
|
||||
sysMode |= unix.S_ISVTX
|
||||
}
|
||||
// We don't allow file type bits.
|
||||
if mode&os.ModeType != 0 {
|
||||
return 0, fmt.Errorf("%w %+.3o (%s): type bits not permitted", errInvalidMode, mode, mode)
|
||||
}
|
||||
// We don't allow other unknown modes.
|
||||
if mode&^modePermExt != 0 || sysMode&unix.S_IFMT != 0 {
|
||||
return 0, fmt.Errorf("%w %+.3o (%s): unknown mode bits", errInvalidMode, mode, mode)
|
||||
}
|
||||
return sysMode, nil
|
||||
}
|
||||
|
||||
// MkdirAllHandle is equivalent to [MkdirAll], except that it is safer to use
|
||||
// in two respects:
|
||||
//
|
||||
// - The caller provides the root directory as an *[os.File] (preferably O_PATH)
|
||||
// handle. This means that the caller can be sure which root directory is
|
||||
// being used. Note that this can be emulated by using /proc/self/fd/... as
|
||||
// the root path with [os.MkdirAll].
|
||||
//
|
||||
// - Once all of the directories have been created, an *[os.File] O_PATH handle
|
||||
// to the directory at unsafePath is returned to the caller. This is done in
|
||||
// an effectively-race-free way (an attacker would only be able to swap the
|
||||
// final directory component), which is not possible to emulate with
|
||||
// [MkdirAll].
|
||||
//
|
||||
// In addition, the returned handle is obtained far more efficiently than doing
|
||||
// a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after
|
||||
// doing [MkdirAll]. If you intend to open the directory after creating it, you
|
||||
// should use MkdirAllHandle.
|
||||
//
|
||||
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
|
||||
func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.File, Err error) {
|
||||
unixMode, err := toUnixMode(mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// On Linux, mkdirat(2) (and os.Mkdir) silently ignore the suid and sgid
|
||||
// bits. We could also silently ignore them but since we have very few
|
||||
// users it seems more prudent to return an error so users notice that
|
||||
// these bits will not be set.
|
||||
if unixMode&^0o1777 != 0 {
|
||||
return nil, fmt.Errorf("%w for mkdir %+.3o: suid and sgid are ignored by mkdir", errInvalidMode, mode)
|
||||
}
|
||||
|
||||
// Try to open as much of the path as possible.
|
||||
currentDir, remainingPath, err := partialLookupInRoot(root, unsafePath)
|
||||
defer func() {
|
||||
if Err != nil {
|
||||
_ = currentDir.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil && !errors.Is(err, unix.ENOENT) {
|
||||
return nil, fmt.Errorf("find existing subpath of %q: %w", unsafePath, err)
|
||||
}
|
||||
|
||||
// If there is an attacker deleting directories as we walk into them,
|
||||
// detect this proactively. Note this is guaranteed to detect if the
|
||||
// attacker deleted any part of the tree up to currentDir.
|
||||
//
|
||||
// Once we walk into a dead directory, partialLookupInRoot would not be
|
||||
// able to walk further down the tree (directories must be empty before
|
||||
// they are deleted), and if the attacker has removed the entire tree we
|
||||
// can be sure that anything that was originally inside a dead directory
|
||||
// must also be deleted and thus is a dead directory in its own right.
|
||||
//
|
||||
// This is mostly a quality-of-life check, because mkdir will simply fail
|
||||
// later if the attacker deletes the tree after this check.
|
||||
if err := fd.IsDeadInode(currentDir); err != nil {
|
||||
return nil, fmt.Errorf("finding existing subpath of %q: %w", unsafePath, err)
|
||||
}
|
||||
|
||||
// Re-open the path to match the O_DIRECTORY reopen loop later (so that we
|
||||
// always return a non-O_PATH handle). We also check that we actually got a
|
||||
// directory.
|
||||
if reopenDir, err := Reopen(currentDir, unix.O_DIRECTORY|unix.O_CLOEXEC); errors.Is(err, unix.ENOTDIR) {
|
||||
return nil, fmt.Errorf("cannot create subdirectories in %q: %w", currentDir.Name(), unix.ENOTDIR)
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("re-opening handle to %q: %w", currentDir.Name(), err)
|
||||
} else { //nolint:revive // indent-error-flow lint doesn't make sense here
|
||||
_ = currentDir.Close()
|
||||
currentDir = reopenDir
|
||||
}
|
||||
|
||||
remainingParts := strings.Split(remainingPath, string(filepath.Separator))
|
||||
if gocompat.SlicesContains(remainingParts, "..") {
|
||||
// The path contained ".." components after the end of the "real"
|
||||
// components. We could try to safely resolve ".." here but that would
|
||||
// add a bunch of extra logic for something that it's not clear even
|
||||
// needs to be supported. So just return an error.
|
||||
//
|
||||
// If we do filepath.Clean(remainingPath) then we end up with the
|
||||
// problem that ".." can erase a trailing dangling symlink and produce
|
||||
// a path that doesn't quite match what the user asked for.
|
||||
return nil, fmt.Errorf("%w: yet-to-be-created path %q contains '..' components", unix.ENOENT, remainingPath)
|
||||
}
|
||||
|
||||
// Create the remaining components.
|
||||
for _, part := range remainingParts {
|
||||
switch part {
|
||||
case "", ".":
|
||||
// Skip over no-op paths.
|
||||
continue
|
||||
}
|
||||
|
||||
// NOTE: mkdir(2) will not follow trailing symlinks, so we can safely
|
||||
// create the final component without worrying about symlink-exchange
|
||||
// attacks.
|
||||
//
|
||||
// If we get -EEXIST, it's possible that another program created the
|
||||
// directory at the same time as us. In that case, just continue on as
|
||||
// if we created it (if the created inode is not a directory, the
|
||||
// following open call will fail).
|
||||
if err := unix.Mkdirat(int(currentDir.Fd()), part, unixMode); err != nil && !errors.Is(err, unix.EEXIST) {
|
||||
err = &os.PathError{Op: "mkdirat", Path: currentDir.Name() + "/" + part, Err: err}
|
||||
// Make the error a bit nicer if the directory is dead.
|
||||
if deadErr := fd.IsDeadInode(currentDir); deadErr != nil {
|
||||
// TODO: Once we bump the minimum Go version to 1.20, we can use
|
||||
// multiple %w verbs for this wrapping. For now we need to use a
|
||||
// compatibility shim for older Go versions.
|
||||
// err = fmt.Errorf("%w (%w)", err, deadErr)
|
||||
err = gocompat.WrapBaseError(err, deadErr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get a handle to the next component. O_DIRECTORY means we don't need
|
||||
// to use O_PATH.
|
||||
var nextDir *os.File
|
||||
if linux.HasOpenat2() {
|
||||
nextDir, err = openat2(currentDir, part, &unix.OpenHow{
|
||||
Flags: unix.O_NOFOLLOW | unix.O_DIRECTORY | unix.O_CLOEXEC,
|
||||
Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_XDEV,
|
||||
})
|
||||
} else {
|
||||
nextDir, err = fd.Openat(currentDir, part, unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = currentDir.Close()
|
||||
currentDir = nextDir
|
||||
|
||||
// It's possible that the directory we just opened was swapped by an
|
||||
// attacker. Unfortunately there isn't much we can do to protect
|
||||
// against this, and MkdirAll's behaviour is that we will reuse
|
||||
// existing directories anyway so the need to protect against this is
|
||||
// incredibly limited (and arguably doesn't even deserve mention here).
|
||||
//
|
||||
// Ideally we might want to check that the owner and mode match what we
|
||||
// would've created -- unfortunately, it is non-trivial to verify that
|
||||
// the owner and mode of the created directory match. While plain Unix
|
||||
// DAC rules seem simple enough to emulate, there are a bunch of other
|
||||
// factors that can change the mode or owner of created directories
|
||||
// (default POSIX ACLs, mount options like uid=1,gid=2,umask=0 on
|
||||
// filesystems like vfat, etc etc). We used to try to verify this but
|
||||
// it just lead to a series of spurious errors.
|
||||
//
|
||||
// We could also check that the directory is non-empty, but
|
||||
// unfortunately some pseduofilesystems (like cgroupfs) create
|
||||
// non-empty directories, which would result in different spurious
|
||||
// errors.
|
||||
}
|
||||
return currentDir, nil
|
||||
}
|
||||
|
||||
// MkdirAll is a race-safe alternative to the [os.MkdirAll] function,
|
||||
// where the new directory is guaranteed to be within the root directory (if an
|
||||
// attacker can move directories from inside the root to outside the root, the
|
||||
// created directory tree might be outside of the root but the key constraint
|
||||
// is that at no point will we walk outside of the directory tree we are
|
||||
// creating).
|
||||
//
|
||||
// Effectively, MkdirAll(root, unsafePath, mode) is equivalent to
|
||||
//
|
||||
// path, _ := securejoin.SecureJoin(root, unsafePath)
|
||||
// err := os.MkdirAll(path, mode)
|
||||
//
|
||||
// But is much safer. The above implementation is unsafe because if an attacker
|
||||
// can modify the filesystem tree between [SecureJoin] and [os.MkdirAll], it is
|
||||
// possible for MkdirAll to resolve unsafe symlink components and create
|
||||
// directories outside of the root.
|
||||
//
|
||||
// If you plan to open the directory after you have created it or want to use
|
||||
// an open directory handle as the root, you should use [MkdirAllHandle] instead.
|
||||
// This function is a wrapper around [MkdirAllHandle].
|
||||
//
|
||||
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
|
||||
func MkdirAll(root, unsafePath string, mode os.FileMode) error {
|
||||
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rootDir.Close() //nolint:errcheck // close failures aren't critical here
|
||||
|
||||
f, err := MkdirAllHandle(rootDir, unsafePath, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = f.Close()
|
||||
return nil
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package pathrs
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs"
|
||||
)
|
||||
|
||||
// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
|
||||
// using an *[os.File] handle, to ensure that the correct root directory is used.
|
||||
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) {
|
||||
handle, err := completeLookupInRoot(root, unsafePath)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: err}
|
||||
}
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
// OpenInRoot safely opens the provided unsafePath within the root.
|
||||
// Effectively, OpenInRoot(root, unsafePath) is equivalent to
|
||||
//
|
||||
// path, _ := securejoin.SecureJoin(root, unsafePath)
|
||||
// handle, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC)
|
||||
//
|
||||
// But is much safer. The above implementation is unsafe because if an attacker
|
||||
// can modify the filesystem tree between [SecureJoin] and [os.OpenFile], it is
|
||||
// possible for the returned file to be outside of the root.
|
||||
//
|
||||
// Note that the returned handle is an O_PATH handle, meaning that only a very
|
||||
// limited set of operations will work on the handle. This is done to avoid
|
||||
// accidentally opening an untrusted file that could cause issues (such as a
|
||||
// disconnected TTY that could cause a DoS, or some other issue). In order to
|
||||
// use the returned handle, you can "upgrade" it to a proper handle using
|
||||
// [Reopen].
|
||||
//
|
||||
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
|
||||
func OpenInRoot(root, unsafePath string) (*os.File, error) {
|
||||
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rootDir.Close() //nolint:errcheck // close failures aren't critical here
|
||||
return OpenatInRoot(rootDir, unsafePath)
|
||||
}
|
||||
|
||||
// Reopen takes an *[os.File] handle and re-opens it through /proc/self/fd.
|
||||
// Reopen(file, flags) is effectively equivalent to
|
||||
//
|
||||
// fdPath := fmt.Sprintf("/proc/self/fd/%d", file.Fd())
|
||||
// os.OpenFile(fdPath, flags|unix.O_CLOEXEC)
|
||||
//
|
||||
// But with some extra hardenings to ensure that we are not tricked by a
|
||||
// maliciously-configured /proc mount. While this attack scenario is not
|
||||
// common, in container runtimes it is possible for higher-level runtimes to be
|
||||
// tricked into configuring an unsafe /proc that can be used to attack file
|
||||
// operations. See [CVE-2019-19921] for more details.
|
||||
//
|
||||
// [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw
|
||||
func Reopen(handle *os.File, flags int) (*os.File, error) {
|
||||
return procfs.ReopenFd(handle, flags)
|
||||
}
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package pathrs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/procfs"
|
||||
)
|
||||
|
||||
func openat2(dir fd.Fd, path string, how *unix.OpenHow) (*os.File, error) {
|
||||
file, err := fd.Openat2(dir, path, how)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If we are using RESOLVE_IN_ROOT, the name we generated may be wrong.
|
||||
if how.Resolve&unix.RESOLVE_IN_ROOT == unix.RESOLVE_IN_ROOT {
|
||||
if actualPath, err := procfs.ProcSelfFdReadlink(file); err == nil {
|
||||
// TODO: Ideally we would not need to dup the fd, but you cannot
|
||||
// easily just swap an *os.File with one from the same fd
|
||||
// (the GC will close the old one, and you cannot clear the
|
||||
// finaliser easily because it is associated with an internal
|
||||
// field of *os.File not *os.File itself).
|
||||
newFile, err := fd.DupWithName(file, actualPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file = newFile
|
||||
}
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func lookupOpenat2(root fd.Fd, unsafePath string, partial bool) (*os.File, string, error) {
|
||||
if !partial {
|
||||
file, err := openat2(root, unsafePath, &unix.OpenHow{
|
||||
Flags: unix.O_PATH | unix.O_CLOEXEC,
|
||||
Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
|
||||
})
|
||||
return file, "", err
|
||||
}
|
||||
return partialLookupOpenat2(root, unsafePath)
|
||||
}
|
||||
|
||||
// partialLookupOpenat2 is an alternative implementation of
|
||||
// partialLookupInRoot, using openat2(RESOLVE_IN_ROOT) to more safely get a
|
||||
// handle to the deepest existing child of the requested path within the root.
|
||||
func partialLookupOpenat2(root fd.Fd, unsafePath string) (*os.File, string, error) {
|
||||
// TODO: Implement this as a git-bisect-like binary search.
|
||||
|
||||
unsafePath = filepath.ToSlash(unsafePath) // noop
|
||||
endIdx := len(unsafePath)
|
||||
var lastError error
|
||||
for endIdx > 0 {
|
||||
subpath := unsafePath[:endIdx]
|
||||
|
||||
handle, err := openat2(root, subpath, &unix.OpenHow{
|
||||
Flags: unix.O_PATH | unix.O_CLOEXEC,
|
||||
Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
|
||||
})
|
||||
if err == nil {
|
||||
// Jump over the slash if we have a non-"" remainingPath.
|
||||
if endIdx < len(unsafePath) {
|
||||
endIdx++
|
||||
}
|
||||
// We found a subpath!
|
||||
return handle, unsafePath[endIdx:], lastError
|
||||
}
|
||||
if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) {
|
||||
// That path doesn't exist, let's try the next directory up.
|
||||
endIdx = strings.LastIndexByte(subpath, '/')
|
||||
lastError = err
|
||||
continue
|
||||
}
|
||||
return nil, "", fmt.Errorf("open subpath: %w", err)
|
||||
}
|
||||
// If we couldn't open anything, the whole subpath is missing. Return a
|
||||
// copy of the root fd so that the caller doesn't close this one by
|
||||
// accident.
|
||||
rootClone, err := fd.Dup(root)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return rootClone, unsafePath, lastError
|
||||
}
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Package procfs provides a safe API for operating on /proc on Linux.
|
||||
package procfs
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs"
|
||||
)
|
||||
|
||||
// This package mostly just wraps internal/procfs APIs. This is necessary
|
||||
// because we are forced to export some things from internal/procfs in order to
|
||||
// avoid some dependency cycle issues, but we don't want users to see or use
|
||||
// them.
|
||||
|
||||
// ProcThreadSelfCloser is a callback that needs to be called when you are done
|
||||
// operating on an [os.File] fetched using [Handle.OpenThreadSelf].
|
||||
//
|
||||
// [os.File]: https://pkg.go.dev/os#File
|
||||
type ProcThreadSelfCloser = procfs.ProcThreadSelfCloser
|
||||
|
||||
// Handle is a wrapper around an *os.File handle to "/proc", which can be used
|
||||
// to do further procfs-related operations in a safe way.
|
||||
type Handle struct {
|
||||
inner *procfs.Handle
|
||||
}
|
||||
|
||||
// Close close the resources associated with this [Handle]. Note that if this
|
||||
// [Handle] was created with [OpenProcRoot], on some kernels the underlying
|
||||
// procfs handle is cached and so this Close operation may be a no-op. However,
|
||||
// you should always call Close on [Handle]s once you are done with them.
|
||||
func (proc *Handle) Close() error { return proc.inner.Close() }
|
||||
|
||||
// OpenProcRoot tries to open a "safer" handle to "/proc" (i.e., one with the
|
||||
// "subset=pid" mount option applied, available from Linux 5.8). Unless you
|
||||
// plan to do many [Handle.OpenRoot] operations, users should prefer to use
|
||||
// this over [OpenUnsafeProcRoot] which is far more dangerous to keep open.
|
||||
//
|
||||
// If a safe handle cannot be opened, OpenProcRoot will fall back to opening a
|
||||
// regular "/proc" handle.
|
||||
//
|
||||
// Note that using [Handle.OpenRoot] will still work with handles returned by
|
||||
// this function. If a subpath cannot be operated on with a safe "/proc"
|
||||
// handle, then [OpenUnsafeProcRoot] will be called internally and a temporary
|
||||
// unsafe handle will be used.
|
||||
func OpenProcRoot() (*Handle, error) {
|
||||
proc, err := procfs.OpenProcRoot()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Handle{inner: proc}, nil
|
||||
}
|
||||
|
||||
// OpenUnsafeProcRoot opens a handle to "/proc" without any overmounts or
|
||||
// masked paths. You must be extremely careful to make sure this handle is
|
||||
// never leaked to a container and that you program cannot be tricked into
|
||||
// writing to arbitrary paths within it.
|
||||
//
|
||||
// This is not necessary if you just wish to use [Handle.OpenRoot], as handles
|
||||
// returned by [OpenProcRoot] will fall back to using a *temporary* unsafe
|
||||
// handle in that case. You should only really use this if you need to do many
|
||||
// operations with [Handle.OpenRoot] and the performance overhead of making
|
||||
// many procfs handles is an issue. If you do use OpenUnsafeProcRoot, you
|
||||
// should make sure to close the handle as soon as possible to avoid
|
||||
// known-fd-number attacks.
|
||||
func OpenUnsafeProcRoot() (*Handle, error) {
|
||||
proc, err := procfs.OpenUnsafeProcRoot()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Handle{inner: proc}, nil
|
||||
}
|
||||
|
||||
// OpenThreadSelf returns a handle to "/proc/thread-self/<subpath>" (or an
|
||||
// equivalent handle on older kernels where "/proc/thread-self" doesn't exist).
|
||||
// Once finished with the handle, you must call the returned closer function
|
||||
// ([runtime.UnlockOSThread]). You must not pass the returned *os.File to other
|
||||
// Go threads or use the handle after calling the closer.
|
||||
//
|
||||
// [runtime.UnlockOSThread]: https://pkg.go.dev/runtime#UnlockOSThread
|
||||
func (proc *Handle) OpenThreadSelf(subpath string) (*os.File, ProcThreadSelfCloser, error) {
|
||||
return proc.inner.OpenThreadSelf(subpath)
|
||||
}
|
||||
|
||||
// OpenSelf returns a handle to /proc/self/<subpath>.
|
||||
//
|
||||
// Note that in Go programs with non-homogenous threads, this may result in
|
||||
// spurious errors. If you are monkeying around with APIs that are
|
||||
// thread-specific, you probably want to use [Handle.OpenThreadSelf] instead
|
||||
// which will guarantee that the handle refers to the same thread as the caller
|
||||
// is executing on.
|
||||
func (proc *Handle) OpenSelf(subpath string) (*os.File, error) {
|
||||
return proc.inner.OpenSelf(subpath)
|
||||
}
|
||||
|
||||
// OpenRoot returns a handle to /proc/<subpath>.
|
||||
//
|
||||
// You should only use this when you need to operate on global procfs files
|
||||
// (such as sysctls in /proc/sys). Unlike [Handle.OpenThreadSelf],
|
||||
// [Handle.OpenSelf], and [Handle.OpenPid], the procfs handle used internally
|
||||
// for this operation will never use "subset=pid", which makes it a more juicy
|
||||
// target for [CVE-2024-21626]-style attacks (and doing something like opening
|
||||
// a directory with OpenRoot effectively leaks [OpenUnsafeProcRoot] as long as
|
||||
// the file descriptor is open).
|
||||
//
|
||||
// [CVE-2024-21626]: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv
|
||||
func (proc *Handle) OpenRoot(subpath string) (*os.File, error) {
|
||||
return proc.inner.OpenRoot(subpath)
|
||||
}
|
||||
|
||||
// OpenPid returns a handle to /proc/$pid/<subpath> (pid can be a pid or tid).
|
||||
// This is mainly intended for usage when operating on other processes.
|
||||
//
|
||||
// You should not use this for the current thread, as special handling is
|
||||
// needed for /proc/thread-self (or /proc/self/task/<tid>) when dealing with
|
||||
// goroutine scheduling -- use [Handle.OpenThreadSelf] instead.
|
||||
//
|
||||
// To refer to the current thread-group, you should use prefer
|
||||
// [Handle.OpenSelf] to passing os.Getpid as the pid argument.
|
||||
func (proc *Handle) OpenPid(pid int, subpath string) (*os.File, error) {
|
||||
return proc.inner.OpenPid(pid, subpath)
|
||||
}
|
||||
|
||||
// ProcSelfFdReadlink gets the real path of the given file by looking at
|
||||
// /proc/self/fd/<fd> with [readlink]. It is effectively just shorthand for
|
||||
// something along the lines of:
|
||||
//
|
||||
// proc, err := procfs.OpenProcRoot()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// link, err := proc.OpenThreadSelf(fmt.Sprintf("fd/%d", f.Fd()))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer link.Close()
|
||||
// var buf [4096]byte
|
||||
// n, err := unix.Readlinkat(int(link.Fd()), "", buf[:])
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// pathname := buf[:n]
|
||||
//
|
||||
// [readlink]: https://pkg.go.dev/golang.org/x/sys/unix#Readlinkat
|
||||
func ProcSelfFdReadlink(f *os.File) (string, error) {
|
||||
return procfs.ProcSelfFdReadlink(f)
|
||||
}
|
||||
+4
@@ -5,6 +5,10 @@ Billy implements an interface based on the `os` standard library, allowing to de
|
||||
|
||||
Billy was born as part of [go-git/go-git](https://github.com/go-git/go-git) project.
|
||||
|
||||
## Version support
|
||||
|
||||
go-billy v5 is in maintenance mode. Users should upgrade to [go-billy v6](https://pkg.go.dev/github.com/go-git/go-billy/v6) where possible.
|
||||
|
||||
## Installation
|
||||
|
||||
```go
|
||||
|
||||
+198
-10
@@ -3,19 +3,25 @@ package chroot
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-billy/v5/helper/polyfill"
|
||||
)
|
||||
|
||||
// ChrootHelper is a helper to implement billy.Chroot.
|
||||
// It is not a security boundary, callers that need containment should use a
|
||||
// filesystem implementation that enforces paths at the OS boundary instead.
|
||||
type ChrootHelper struct {
|
||||
underlying billy.Filesystem
|
||||
base string
|
||||
}
|
||||
|
||||
const maxFollowedSymlinks = 8 // Aligns with POSIX_SYMLOOP_MAX
|
||||
|
||||
// New creates a new filesystem wrapping up the given 'fs'.
|
||||
// The created filesystem has its base in the given ChrootHelperectory of the
|
||||
// underlying filesystem.
|
||||
@@ -34,15 +40,184 @@ func (fs *ChrootHelper) underlyingPath(filename string) (string, error) {
|
||||
return fs.Join(fs.Root(), filename), nil
|
||||
}
|
||||
|
||||
func isCrossBoundaries(path string) bool {
|
||||
path = filepath.ToSlash(path)
|
||||
path = filepath.Clean(path)
|
||||
func (fs *ChrootHelper) followedPath(filename string, followFinal bool, op string) (string, error) {
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.HasPrefix(path, ".."+string(filepath.Separator))
|
||||
sl, ok := fs.underlying.(billy.Symlink)
|
||||
if !ok {
|
||||
return fullpath, nil
|
||||
}
|
||||
|
||||
rel, err := fs.relativeToRoot(fullpath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fullpath, err = fs.resolveFollowedPath(rel, followFinal, op, sl)
|
||||
if errors.Is(err, billy.ErrNotSupported) {
|
||||
return fs.underlyingPath(filename)
|
||||
}
|
||||
|
||||
return fullpath, err
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) resolveFollowedPath(rel string, followFinal bool, op string, sl billy.Symlink) (string, error) {
|
||||
if rel == "" {
|
||||
return fs.resolveFollowedRoot(followFinal, op, sl)
|
||||
}
|
||||
|
||||
parts := splitRelativePath(rel)
|
||||
resolved := ""
|
||||
followed := 0
|
||||
|
||||
for len(parts) > 0 {
|
||||
part := parts[0]
|
||||
parts = parts[1:]
|
||||
|
||||
currentRel := joinRelativePath(resolved, part)
|
||||
currentPath := fs.Join(fs.Root(), currentRel)
|
||||
if len(parts) == 0 && !followFinal {
|
||||
return currentPath, nil
|
||||
}
|
||||
|
||||
fi, err := sl.Lstat(currentPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fs.Join(fs.Root(), joinRelativePath(append([]string{currentRel}, parts...)...)), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if fi.Mode()&os.ModeSymlink == 0 {
|
||||
resolved = currentRel
|
||||
continue
|
||||
}
|
||||
|
||||
followed++
|
||||
if followed > maxFollowedSymlinks {
|
||||
return "", symlinkLoopError(op, currentPath)
|
||||
}
|
||||
|
||||
target, err := sl.Readlink(currentPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
targetRel, err := fs.linkTargetRel(currentPath, target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if targetRel == currentRel {
|
||||
return "", symlinkLoopError(op, currentPath)
|
||||
}
|
||||
|
||||
parts = append(splitRelativePath(targetRel), parts...)
|
||||
resolved = ""
|
||||
}
|
||||
|
||||
return fs.Join(fs.Root(), resolved), nil
|
||||
}
|
||||
|
||||
func symlinkLoopError(op, path string) error {
|
||||
return &os.PathError{Op: op, Path: path, Err: syscall.ELOOP}
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) resolveFollowedRoot(followFinal bool, op string, sl billy.Symlink) (string, error) {
|
||||
root := fs.Join(fs.Root(), "")
|
||||
if !followFinal {
|
||||
return root, nil
|
||||
}
|
||||
|
||||
fi, err := sl.Lstat(root)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return root, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if fi.Mode()&os.ModeSymlink == 0 {
|
||||
return root, nil
|
||||
}
|
||||
|
||||
target, err := sl.Readlink(root)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
targetRel, err := fs.linkTargetRel(root, target)
|
||||
if err != nil {
|
||||
return root, err
|
||||
}
|
||||
if targetRel == "" {
|
||||
return "", symlinkLoopError(op, root)
|
||||
}
|
||||
|
||||
return fs.resolveFollowedPath(targetRel, followFinal, op, sl)
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) relativeToRoot(filename string) (string, error) {
|
||||
rel, err := filepath.Rel(filepath.Clean(fs.Root()), filepath.Clean(filename))
|
||||
if err != nil || isCrossBoundaries(rel) {
|
||||
return "", billy.ErrCrossedBoundary
|
||||
}
|
||||
|
||||
if rel == "." {
|
||||
return "", nil
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) linkTargetRel(linkPath, target string) (string, error) {
|
||||
target = filepath.FromSlash(target)
|
||||
if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) {
|
||||
return fs.relativeToRoot(target)
|
||||
}
|
||||
|
||||
return fs.relativeToRoot(fs.Join(filepath.Dir(linkPath), target))
|
||||
}
|
||||
|
||||
func splitRelativePath(filename string) []string {
|
||||
filename = filepath.Clean(filename)
|
||||
if filename == "" || filename == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(filepath.ToSlash(filename), "/")
|
||||
}
|
||||
|
||||
func joinRelativePath(elem ...string) string {
|
||||
parts := make([]string, 0, len(elem))
|
||||
for _, part := range elem {
|
||||
if part == "" || part == "." {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, part)
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(parts...)
|
||||
}
|
||||
|
||||
func isCreateExclusive(flag int) bool {
|
||||
return flag&os.O_CREATE != 0 && flag&os.O_EXCL != 0
|
||||
}
|
||||
|
||||
func isCrossBoundaries(name string) bool {
|
||||
name = filepath.ToSlash(name)
|
||||
name = strings.TrimLeft(name, "/")
|
||||
name = path.Clean(name)
|
||||
|
||||
return name == ".." || strings.HasPrefix(name, "../")
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
fullpath, err := fs.followedPath(filename, true, "create")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -56,7 +231,7 @@ func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Open(filename string) (billy.File, error) {
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
fullpath, err := fs.followedPath(filename, true, "open")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -70,7 +245,7 @@ func (fs *ChrootHelper) Open(filename string) (billy.File, error) {
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (billy.File, error) {
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
fullpath, err := fs.followedPath(filename, !isCreateExclusive(flag), "open")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -84,12 +259,16 @@ func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (b
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Stat(filename string) (os.FileInfo, error) {
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
fullpath, err := fs.followedPath(filename, true, "stat")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fs.underlying.Stat(fullpath)
|
||||
fi, err := fs.underlying.Stat(fullpath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileInfo{FileInfo: fi, name: filepath.Base(filename)}, nil
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Rename(from, to string) error {
|
||||
@@ -135,7 +314,7 @@ func (fs *ChrootHelper) TempFile(dir, prefix string) (billy.File, error) {
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
fullpath, err := fs.underlyingPath(path)
|
||||
fullpath, err := fs.followedPath(path, true, "readdir")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -241,6 +420,11 @@ type file struct {
|
||||
name string
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
os.FileInfo
|
||||
name string
|
||||
}
|
||||
|
||||
func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File {
|
||||
filename = fs.Join(fs.Root(), filename)
|
||||
filename, _ = filepath.Rel(fs.Root(), filename)
|
||||
@@ -254,3 +438,7 @@ func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File {
|
||||
func (f *file) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (fi fileInfo) Name() string {
|
||||
return fi.name
|
||||
}
|
||||
|
||||
+5
@@ -24,6 +24,9 @@ var Default = &ChrootOS{}
|
||||
// New returns a new OS filesystem.
|
||||
// By default paths are deduplicated, but still enforced
|
||||
// under baseDir. For more info refer to WithDeduplicatePath.
|
||||
//
|
||||
// New returns ChrootOS by default for v5 compatibility. Users should prefer
|
||||
// New with WithBoundOS.
|
||||
func New(baseDir string, opts ...Option) billy.Filesystem {
|
||||
o := &options{
|
||||
deduplicatePath: true,
|
||||
@@ -47,6 +50,8 @@ func WithBoundOS() Option {
|
||||
}
|
||||
|
||||
// WithChrootOS returns the option of using a Chroot filesystem OS.
|
||||
//
|
||||
// Deprecated: use WithBoundOS instead.
|
||||
func WithChrootOS() Option {
|
||||
return func(o *options) {
|
||||
o.Type = ChrootOSFS
|
||||
|
||||
+85
-15
@@ -20,6 +20,7 @@
|
||||
package osfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -29,6 +30,31 @@ import (
|
||||
"github.com/go-git/go-billy/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrBaseDirCannotBeRemoved is returned when removing the BoundOS base dir.
|
||||
ErrBaseDirCannotBeRemoved = errors.New("base dir cannot be removed")
|
||||
|
||||
// ErrBaseDirCannotBeRenamed is returned when renaming the BoundOS base dir.
|
||||
ErrBaseDirCannotBeRenamed = errors.New("base dir cannot be renamed")
|
||||
|
||||
dotPrefixes = dotPathPrefixes()
|
||||
dotSeparators = dotPathSeparators()
|
||||
)
|
||||
|
||||
func dotPathPrefixes() []string {
|
||||
if filepath.Separator == '\\' {
|
||||
return []string{"./", ".\\"}
|
||||
}
|
||||
return []string{"./"}
|
||||
}
|
||||
|
||||
func dotPathSeparators() string {
|
||||
if filepath.Separator == '\\' {
|
||||
return `/\`
|
||||
}
|
||||
return `/`
|
||||
}
|
||||
|
||||
// BoundOS is a fs implementation based on the OS filesystem which is bound to
|
||||
// a base dir.
|
||||
// Prefer this fs implementation over ChrootOS.
|
||||
@@ -54,6 +80,7 @@ func (fs *BoundOS) Create(filename string) (billy.File, error) {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
|
||||
filename = fs.expandDot(filename)
|
||||
fn, err := fs.abs(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -62,6 +89,7 @@ func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy.
|
||||
}
|
||||
|
||||
func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
path = fs.expandDot(path)
|
||||
dir, err := fs.abs(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -71,6 +99,12 @@ func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Rename(from, to string) error {
|
||||
if fs.isBaseDir(from) {
|
||||
return ErrBaseDirCannotBeRenamed
|
||||
}
|
||||
from = fs.expandDot(from)
|
||||
to = fs.expandDot(to)
|
||||
|
||||
f, err := fs.abs(from)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -89,6 +123,7 @@ func (fs *BoundOS) Rename(from, to string) error {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) MkdirAll(path string, perm os.FileMode) error {
|
||||
path = fs.expandDot(path)
|
||||
dir, err := fs.abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -101,6 +136,7 @@ func (fs *BoundOS) Open(filename string) (billy.File, error) {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) {
|
||||
filename = fs.expandDot(filename)
|
||||
filename, err := fs.abs(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -109,6 +145,11 @@ func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Remove(filename string) error {
|
||||
if fs.isBaseDir(filename) {
|
||||
return ErrBaseDirCannotBeRemoved
|
||||
}
|
||||
filename = fs.expandDot(filename)
|
||||
|
||||
fn, err := fs.abs(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -122,6 +163,7 @@ func (fs *BoundOS) Remove(filename string) error {
|
||||
func (fs *BoundOS) TempFile(dir, prefix string) (billy.File, error) {
|
||||
if dir != "" {
|
||||
var err error
|
||||
dir = fs.expandDot(dir)
|
||||
dir, err = fs.abs(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -144,6 +186,11 @@ func (fs *BoundOS) Join(elem ...string) string {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) RemoveAll(path string) error {
|
||||
if fs.isBaseDir(path) {
|
||||
return ErrBaseDirCannotBeRemoved
|
||||
}
|
||||
path = fs.expandDot(path)
|
||||
|
||||
dir, err := fs.abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -152,6 +199,7 @@ func (fs *BoundOS) RemoveAll(path string) error {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Symlink(target, link string) error {
|
||||
link = fs.expandDot(link)
|
||||
ln, err := fs.abs(link)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -164,6 +212,7 @@ func (fs *BoundOS) Symlink(target, link string) error {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) {
|
||||
filename = fs.expandDot(filename)
|
||||
filename = filepath.Clean(filename)
|
||||
if !filepath.IsAbs(filename) {
|
||||
filename = filepath.Join(fs.baseDir, filename)
|
||||
@@ -175,6 +224,7 @@ func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Readlink(link string) (string, error) {
|
||||
link = fs.expandDot(link)
|
||||
if !filepath.IsAbs(link) {
|
||||
link = filepath.Clean(filepath.Join(fs.baseDir, link))
|
||||
}
|
||||
@@ -185,6 +235,7 @@ func (fs *BoundOS) Readlink(link string) (string, error) {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Chmod(path string, mode os.FileMode) error {
|
||||
path = fs.expandDot(path)
|
||||
abspath, err := fs.abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -199,7 +250,7 @@ func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return New(joined), nil
|
||||
return New(joined, WithBoundOS()), nil
|
||||
}
|
||||
|
||||
// Root returns the current base dir of the billy.Filesystem.
|
||||
@@ -220,6 +271,37 @@ func (fs *BoundOS) createDir(fullpath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *BoundOS) expandDot(path string) string {
|
||||
if path == "." {
|
||||
return fs.baseDir
|
||||
}
|
||||
for _, prefix := range dotPrefixes {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
path = strings.TrimLeft(strings.TrimPrefix(path, prefix), dotSeparators)
|
||||
if path == "" {
|
||||
return fs.baseDir
|
||||
}
|
||||
return path
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func (fs *BoundOS) isBaseDir(path string) bool {
|
||||
if path == "" || filepath.Clean(path) == "." {
|
||||
return true
|
||||
}
|
||||
path = fs.expandDot(path)
|
||||
if filepath.Clean(path) == filepath.Clean(fs.baseDir) {
|
||||
return true
|
||||
}
|
||||
abspath, err := fs.abs(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return filepath.Clean(abspath) == filepath.Clean(fs.baseDir)
|
||||
}
|
||||
|
||||
// abs transforms filename to an absolute path, taking into account the base dir.
|
||||
// Relative paths won't be allowed to ascend the base dir, so `../file` will become
|
||||
// `/working-dir/file`.
|
||||
@@ -233,7 +315,7 @@ func (fs *BoundOS) abs(filename string) (string, error) {
|
||||
|
||||
path, err := securejoin.SecureJoin(fs.baseDir, filename)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
return "", err
|
||||
}
|
||||
|
||||
if fs.deduplicatePath {
|
||||
@@ -246,24 +328,12 @@ func (fs *BoundOS) abs(filename string) (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// insideBaseDir checks whether filename is located within
|
||||
// the fs.baseDir.
|
||||
func (fs *BoundOS) insideBaseDir(filename string) (bool, error) {
|
||||
if filename == fs.baseDir {
|
||||
return true, nil
|
||||
}
|
||||
if !strings.HasPrefix(filename, fs.baseDir+string(filepath.Separator)) {
|
||||
return false, fmt.Errorf("path outside base dir")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// insideBaseDirEval checks whether filename is contained within
|
||||
// a dir that is within the fs.baseDir, by first evaluating any symlinks
|
||||
// that either filename or fs.baseDir may contain.
|
||||
func (fs *BoundOS) insideBaseDirEval(filename string) (bool, error) {
|
||||
// "/" contains all others.
|
||||
if fs.baseDir == "/" {
|
||||
if fs.baseDir == "/" || fs.baseDir == filename {
|
||||
return true, nil
|
||||
}
|
||||
dir, err := filepath.EvalSymlinks(filepath.Dir(filename))
|
||||
|
||||
+10
@@ -14,6 +14,8 @@ import (
|
||||
// ChrootOS is a legacy filesystem based on a "soft chroot" of the os filesystem.
|
||||
// Although this is still the default os filesystem, consider using BoundOS instead.
|
||||
//
|
||||
// Deprecated: use New with WithBoundOS instead.
|
||||
//
|
||||
// Behaviours of note:
|
||||
// 1. A "soft chroot" translates the base dir to "/" for the purposes of the
|
||||
// fs abstraction.
|
||||
@@ -24,6 +26,14 @@ import (
|
||||
type ChrootOS struct{}
|
||||
|
||||
func newChrootOS(baseDir string) billy.Filesystem {
|
||||
if baseDir != "" {
|
||||
resolved, err := filepath.EvalSymlinks(baseDir)
|
||||
if err != nil {
|
||||
return chroot.New(&ChrootOS{}, baseDir)
|
||||
}
|
||||
baseDir = resolved
|
||||
}
|
||||
|
||||
return chroot.New(&ChrootOS{}, baseDir)
|
||||
}
|
||||
|
||||
|
||||
+19
-24
@@ -16,8 +16,6 @@ import (
|
||||
// can but returns the first error it encounters. If the path does not exist,
|
||||
// RemoveAll returns nil (no error).
|
||||
func RemoveAll(fs billy.Basic, path string) error {
|
||||
fs, path = getUnderlyingAndPath(fs, path)
|
||||
|
||||
if r, ok := fs.(removerAll); ok {
|
||||
return r.RemoveAll(path)
|
||||
}
|
||||
@@ -39,7 +37,7 @@ func removeAll(fs billy.Basic, path string) error {
|
||||
}
|
||||
|
||||
// Otherwise, is this a directory we need to recurse into?
|
||||
dir, serr := fs.Stat(path)
|
||||
dir, serr := lstat(fs, path)
|
||||
if serr != nil {
|
||||
if errors.Is(serr, os.ErrNotExist) {
|
||||
return nil
|
||||
@@ -48,8 +46,8 @@ func removeAll(fs billy.Basic, path string) error {
|
||||
return serr
|
||||
}
|
||||
|
||||
if !dir.IsDir() {
|
||||
// Not a directory; return the error from Remove.
|
||||
if dir.Mode()&os.ModeSymlink != 0 || !dir.IsDir() {
|
||||
// Not a directory we should recurse into; return the error from Remove.
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -62,7 +60,7 @@ func removeAll(fs billy.Basic, path string) error {
|
||||
fis, err := dirfs.ReadDir(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Race. It was deleted between the Lstat and Open.
|
||||
// Race. It was deleted between the Lstat and ReadDir.
|
||||
// Return nil per RemoveAll's docs.
|
||||
return nil
|
||||
}
|
||||
@@ -91,7 +89,18 @@ func removeAll(fs billy.Basic, path string) error {
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func lstat(filesystem billy.Basic, path string) (os.FileInfo, error) {
|
||||
if sl, ok := filesystem.(billy.Symlink); ok {
|
||||
// Avoid following a symlink substituted after the initial Remove fails.
|
||||
fi, err := sl.Lstat(path)
|
||||
if err == nil || !errors.Is(err, billy.ErrNotSupported) {
|
||||
return fi, err
|
||||
}
|
||||
}
|
||||
|
||||
return filesystem.Stat(path)
|
||||
}
|
||||
|
||||
// WriteFile writes data to a file named by filename in the given filesystem.
|
||||
@@ -123,8 +132,10 @@ func WriteFile(fs billy.Basic, filename string, data []byte, perm os.FileMode) (
|
||||
// We generate random temporary file names so that there's a good
|
||||
// chance the file doesn't exist yet - keeps the number of tries in
|
||||
// TempFile to a minimum.
|
||||
var rand uint32
|
||||
var randmu sync.Mutex
|
||||
var (
|
||||
rand uint32
|
||||
randmu sync.Mutex
|
||||
)
|
||||
|
||||
func reseed() uint32 {
|
||||
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
|
||||
@@ -220,22 +231,6 @@ func getTempDir(fs billy.Basic) string {
|
||||
return ".tmp"
|
||||
}
|
||||
|
||||
type underlying interface {
|
||||
Underlying() billy.Basic
|
||||
}
|
||||
|
||||
func getUnderlyingAndPath(fs billy.Basic, path string) (billy.Basic, string) {
|
||||
u, ok := fs.(underlying)
|
||||
if !ok {
|
||||
return fs, path
|
||||
}
|
||||
if ch, ok := fs.(billy.Chroot); ok {
|
||||
path = fs.Join(ch.Root(), path)
|
||||
}
|
||||
|
||||
return u.Underlying(), path
|
||||
}
|
||||
|
||||
// ReadFile reads the named file and returns the contents from the given filesystem.
|
||||
// A successful call returns err == nil, not err == EOF.
|
||||
// Because ReadFile reads the whole file, it does not treat an EOF from Read
|
||||
|
||||
+113
-94
@@ -5,7 +5,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
beginpgp string = "-----BEGIN PGP SIGNATURE-----"
|
||||
endpgp string = "-----END PGP SIGNATURE-----"
|
||||
headerpgp string = "gpgsig"
|
||||
headerpgp256 string = "gpgsig-sha256"
|
||||
headerencoding string = "encoding"
|
||||
|
||||
// https://github.com/git/git/blob/bcb6cae2966cc407ca1afc77413b3ef11103c175/Documentation/gitformat-signature.txt#L153
|
||||
@@ -41,6 +42,11 @@ type MessageEncoding string
|
||||
// in time, such as a timestamp, the author of the changes since the last
|
||||
// commit, a pointer to the previous commit(s), etc.
|
||||
// http://shafiulazam.com/gitbook/1_the_git_object_model.html
|
||||
//
|
||||
// When a Commit is populated by Decode it retains a reference to the source
|
||||
// plumbing.EncodedObject so that EncodeWithoutSignature can reproduce the
|
||||
// exact bytes the signature was computed over. Refer to EncodeWithoutSignature
|
||||
// for more information.
|
||||
type Commit struct {
|
||||
// Hash of the commit object.
|
||||
Hash plumbing.Hash
|
||||
@@ -66,6 +72,9 @@ type Commit struct {
|
||||
ExtraHeaders []ExtraHeader
|
||||
|
||||
s storer.EncodedObjectStorer
|
||||
// src holds the encoded object this Commit was decoded from, used by
|
||||
// EncodeWithoutSignature to recover the canonical signed bytes.
|
||||
src plumbing.EncodedObject
|
||||
}
|
||||
|
||||
// ExtraHeader holds any non-standard header
|
||||
@@ -98,8 +107,8 @@ func (h ExtraHeader) Format(f fmt.State, verb rune) {
|
||||
func parseExtraHeader(line []byte) (ExtraHeader, bool) {
|
||||
split := bytes.SplitN(line, []byte{' '}, 2)
|
||||
|
||||
out := ExtraHeader {
|
||||
Key: string(bytes.TrimRight(split[0], "\n")),
|
||||
out := ExtraHeader{
|
||||
Key: string(bytes.TrimRight(split[0], "\n")),
|
||||
Value: "",
|
||||
}
|
||||
|
||||
@@ -181,6 +190,11 @@ func (c *Commit) NumParents() int {
|
||||
|
||||
var ErrParentNotFound = errors.New("commit parent not found")
|
||||
|
||||
// ErrMalformedCommit is returned when a commit object cannot be decoded
|
||||
// because its standard headers (tree, parent, author, committer) are missing,
|
||||
// duplicated, or out of order.
|
||||
var ErrMalformedCommit = errors.New("malformed commit")
|
||||
|
||||
// Parent returns the ith parent of a commit.
|
||||
func (c *Commit) Parent(i int) (*Commit, error) {
|
||||
if len(c.ParentHashes) == 0 || i > len(c.ParentHashes)-1 {
|
||||
@@ -227,14 +241,23 @@ func (c *Commit) Type() plumbing.ObjectType {
|
||||
return plumbing.CommitObject
|
||||
}
|
||||
|
||||
func (c *Commit) reset() {
|
||||
storer := c.s
|
||||
*c = Commit{
|
||||
Encoding: defaultUtf8CommitMessageEncoding,
|
||||
s: storer,
|
||||
}
|
||||
}
|
||||
|
||||
// Decode transforms a plumbing.EncodedObject into a Commit struct.
|
||||
func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
|
||||
if o.Type() != plumbing.CommitObject {
|
||||
return ErrUnsupportedObject
|
||||
}
|
||||
|
||||
c.reset()
|
||||
c.Hash = o.Hash()
|
||||
c.Encoding = defaultUtf8CommitMessageEncoding
|
||||
c.src = o
|
||||
|
||||
reader, err := o.Reader()
|
||||
if err != nil {
|
||||
@@ -245,97 +268,17 @@ func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
|
||||
r := sync.GetBufioReader(reader)
|
||||
defer sync.PutBufioReader(r)
|
||||
|
||||
var message bool
|
||||
var mergetag bool
|
||||
var pgpsig bool
|
||||
var msgbuf bytes.Buffer
|
||||
var extraheader *ExtraHeader = nil
|
||||
for {
|
||||
line, err := r.ReadBytes('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
s := &commitScanner{r: r, c: c}
|
||||
for state := scanTree; state != nil; {
|
||||
state, err = state(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mergetag {
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
line = bytes.TrimLeft(line, " ")
|
||||
c.MergeTag += string(line)
|
||||
continue
|
||||
} else {
|
||||
mergetag = false
|
||||
}
|
||||
}
|
||||
|
||||
if pgpsig {
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
line = bytes.TrimLeft(line, " ")
|
||||
c.PGPSignature += string(line)
|
||||
continue
|
||||
} else {
|
||||
pgpsig = false
|
||||
}
|
||||
}
|
||||
|
||||
if extraheader != nil {
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
extraheader.Value += string(line[1:])
|
||||
continue
|
||||
} else {
|
||||
extraheader.Value = strings.TrimRight(extraheader.Value, "\n")
|
||||
c.ExtraHeaders = append(c.ExtraHeaders, *extraheader)
|
||||
extraheader = nil
|
||||
}
|
||||
}
|
||||
|
||||
if !message {
|
||||
original_line := line
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
message = true
|
||||
continue
|
||||
}
|
||||
|
||||
split := bytes.SplitN(line, []byte{' '}, 2)
|
||||
|
||||
var data []byte
|
||||
if len(split) == 2 {
|
||||
data = split[1]
|
||||
}
|
||||
|
||||
switch string(split[0]) {
|
||||
case "tree":
|
||||
c.TreeHash = plumbing.NewHash(string(data))
|
||||
case "parent":
|
||||
c.ParentHashes = append(c.ParentHashes, plumbing.NewHash(string(data)))
|
||||
case "author":
|
||||
c.Author.Decode(data)
|
||||
case "committer":
|
||||
c.Committer.Decode(data)
|
||||
case headermergetag:
|
||||
c.MergeTag += string(data) + "\n"
|
||||
mergetag = true
|
||||
case headerencoding:
|
||||
c.Encoding = MessageEncoding(data)
|
||||
case headerpgp:
|
||||
c.PGPSignature += string(data) + "\n"
|
||||
pgpsig = true
|
||||
default:
|
||||
h, maybecontinued := parseExtraHeader(original_line)
|
||||
if maybecontinued {
|
||||
extraheader = &h
|
||||
} else {
|
||||
c.ExtraHeaders = append(c.ExtraHeaders, h)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
msgbuf.Write(line)
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Message = msgbuf.String()
|
||||
if !s.sawTree {
|
||||
return fmt.Errorf("%w: missing tree header", ErrMalformedCommit)
|
||||
}
|
||||
c.Message = s.msgbuf.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -344,11 +287,73 @@ func (c *Commit) Encode(o plumbing.EncodedObject) error {
|
||||
return c.encode(o, true)
|
||||
}
|
||||
|
||||
// EncodeWithoutSignature export a Commit into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature).
|
||||
// EncodeWithoutSignature exports a Commit into a plumbing.EncodedObject
|
||||
// without any signature headers, producing the payload that PGP/GPG
|
||||
// signatures are computed over.
|
||||
//
|
||||
// Behaviour depends on how the Commit was created:
|
||||
//
|
||||
// - For Commits populated by Decode whose exported fields still match the
|
||||
// source object, the payload is streamed from the raw source bytes with
|
||||
// gpgsig and gpgsig-sha256 headers (and their continuation lines)
|
||||
// stripped verbatim. This preserves the exact bytes the signature was
|
||||
// computed over, regardless of any normalization performed by Decode.
|
||||
//
|
||||
// - For Commits constructed in memory, or for decoded Commits whose
|
||||
// exported fields have been mutated, the payload is derived from the
|
||||
// current struct fields. Mutation is detected by re-decoding the source
|
||||
// object and comparing exported fields; if any differ, the in-memory
|
||||
// representation prevails.
|
||||
func (c *Commit) EncodeWithoutSignature(o plumbing.EncodedObject) error {
|
||||
if c.matchesSource() {
|
||||
return stripObjectSignatures(o, c.src, plumbing.CommitObject)
|
||||
}
|
||||
return c.encode(o, false)
|
||||
}
|
||||
|
||||
// matchesSource reports whether c.src is set and re-decoding it produces a
|
||||
// Commit whose payload-affecting exported fields are identical to those of
|
||||
// c. It is the auto-detection used by EncodeWithoutSignature to decide
|
||||
// between the raw bytes and the struct-encoded payload.
|
||||
//
|
||||
// PGPSignature is intentionally excluded from the comparison: neither path
|
||||
// emits it, so mutating it must not trigger a switch to struct-encode (which
|
||||
// would change the byte layout the caller is trying to verify against).
|
||||
func (c *Commit) matchesSource() bool {
|
||||
if c.src == nil {
|
||||
return false
|
||||
}
|
||||
fresh := &Commit{}
|
||||
if err := fresh.Decode(c.src); err != nil {
|
||||
return false
|
||||
}
|
||||
return c.Hash == fresh.Hash &&
|
||||
signatureEqual(c.Author, fresh.Author) &&
|
||||
signatureEqual(c.Committer, fresh.Committer) &&
|
||||
c.MergeTag == fresh.MergeTag &&
|
||||
c.Message == fresh.Message &&
|
||||
c.TreeHash == fresh.TreeHash &&
|
||||
c.Encoding == fresh.Encoding &&
|
||||
slices.Equal(c.ParentHashes, fresh.ParentHashes) &&
|
||||
slices.Equal(c.ExtraHeaders, fresh.ExtraHeaders)
|
||||
}
|
||||
|
||||
func signatureEqual(a, b Signature) bool {
|
||||
return a.Name == b.Name &&
|
||||
a.Email == b.Email &&
|
||||
a.When.Unix() == b.When.Unix() &&
|
||||
a.When.Format("-0700") == b.When.Format("-0700")
|
||||
}
|
||||
|
||||
func isStandardHeader(key string) bool {
|
||||
switch key {
|
||||
case "tree", "parent", "author", "committer",
|
||||
headerencoding, headermergetag, headerpgp, headerpgp256:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
o.SetType(plumbing.CommitObject)
|
||||
w, err := o.Writer()
|
||||
@@ -407,7 +412,9 @@ func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
}
|
||||
|
||||
for _, header := range c.ExtraHeaders {
|
||||
|
||||
if isStandardHeader(header.Key) {
|
||||
continue
|
||||
}
|
||||
if _, err = fmt.Fprintf(w, "\n%s", header); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -478,9 +485,21 @@ func (c *Commit) String() string {
|
||||
)
|
||||
}
|
||||
|
||||
// ErrMultipleSignatures is returned by Verify when the commit carries more
|
||||
// than one armored signature block. Mirrors upstream's parse_gpg_output
|
||||
// rejection of GOODSIG/BADSIG status lines after the first
|
||||
// (gpg-interface.c:257-269): multi-signature commits are intentionally
|
||||
// unsupported because their provenance cannot be reduced to a single
|
||||
// authoritative signer.
|
||||
var ErrMultipleSignatures = errors.New("commit has multiple signatures")
|
||||
|
||||
// Verify performs PGP verification of the commit with a provided armored
|
||||
// keyring and returns openpgp.Entity associated with verifying key on success.
|
||||
func (c *Commit) Verify(armoredKeyRing string) (*openpgp.Entity, error) {
|
||||
if countSignatureBlocks([]byte(c.PGPSignature)) > 1 {
|
||||
return nil, ErrMultipleSignatures
|
||||
}
|
||||
|
||||
keyRingReader := strings.NewReader(armoredKeyRing)
|
||||
keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader)
|
||||
if err != nil {
|
||||
|
||||
+377
@@ -0,0 +1,377 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// commitScanner holds the working state of the commit decoder driven by the
|
||||
// stateFn loop in (*Commit).Decode. Each commitState reads one or more lines
|
||||
// from r, updates the in-progress *Commit and the scanner's bookkeeping, and
|
||||
// returns the state that should run next (or nil to stop).
|
||||
type commitScanner struct {
|
||||
r *bufio.Reader
|
||||
c *Commit
|
||||
msgbuf bytes.Buffer
|
||||
|
||||
// pending holds a line that was read but the current state decided to
|
||||
// hand back to the next state, paired with the io.EOF flag that was
|
||||
// returned when the line was originally read.
|
||||
pending []byte
|
||||
pendingErr error
|
||||
|
||||
// First-occurrence tracking: once the corresponding field has been
|
||||
// decoded, subsequent occurrences are silently dropped (matches
|
||||
// upstream's find_commit_header / first-wins semantics).
|
||||
//
|
||||
// gpgsig is not tracked here: upstream's parse_buffer_signed_by_header
|
||||
// (commit.c:1186) accumulates every occurrence into one signature buffer,
|
||||
// so we do the same on the scanner side to keep verification payloads
|
||||
// byte-aligned. gpgsig-sha256 is recognized and skipped without exposing a
|
||||
// new field in v5.
|
||||
sawTree, sawAuthor, sawCommitter bool
|
||||
sawEncoding, sawMergetag bool
|
||||
|
||||
// extra is the multi-line ExtraHeader currently being assembled.
|
||||
extra *ExtraHeader
|
||||
}
|
||||
|
||||
// commitState is one step of the decoder state machine. Each function reads
|
||||
// the lines it needs, mutates *Commit via s.c, and returns the next state to
|
||||
// run (or nil to terminate the loop).
|
||||
type commitState func(*commitScanner) (commitState, error)
|
||||
|
||||
// readLine returns the next line from the buffer, transparently consuming any
|
||||
// line that was previously pushed back by a state that decided not to handle
|
||||
// it.
|
||||
func (s *commitScanner) readLine() ([]byte, error) {
|
||||
if s.pending != nil {
|
||||
line, err := s.pending, s.pendingErr
|
||||
s.pending, s.pendingErr = nil, nil
|
||||
return line, err
|
||||
}
|
||||
line, err := s.r.ReadBytes('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return line, err
|
||||
}
|
||||
return line, err
|
||||
}
|
||||
|
||||
// pushBack stashes an unconsumed line so the next state's readLine call sees
|
||||
// it. Only one line can be pushed back at a time.
|
||||
func (s *commitScanner) pushBack(line []byte, err error) {
|
||||
s.pending = line
|
||||
s.pendingErr = err
|
||||
}
|
||||
|
||||
// scanTree expects the first non-empty header to be `tree HASH`. Anything
|
||||
// else (or an empty buffer) is rejected with ErrMalformedCommit, matching
|
||||
// upstream's `bogus commit object` check.
|
||||
func scanTree(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 || isBlankLine(line) {
|
||||
return nil, fmt.Errorf("%w: missing tree header", ErrMalformedCommit)
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key != "tree" {
|
||||
return nil, fmt.Errorf("%w: tree header must be first", ErrMalformedCommit)
|
||||
}
|
||||
h, herr := parseObjectIDHex(data, ErrMalformedCommit, "tree")
|
||||
if herr != nil {
|
||||
return nil, herr
|
||||
}
|
||||
s.c.TreeHash = h
|
||||
s.sawTree = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanParents, nil
|
||||
}
|
||||
|
||||
// scanParents consumes contiguous `parent HASH` lines. The first non-parent
|
||||
// line ends the parent block and is handed off to scanAuthor; any later
|
||||
// `parent` line is silently dropped (matches upstream's parse_commit_buffer
|
||||
// exiting its parent loop at the first non-parent line and
|
||||
// read_commit_extra_header_lines filtering `parent` out of extras).
|
||||
func scanParents(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if isBlankLine(line) {
|
||||
return scanMessage, nil
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key == "parent" {
|
||||
h, herr := parseObjectIDHex(data, ErrMalformedCommit, "parent")
|
||||
if herr != nil {
|
||||
return nil, herr
|
||||
}
|
||||
s.c.ParentHashes = append(s.c.ParentHashes, h)
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanParents, nil
|
||||
}
|
||||
s.pushBack(line, err)
|
||||
return scanAuthor, nil
|
||||
}
|
||||
|
||||
// scanAuthor accepts an `author` line at its canonical position immediately
|
||||
// after the parent block. Any other header here is pushed back for
|
||||
// scanCommitter; an out-of-place author is therefore silently dropped.
|
||||
// Mirrors upstream's parse_commit_date func.
|
||||
func scanAuthor(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if isBlankLine(line) {
|
||||
return scanMessage, nil
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key == "author" {
|
||||
s.c.Author.Decode(data)
|
||||
s.sawAuthor = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanCommitter, nil
|
||||
}
|
||||
s.pushBack(line, err)
|
||||
return scanCommitter, nil
|
||||
}
|
||||
|
||||
// scanCommitter accepts a `committer` line at its canonical position
|
||||
// immediately after the author. Any other header is pushed back for
|
||||
// scanHeaders. Same upstream rationale as scanAuthor.
|
||||
func scanCommitter(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if isBlankLine(line) {
|
||||
return scanMessage, nil
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key == "committer" {
|
||||
s.c.Committer.Decode(data)
|
||||
s.sawCommitter = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanHeaders, nil
|
||||
}
|
||||
s.pushBack(line, err)
|
||||
return scanHeaders, nil
|
||||
}
|
||||
|
||||
// scanHeaders dispatches one header line. Continuation-bearing headers
|
||||
// (mergetag, gpgsig, gpgsig-sha256, and unknown extras whose value is
|
||||
// continued on subsequent lines) hand off to a dedicated continuation state
|
||||
// that handles the `<space>...` lines and then returns here.
|
||||
func scanHeaders(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if isBlankLine(line) {
|
||||
return scanMessage, nil
|
||||
}
|
||||
|
||||
originalLine := line
|
||||
key, data := splitHeader(line)
|
||||
|
||||
var next commitState = scanHeaders
|
||||
switch key {
|
||||
case "tree", "parent", "author", "committer":
|
||||
// Anything reaching scanHeaders with one of these keys is out of
|
||||
// canonical position: duplicate tree, parent past the contiguous
|
||||
// block, or author/committer not at their expected slot. Drop them
|
||||
// the same way upstream's standard_header_field filter excludes
|
||||
// them from the extras list (read_commit_extra_header_lines,
|
||||
// commit.c:1520-1522).
|
||||
case headerencoding:
|
||||
if !s.sawEncoding {
|
||||
s.c.Encoding = MessageEncoding(data)
|
||||
s.sawEncoding = true
|
||||
}
|
||||
case headermergetag:
|
||||
if s.sawMergetag {
|
||||
next = scanSkipCont
|
||||
} else {
|
||||
s.c.MergeTag += string(data) + "\n"
|
||||
s.sawMergetag = true
|
||||
next = scanMergetagCont
|
||||
}
|
||||
case headerpgp:
|
||||
s.c.PGPSignature += string(data) + "\n"
|
||||
next = scanPgpCont
|
||||
case headerpgp256:
|
||||
next = scanSkipCont
|
||||
default:
|
||||
h, multiline := parseExtraHeader(originalLine)
|
||||
if multiline {
|
||||
s.extra = &h
|
||||
next = scanExtraCont
|
||||
} else {
|
||||
s.c.ExtraHeaders = append(s.c.ExtraHeaders, h)
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// scanMergetagCont accumulates continuation lines for the first mergetag
|
||||
// header. Continuations strip exactly one leading space, mirroring upstream's
|
||||
// `line + 1` (commit.c:1509). The first non-continuation line is pushed back
|
||||
// so scanHeaders can dispatch it.
|
||||
func scanMergetagCont(s *commitScanner) (commitState, error) {
|
||||
return continuationCont(s, &s.c.MergeTag, scanMergetagCont)
|
||||
}
|
||||
|
||||
// scanPgpCont accumulates continuation lines for a signature header.
|
||||
// Continuations strip exactly one leading space, mirroring upstream's
|
||||
// `line + 1` (commit.c:1509). The first non-continuation line is pushed back
|
||||
// so scanHeaders can dispatch it. Repeat occurrences of the same signature
|
||||
// header land back here and concatenate, matching upstream's
|
||||
// parse_buffer_signed_by_header (commit.c:1186).
|
||||
func scanPgpCont(s *commitScanner) (commitState, error) {
|
||||
return continuationCont(s, &s.c.PGPSignature, scanPgpCont)
|
||||
}
|
||||
|
||||
func continuationCont(s *commitScanner, dst *string, self commitState) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
*dst += string(line[1:])
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return self, nil
|
||||
}
|
||||
if len(line) > 0 {
|
||||
s.pushBack(line, err)
|
||||
}
|
||||
return scanHeaders, nil
|
||||
}
|
||||
|
||||
// scanSkipCont discards continuation lines that belong to a header scanHeaders
|
||||
// chose to drop.
|
||||
func scanSkipCont(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanSkipCont, nil
|
||||
}
|
||||
if len(line) > 0 {
|
||||
s.pushBack(line, err)
|
||||
}
|
||||
return scanHeaders, nil
|
||||
}
|
||||
|
||||
// scanExtraCont accumulates continuation lines for an unknown ExtraHeader
|
||||
// whose value spans multiple lines, then finalises the entry once the
|
||||
// continuation block ends.
|
||||
func scanExtraCont(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
s.extra.Value += string(line[1:])
|
||||
if err == io.EOF {
|
||||
s.finaliseExtra()
|
||||
return nil, nil
|
||||
}
|
||||
return scanExtraCont, nil
|
||||
}
|
||||
s.finaliseExtra()
|
||||
if len(line) > 0 {
|
||||
s.pushBack(line, err)
|
||||
}
|
||||
return scanHeaders, nil
|
||||
}
|
||||
|
||||
func (s *commitScanner) finaliseExtra() {
|
||||
s.extra.Value = strings.TrimRight(s.extra.Value, "\n")
|
||||
s.c.ExtraHeaders = append(s.c.ExtraHeaders, *s.extra)
|
||||
s.extra = nil
|
||||
}
|
||||
|
||||
// scanMessage drains the remaining bytes into the message buffer.
|
||||
func scanMessage(s *commitScanner) (commitState, error) {
|
||||
for {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) > 0 {
|
||||
s.msgbuf.Write(line)
|
||||
}
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isBlankLine reports whether line is the canonical header/body separator:
|
||||
// a single newline. Mirrors upstream's `*line == '\n'` test in
|
||||
// read_commit_extra_header_lines (commit.c:1502).
|
||||
func isBlankLine(line []byte) bool {
|
||||
return len(line) == 1 && line[0] == '\n'
|
||||
}
|
||||
|
||||
// splitHeader returns the header keyword (everything before the first space)
|
||||
// and the value (everything after, with the trailing newline stripped). If
|
||||
// the header has no value the returned data is nil.
|
||||
func splitHeader(line []byte) (string, []byte) {
|
||||
trimmed := bytes.TrimRight(line, "\n")
|
||||
key, value, ok := bytes.Cut(trimmed, []byte{' '})
|
||||
if !ok {
|
||||
return string(trimmed), nil
|
||||
}
|
||||
return string(key), value
|
||||
}
|
||||
|
||||
func parseObjectIDHex(data []byte, malformedErr error, header string) (plumbing.Hash, error) {
|
||||
id := string(data)
|
||||
if !plumbing.IsHash(id) {
|
||||
return plumbing.ZeroHash, fmt.Errorf("%w: bad %s hash", malformedErr, header)
|
||||
}
|
||||
return plumbing.NewHash(id), nil
|
||||
}
|
||||
+121
-1
@@ -1,6 +1,13 @@
|
||||
package object
|
||||
|
||||
import "bytes"
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/utils/ioutil"
|
||||
"github.com/go-git/go-git/v5/utils/sync"
|
||||
)
|
||||
|
||||
const (
|
||||
signatureTypeUnknown signatureType = iota
|
||||
@@ -100,3 +107,116 @@ func parseSignedBytes(b []byte) (int, signatureType) {
|
||||
}
|
||||
return match, t
|
||||
}
|
||||
|
||||
// countSignatureBlocks reports how many distinct armored signature blocks
|
||||
// start at a line boundary in b. Used by verification paths to reject
|
||||
// multi-signature payloads, matching upstream's check in gpg-interface.c
|
||||
// where parse_gpg_output bails out the first time it sees a second
|
||||
// exclusive status line (a second GOODSIG/BADSIG/etc.).
|
||||
func countSignatureBlocks(b []byte) int {
|
||||
n, count := 0, 0
|
||||
for n < len(b) {
|
||||
i := b[n:]
|
||||
if typeForSignature(i) != signatureTypeUnknown {
|
||||
count++
|
||||
}
|
||||
if eol := bytes.IndexByte(i, '\n'); eol >= 0 {
|
||||
n += eol + 1
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// isSignatureHeader reports whether line is a canonical "gpgsig "/
|
||||
// "gpgsig-sha256 " header line. Other "gpgsig"-prefixed extra headers
|
||||
// are intentionally not matched.
|
||||
func isSignatureHeader(line []byte) bool {
|
||||
return bytes.HasPrefix(line, []byte(headerpgp+" ")) ||
|
||||
bytes.HasPrefix(line, []byte(headerpgp256+" "))
|
||||
}
|
||||
|
||||
// stripObjectSignatures streams src into dst, producing the byte sequence
|
||||
// over which a PGP/GPG signature is computed:
|
||||
//
|
||||
// - Canonical "gpgsig" and "gpgsig-sha256" headers (and their
|
||||
// continuation lines) are dropped, mirroring upstream's
|
||||
// remove_signature in commit.c.
|
||||
// - For tag objects, the inline trailing PGP signature is additionally
|
||||
// truncated, mirroring upstream's parse_signature in gpg-interface.c
|
||||
// used by gpg_verify_tag.
|
||||
//
|
||||
// The returned object's type is set to objType. Used by both
|
||||
// Commit.EncodeWithoutSignature and Tag.EncodeWithoutSignature to
|
||||
// reproduce the exact bytes the signature was computed over.
|
||||
func stripObjectSignatures(dst, src plumbing.EncodedObject, objType plumbing.ObjectType) (err error) {
|
||||
dst.SetType(objType)
|
||||
|
||||
r, err := src.Reader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ioutil.CheckClose(r, &err)
|
||||
|
||||
var input io.Reader = r
|
||||
if objType == plumbing.TagObject {
|
||||
raw, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sm, _ := parseSignedBytes(raw); sm >= 0 {
|
||||
raw = raw[:sm]
|
||||
}
|
||||
input = bytes.NewReader(raw)
|
||||
}
|
||||
|
||||
w, err := dst.Writer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ioutil.CheckClose(w, &err)
|
||||
|
||||
return stripHeaderSignatures(w, input)
|
||||
}
|
||||
|
||||
// stripHeaderSignatures copies r to w, dropping canonical signature header
|
||||
// lines (gpgsig and gpgsig-sha256) and their continuation lines. Lines
|
||||
// past the blank line that closes the header block are copied verbatim.
|
||||
func stripHeaderSignatures(w io.Writer, r io.Reader) error {
|
||||
br := sync.GetBufioReader(r)
|
||||
defer sync.PutBufioReader(br)
|
||||
|
||||
var inBody, skipping bool
|
||||
for {
|
||||
line, rerr := br.ReadBytes('\n')
|
||||
if rerr != nil && rerr != io.EOF {
|
||||
return rerr
|
||||
}
|
||||
|
||||
write := true
|
||||
if !inBody {
|
||||
switch {
|
||||
case skipping && len(line) > 0 && line[0] == ' ':
|
||||
write = false
|
||||
case isSignatureHeader(line):
|
||||
skipping = true
|
||||
write = false
|
||||
case len(line) == 1 && line[0] == '\n':
|
||||
skipping = false
|
||||
inBody = true
|
||||
default:
|
||||
skipping = false
|
||||
}
|
||||
}
|
||||
|
||||
if write && len(line) > 0 {
|
||||
if _, werr := w.Write(line); werr != nil {
|
||||
return werr
|
||||
}
|
||||
}
|
||||
if rerr == io.EOF {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+90
-45
@@ -1,9 +1,8 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
@@ -13,6 +12,10 @@ import (
|
||||
"github.com/go-git/go-git/v5/utils/sync"
|
||||
)
|
||||
|
||||
// ErrMalformedTag is returned when a tag object cannot be decoded because
|
||||
// its required headers (object, type, tag) are missing or out of order.
|
||||
var ErrMalformedTag = errors.New("malformed tag")
|
||||
|
||||
// Tag represents an annotated tag object. It points to a single git object of
|
||||
// any type, but tags typically are applied to commit or blob objects. It
|
||||
// provides a reference that associates the target with a tag name. It also
|
||||
@@ -39,6 +42,9 @@ type Tag struct {
|
||||
Target plumbing.Hash
|
||||
|
||||
s storer.EncodedObjectStorer
|
||||
// src holds the encoded object this Tag was decoded from, used by
|
||||
// EncodeWithoutSignature to recover the canonical signed bytes.
|
||||
src plumbing.EncodedObject
|
||||
}
|
||||
|
||||
// GetTag gets a tag from an object storer and decodes it.
|
||||
@@ -77,13 +83,20 @@ func (t *Tag) Type() plumbing.ObjectType {
|
||||
return plumbing.TagObject
|
||||
}
|
||||
|
||||
func (t *Tag) reset() {
|
||||
storer := t.s
|
||||
*t = Tag{s: storer}
|
||||
}
|
||||
|
||||
// Decode transforms a plumbing.EncodedObject into a Tag struct.
|
||||
func (t *Tag) Decode(o plumbing.EncodedObject) (err error) {
|
||||
if o.Type() != plumbing.TagObject {
|
||||
return ErrUnsupportedObject
|
||||
}
|
||||
|
||||
t.reset()
|
||||
t.Hash = o.Hash()
|
||||
t.src = o
|
||||
|
||||
reader, err := o.Reader()
|
||||
if err != nil {
|
||||
@@ -94,42 +107,15 @@ func (t *Tag) Decode(o plumbing.EncodedObject) (err error) {
|
||||
r := sync.GetBufioReader(reader)
|
||||
defer sync.PutBufioReader(r)
|
||||
|
||||
for {
|
||||
var line []byte
|
||||
line, err = r.ReadBytes('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
scanner := &tagScanner{r: r, t: t}
|
||||
for state := scanTagObject; state != nil; {
|
||||
state, err = state(scanner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
break // Start of message
|
||||
}
|
||||
|
||||
split := bytes.SplitN(line, []byte{' '}, 2)
|
||||
switch string(split[0]) {
|
||||
case "object":
|
||||
t.Target = plumbing.NewHash(string(split[1]))
|
||||
case "type":
|
||||
t.TargetType, err = plumbing.ParseObjectType(string(split[1]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "tag":
|
||||
t.Name = string(split[1])
|
||||
case "tagger":
|
||||
t.Tagger.Decode(split[1])
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := scanner.msgbuf.Bytes()
|
||||
if sm, _ := parseSignedBytes(data); sm >= 0 {
|
||||
t.PGPSignature = string(data[sm:])
|
||||
data = data[:sm]
|
||||
@@ -144,11 +130,54 @@ func (t *Tag) Encode(o plumbing.EncodedObject) error {
|
||||
return t.encode(o, true)
|
||||
}
|
||||
|
||||
// EncodeWithoutSignature export a Tag into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature).
|
||||
// EncodeWithoutSignature exports a Tag into a plumbing.EncodedObject without
|
||||
// any signature data, producing the payload that PGP/GPG signatures are
|
||||
// computed over.
|
||||
//
|
||||
// Behaviour mirrors Commit.EncodeWithoutSignature:
|
||||
//
|
||||
// - For Tags populated by Decode whose exported fields still match the
|
||||
// source object, the payload is streamed from the raw source bytes with
|
||||
// the inline trailing signature truncated and gpgsig/gpgsig-sha256
|
||||
// headers (and their continuation lines) stripped verbatim. This
|
||||
// preserves the exact bytes the signature was computed over, regardless
|
||||
// of any normalization performed by Decode.
|
||||
//
|
||||
// - For Tags constructed in memory, or for decoded Tags whose exported
|
||||
// fields have been mutated, the payload is derived from the current
|
||||
// struct fields. Mutation is detected by re-decoding the source object
|
||||
// and comparing exported fields; if any differ, the in-memory
|
||||
// representation prevails.
|
||||
func (t *Tag) EncodeWithoutSignature(o plumbing.EncodedObject) error {
|
||||
if t.matchesSource() {
|
||||
return stripObjectSignatures(o, t.src, plumbing.TagObject)
|
||||
}
|
||||
return t.encode(o, false)
|
||||
}
|
||||
|
||||
// matchesSource reports whether t.src is set and re-decoding it produces a
|
||||
// Tag whose payload-affecting exported fields are identical to those of t.
|
||||
//
|
||||
// PGPSignature is intentionally excluded from the comparison: neither path
|
||||
// emits it as part of the verification payload, so mutating it must not
|
||||
// trigger a switch to struct-encode (which would change the byte layout the
|
||||
// caller is trying to verify against).
|
||||
func (t *Tag) matchesSource() bool {
|
||||
if t.src == nil {
|
||||
return false
|
||||
}
|
||||
fresh := &Tag{}
|
||||
if err := fresh.Decode(t.src); err != nil {
|
||||
return false
|
||||
}
|
||||
return t.Hash == fresh.Hash &&
|
||||
t.Name == fresh.Name &&
|
||||
signatureEqual(t.Tagger, fresh.Tagger) &&
|
||||
t.Message == fresh.Message &&
|
||||
t.TargetType == fresh.TargetType &&
|
||||
t.Target == fresh.Target
|
||||
}
|
||||
|
||||
func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
o.SetType(plumbing.TagObject)
|
||||
w, err := o.Writer()
|
||||
@@ -158,16 +187,26 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
defer ioutil.CheckClose(w, &err)
|
||||
|
||||
if _, err = fmt.Fprintf(w,
|
||||
"object %s\ntype %s\ntag %s\ntagger ",
|
||||
"object %s\ntype %s\ntag %s\n",
|
||||
t.Target.String(), t.TargetType.Bytes(), t.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = t.Tagger.Encode(w); err != nil {
|
||||
return err
|
||||
if !isZeroSignature(t.Tagger) {
|
||||
if _, err = fmt.Fprint(w, "tagger "); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = t.Tagger.Encode(w); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprint(w, "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprint(w, "\n\n"); err != nil {
|
||||
if _, err = fmt.Fprint(w, "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -175,11 +214,12 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Note that this is highly sensitive to what it sent along in the message.
|
||||
// Message *always* needs to end with a newline, or else the message and the
|
||||
// signature will be concatenated into a corrupt object. Since this is a
|
||||
// lower-level method, we assume you know what you are doing and have already
|
||||
// done the needful on the message in the caller.
|
||||
// Note that this is highly sensitive to what is sent along in the
|
||||
// message. Message *always* needs to end with a newline, or else the
|
||||
// message and the trailing signature will be concatenated into a
|
||||
// corrupt object. Since this is a lower-level method, we assume you
|
||||
// know what you are doing and have already done the needful on the
|
||||
// message in the caller.
|
||||
if includeSig {
|
||||
if _, err = fmt.Fprint(w, t.PGPSignature); err != nil {
|
||||
return err
|
||||
@@ -189,6 +229,10 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func isZeroSignature(s Signature) bool {
|
||||
return s.Name == "" && s.Email == "" && s.When.IsZero()
|
||||
}
|
||||
|
||||
// Commit returns the commit pointed to by the tag. If the tag points to a
|
||||
// different type of object ErrUnsupportedObject will be returned.
|
||||
func (t *Tag) Commit() (*Commit, error) {
|
||||
@@ -256,7 +300,8 @@ func (t *Tag) String() string {
|
||||
}
|
||||
|
||||
// Verify performs PGP verification of the tag with a provided armored
|
||||
// keyring and returns openpgp.Entity associated with verifying key on success.
|
||||
// keyring and returns openpgp.Entity associated with verifying key on
|
||||
// success.
|
||||
func (t *Tag) Verify(armoredKeyRing string) (*openpgp.Entity, error) {
|
||||
keyRingReader := strings.NewReader(armoredKeyRing)
|
||||
keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader)
|
||||
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// tagScanner holds the working state of the tag decoder driven by the
|
||||
// stateFn loop in (*Tag).Decode. Each tagState reads one or more lines
|
||||
// from r, updates the in-progress *Tag and the scanner's bookkeeping,
|
||||
// and returns the state that should run next (or nil to stop).
|
||||
type tagScanner struct {
|
||||
r *bufio.Reader
|
||||
t *Tag
|
||||
msgbuf bytes.Buffer
|
||||
|
||||
// pending holds a line that was read but the current state decided to
|
||||
// hand back to the next state, paired with the io.EOF flag returned
|
||||
// when the line was originally read.
|
||||
pending []byte
|
||||
pendingErr error
|
||||
|
||||
// First-occurrence tracking: once the corresponding canonical
|
||||
// header has been decoded at its expected position, subsequent
|
||||
// occurrences (or out-of-position lines) are silently dropped,
|
||||
// matching the strict layout enforced by upstream's
|
||||
// parse_tag_buffer (tag.c:130).
|
||||
//
|
||||
// gpgsig-sha256 is recognized and skipped without exposing a new field
|
||||
// in v5.
|
||||
sawObject, sawType, sawName, sawTagger bool
|
||||
}
|
||||
|
||||
// tagState is one step of the decoder state machine. Each function reads
|
||||
// the lines it needs, mutates *Tag via s.t, and returns the next state
|
||||
// to run (or nil to terminate the loop).
|
||||
type tagState func(*tagScanner) (tagState, error)
|
||||
|
||||
// readLine returns the next line from the buffer, transparently
|
||||
// consuming any line that was previously pushed back by a state that
|
||||
// decided not to handle it.
|
||||
func (s *tagScanner) readLine() ([]byte, error) {
|
||||
if s.pending != nil {
|
||||
line, err := s.pending, s.pendingErr
|
||||
s.pending, s.pendingErr = nil, nil
|
||||
return line, err
|
||||
}
|
||||
return s.r.ReadBytes('\n')
|
||||
}
|
||||
|
||||
// pushBack stashes an unconsumed line so the next state's readLine call
|
||||
// sees it. Only one line can be pushed back at a time.
|
||||
func (s *tagScanner) pushBack(line []byte, err error) {
|
||||
s.pending = line
|
||||
s.pendingErr = err
|
||||
}
|
||||
|
||||
// scanTagObject requires the first line to be `object HASH`, mirroring
|
||||
// upstream's strict parse_tag_buffer (tag.c:151-156). Anything else
|
||||
// returns ErrMalformedTag.
|
||||
func scanTagObject(s *tagScanner) (tagState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 || isBlankLine(line) {
|
||||
return nil, fmt.Errorf("%w: missing object header", ErrMalformedTag)
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key != "object" {
|
||||
return nil, fmt.Errorf("%w: object header must be first", ErrMalformedTag)
|
||||
}
|
||||
h, herr := parseObjectIDHex(data, ErrMalformedTag, "object")
|
||||
if herr != nil {
|
||||
return nil, herr
|
||||
}
|
||||
s.t.Target = h
|
||||
s.sawObject = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanTagType, nil
|
||||
}
|
||||
|
||||
// scanTagType requires a `type` line immediately after the object header,
|
||||
// mirroring upstream's parse_tag_buffer (tag.c:158-166).
|
||||
func scanTagType(s *tagScanner) (tagState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 || isBlankLine(line) {
|
||||
return nil, fmt.Errorf("%w: missing type header", ErrMalformedTag)
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key != "type" {
|
||||
return nil, fmt.Errorf("%w: type header must follow object", ErrMalformedTag)
|
||||
}
|
||||
ot, perr := plumbing.ParseObjectType(string(data))
|
||||
if perr != nil {
|
||||
return nil, perr
|
||||
}
|
||||
s.t.TargetType = ot
|
||||
s.sawType = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanTagName, nil
|
||||
}
|
||||
|
||||
// scanTagName requires a `tag` line immediately after the type header,
|
||||
// mirroring upstream's parse_tag_buffer (tag.c:186-194).
|
||||
func scanTagName(s *tagScanner) (tagState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 || isBlankLine(line) {
|
||||
return nil, fmt.Errorf("%w: missing tag header", ErrMalformedTag)
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key != "tag" {
|
||||
return nil, fmt.Errorf("%w: tag header must follow type", ErrMalformedTag)
|
||||
}
|
||||
s.t.Name = string(data)
|
||||
s.sawName = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanTagTagger, nil
|
||||
}
|
||||
|
||||
// scanTagTagger accepts a `tagger` line at its canonical position. Any
|
||||
// other header is pushed back for scanTagHeaders.
|
||||
func scanTagTagger(s *tagScanner) (tagState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if isBlankLine(line) {
|
||||
return scanTagMessage, nil
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key == "tagger" {
|
||||
s.t.Tagger.Decode(data)
|
||||
s.sawTagger = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanTagHeaders, nil
|
||||
}
|
||||
s.pushBack(line, err)
|
||||
return scanTagHeaders, nil
|
||||
}
|
||||
|
||||
// scanTagHeaders dispatches one header line. gpgsig-sha256 hands off to
|
||||
// scanTagSkipCont so the continuation block can be consumed; out-of-position
|
||||
// canonical fields and unknown headers are silently dropped.
|
||||
func scanTagHeaders(s *tagScanner) (tagState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if isBlankLine(line) {
|
||||
return scanTagMessage, nil
|
||||
}
|
||||
|
||||
key, _ := splitHeader(line)
|
||||
next := scanTagHeaders
|
||||
switch key {
|
||||
case "object", "type", "tag", "tagger":
|
||||
// Out-of-canonical-position duplicates are dropped, mirroring the
|
||||
// strict ordering of upstream's parse_tag_buffer.
|
||||
case headerpgp256:
|
||||
next = scanTagSkipCont
|
||||
default:
|
||||
// Unknown header: silently dropped (the Tag struct does not
|
||||
// expose ExtraHeaders).
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// scanTagSkipCont discards continuation lines for a header scanTagHeaders chose
|
||||
// to drop. The first non-continuation line is pushed back so scanTagHeaders can
|
||||
// dispatch it.
|
||||
func scanTagSkipCont(s *tagScanner) (tagState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanTagSkipCont, nil
|
||||
}
|
||||
if len(line) > 0 {
|
||||
s.pushBack(line, err)
|
||||
}
|
||||
return scanTagHeaders, nil
|
||||
}
|
||||
|
||||
// scanTagMessage drains the remaining bytes into the message buffer.
|
||||
// (*Tag).Decode then runs parseSignedBytes over those bytes to peel off
|
||||
// the optional inline trailing PGP signature.
|
||||
func scanTagMessage(s *tagScanner) (tagState, error) {
|
||||
for {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) > 0 {
|
||||
s.msgbuf.Write(line)
|
||||
}
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
-33
@@ -29,6 +29,7 @@ var (
|
||||
ErrDirectoryNotFound = errors.New("directory not found")
|
||||
ErrEntryNotFound = errors.New("entry not found")
|
||||
ErrEntriesNotSorted = errors.New("entries in tree are not sorted")
|
||||
ErrMalformedTree = errors.New("malformed tree")
|
||||
)
|
||||
|
||||
// Tree is basically like a directory - it references a bunch of other trees
|
||||
@@ -37,9 +38,9 @@ type Tree struct {
|
||||
Entries []TreeEntry
|
||||
Hash plumbing.Hash
|
||||
|
||||
s storer.EncodedObjectStorer
|
||||
m map[string]*TreeEntry
|
||||
t map[string]*Tree // tree path cache
|
||||
s storer.EncodedObjectStorer
|
||||
t map[string]*Tree // tree path cache
|
||||
entriesSorted bool
|
||||
}
|
||||
|
||||
// GetTree gets a tree from an object storer and decodes it.
|
||||
@@ -182,16 +183,43 @@ func (t *Tree) dir(baseName string) (*Tree, error) {
|
||||
}
|
||||
|
||||
func (t *Tree) entry(baseName string) (*TreeEntry, error) {
|
||||
if t.m == nil {
|
||||
t.buildMap()
|
||||
}
|
||||
|
||||
entry, ok := t.m[baseName]
|
||||
if !ok {
|
||||
if t.entriesSorted {
|
||||
if entry := t.searchEntry(baseName); entry != nil {
|
||||
return entry, nil
|
||||
}
|
||||
return nil, ErrEntryNotFound
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
pastName := baseName + "/"
|
||||
for i := range t.Entries {
|
||||
entry := &t.Entries[i]
|
||||
if entry.Name == baseName {
|
||||
return entry, nil
|
||||
}
|
||||
if treeEntrySortName(entry) > pastName {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrEntryNotFound
|
||||
}
|
||||
|
||||
func (t *Tree) searchEntry(baseName string) *TreeEntry {
|
||||
if i := t.searchEntryIndex(baseName); i < len(t.Entries) && t.Entries[i].Name == baseName {
|
||||
return &t.Entries[i]
|
||||
}
|
||||
|
||||
if i := t.searchEntryIndex(baseName + "/"); i < len(t.Entries) && t.Entries[i].Name == baseName {
|
||||
return &t.Entries[i]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tree) searchEntryIndex(name string) int {
|
||||
return sort.Search(len(t.Entries), func(i int) bool {
|
||||
return treeEntrySortName(&t.Entries[i]) >= name
|
||||
})
|
||||
}
|
||||
|
||||
// Files returns a FileIter allowing to iterate over the Tree
|
||||
@@ -212,20 +240,25 @@ func (t *Tree) Type() plumbing.ObjectType {
|
||||
return plumbing.TreeObject
|
||||
}
|
||||
|
||||
func (t *Tree) reset() {
|
||||
storer := t.s
|
||||
*t = Tree{s: storer}
|
||||
}
|
||||
|
||||
// Decode transform an plumbing.EncodedObject into a Tree struct
|
||||
func (t *Tree) Decode(o plumbing.EncodedObject) (err error) {
|
||||
if o.Type() != plumbing.TreeObject {
|
||||
return ErrUnsupportedObject
|
||||
}
|
||||
|
||||
t.reset()
|
||||
t.Hash = o.Hash()
|
||||
// assume tree is sorted as a valid tree should always be sorted.
|
||||
t.entriesSorted = true
|
||||
if o.Size() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.Entries = nil
|
||||
t.m = nil
|
||||
|
||||
reader, err := o.Reader()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -235,10 +268,14 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) {
|
||||
r := sync.GetBufioReader(reader)
|
||||
defer sync.PutBufioReader(r)
|
||||
|
||||
var prevSortName string
|
||||
for {
|
||||
str, err := r.ReadString(' ')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
if len(str) != 0 {
|
||||
return fmt.Errorf("%w: missing mode terminator", ErrMalformedTree)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -248,25 +285,41 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) {
|
||||
|
||||
mode, err := filemode.New(str)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%w: malformed mode", ErrMalformedTree)
|
||||
}
|
||||
mode = canonicalTreeMode(mode)
|
||||
|
||||
name, err := r.ReadString(0)
|
||||
if err != nil && err != io.EOF {
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return fmt.Errorf("%w: missing filename terminator", ErrMalformedTree)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if len(name) == 1 {
|
||||
return fmt.Errorf("%w: empty filename", ErrMalformedTree)
|
||||
}
|
||||
|
||||
var hash plumbing.Hash
|
||||
if _, err = io.ReadFull(r, hash[:]); err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return fmt.Errorf("%w: truncated object id", ErrMalformedTree)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
baseName := name[:len(name)-1]
|
||||
t.Entries = append(t.Entries, TreeEntry{
|
||||
entry := TreeEntry{
|
||||
Hash: hash,
|
||||
Mode: mode,
|
||||
Name: baseName,
|
||||
})
|
||||
}
|
||||
sortName := treeEntrySortName(&entry)
|
||||
if len(t.Entries) != 0 && prevSortName > sortName {
|
||||
t.entriesSorted = false
|
||||
}
|
||||
prevSortName = sortName
|
||||
t.Entries = append(t.Entries, entry)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -279,21 +332,37 @@ func (s TreeEntrySorter) Len() int {
|
||||
}
|
||||
|
||||
func (s TreeEntrySorter) Less(i, j int) bool {
|
||||
name1 := s[i].Name
|
||||
name2 := s[j].Name
|
||||
if s[i].Mode == filemode.Dir {
|
||||
name1 += "/"
|
||||
}
|
||||
if s[j].Mode == filemode.Dir {
|
||||
name2 += "/"
|
||||
}
|
||||
return name1 < name2
|
||||
return treeEntrySortName(&s[i]) < treeEntrySortName(&s[j])
|
||||
}
|
||||
|
||||
func (s TreeEntrySorter) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// Git compares tree entries as if directory names had a trailing slash.
|
||||
func treeEntrySortName(e *TreeEntry) string {
|
||||
if e.Mode == filemode.Dir {
|
||||
return e.Name + "/"
|
||||
}
|
||||
return e.Name
|
||||
}
|
||||
|
||||
func canonicalTreeMode(mode filemode.FileMode) filemode.FileMode {
|
||||
switch mode & 0o170000 {
|
||||
case 0o040000:
|
||||
return filemode.Dir
|
||||
case 0o100000:
|
||||
if mode&0o111 != 0 {
|
||||
return filemode.Executable
|
||||
}
|
||||
return filemode.Regular
|
||||
case 0o120000:
|
||||
return filemode.Symlink
|
||||
default:
|
||||
return filemode.Submodule
|
||||
}
|
||||
}
|
||||
|
||||
// Encode transforms a Tree into a plumbing.EncodedObject.
|
||||
// The tree entries must be sorted by name.
|
||||
func (t *Tree) Encode(o plumbing.EncodedObject) (err error) {
|
||||
@@ -329,13 +398,6 @@ func (t *Tree) Encode(o plumbing.EncodedObject) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Tree) buildMap() {
|
||||
t.m = make(map[string]*TreeEntry)
|
||||
for i := 0; i < len(t.Entries); i++ {
|
||||
t.m[t.Entries[i].Name] = &t.Entries[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Diff returns a list of changes between this tree and the provided one
|
||||
func (t *Tree) Diff(to *Tree) (Changes, error) {
|
||||
return t.DiffContext(context.Background(), to)
|
||||
|
||||
+5
-1
@@ -281,7 +281,7 @@ Exit Code 1
|
||||
| AMXBF16 | Tile computational operations on BFLOAT16 numbers |
|
||||
| AMXINT8 | Tile computational operations on 8-bit integers |
|
||||
| AMXFP16 | Tile computational operations on FP16 numbers |
|
||||
| AMXFP8 | Tile computational operations on FP8 numbers |
|
||||
| AMXFP8 | Tile computational operations on FP8 numbers |
|
||||
| AMXCOMPLEX | Tile computational operations on complex numbers |
|
||||
| AMXTILE | Tile architecture |
|
||||
| AMXTF32 | Matrix Multiplication of TF32 Tiles into Packed Single Precision Tile |
|
||||
@@ -418,6 +418,7 @@ Exit Code 1
|
||||
| SEV_SNP | AMD SEV Secure Nested Paging supported |
|
||||
| SGX | Software Guard Extensions |
|
||||
| SGXLC | Software Guard Extensions Launch Control |
|
||||
| SGXPQC | Software Guard Extensions 256-bit Encryption |
|
||||
| SHA | Intel SHA Extensions |
|
||||
| SME | AMD Secure Memory Encryption supported |
|
||||
| SME_COHERENT | AMD Hardware cache coherency across encryption domains enforced |
|
||||
@@ -450,6 +451,9 @@ Exit Code 1
|
||||
| TLB_FLUSH_NESTED | AMD: Flushing includes all the nested translations for guest translations |
|
||||
| TME | Intel Total Memory Encryption. The following MSRs are supported: IA32_TME_CAPABILITY, IA32_TME_ACTIVATE, IA32_TME_EXCLUDE_MASK, and IA32_TME_EXCLUDE_BASE. |
|
||||
| TOPEXT | TopologyExtensions: topology extensions support. Indicates support for CPUID Fn8000_001D_EAX_x[N:0]-CPUID Fn8000_001E_EDX. |
|
||||
| TSA_L1_NO | AMD only: Not vulnerable to TSA-L1 |
|
||||
| TSA_SQ_NO | AMD only: Not vulnerable to TSA-SQ |
|
||||
| TSA_VERW_CLEAR | AMD: If set, the memory form of the VERW instruction may be used to help mitigate TSA |
|
||||
| TSCRATEMSR | MSR based TSC rate control. Indicates support for MSR TSC ratio MSRC000_0104 |
|
||||
| TSXLDTRK | Intel TSX Suspend Load Address Tracking |
|
||||
| VAES | Vector AES. AVX(512) versions requires additional checks. |
|
||||
|
||||
+103
-1
@@ -220,6 +220,7 @@ const (
|
||||
SEV_SNP // AMD SEV Secure Nested Paging supported
|
||||
SGX // Software Guard Extensions
|
||||
SGXLC // Software Guard Extensions Launch Control
|
||||
SGXPQC // Software Guard Extensions 256-bit Encryption
|
||||
SHA // Intel SHA Extensions
|
||||
SME // AMD Secure Memory Encryption supported
|
||||
SME_COHERENT // AMD Hardware cache coherency across encryption domains enforced
|
||||
@@ -255,6 +256,9 @@ const (
|
||||
TLB_FLUSH_NESTED // AMD: Flushing includes all the nested translations for guest translations
|
||||
TME // Intel Total Memory Encryption. The following MSRs are supported: IA32_TME_CAPABILITY, IA32_TME_ACTIVATE, IA32_TME_EXCLUDE_MASK, and IA32_TME_EXCLUDE_BASE.
|
||||
TOPEXT // TopologyExtensions: topology extensions support. Indicates support for CPUID Fn8000_001D_EAX_x[N:0]-CPUID Fn8000_001E_EDX.
|
||||
TSA_L1_NO // AMD only: Not vulnerable to TSA-L1
|
||||
TSA_SQ_NO // AM onlyD: Not vulnerable to TSA-SQ
|
||||
TSA_VERW_CLEAR // If set, the memory form of the VERW instruction may be used to help mitigate TSA
|
||||
TSCRATEMSR // MSR based TSC rate control. Indicates support for MSR TSC ratio MSRC000_0104
|
||||
TSXLDTRK // Intel TSX Suspend Load Address Tracking
|
||||
VAES // Vector AES. AVX(512) versions requires additional checks.
|
||||
@@ -304,6 +308,13 @@ const (
|
||||
SM3 // SM3 instructions
|
||||
SM4 // SM4 instructions
|
||||
SVE // Scalable Vector Extension
|
||||
|
||||
// PMU
|
||||
PMU_FIXEDCOUNTER_CYCLES
|
||||
PMU_FIXEDCOUNTER_REFCYCLES
|
||||
PMU_FIXEDCOUNTER_INSTRUCTIONS
|
||||
PMU_FIXEDCOUNTER_TOPDOWN_SLOTS
|
||||
|
||||
// Keep it last. It automatically defines the size of []flagSet
|
||||
lastID
|
||||
|
||||
@@ -336,11 +347,36 @@ type CPUInfo struct {
|
||||
SGX SGXSupport
|
||||
AMDMemEncryption AMDMemEncryptionSupport
|
||||
AVX10Level uint8
|
||||
PMU PerformanceMonitoringInfo // holds information about the PMU
|
||||
|
||||
maxFunc uint32
|
||||
maxExFunc uint32
|
||||
}
|
||||
|
||||
// PerformanceMonitoringInfo holds information about CPU performance monitoring capabilities.
|
||||
// This is primarily populated from CPUID leaf 0xAh on x86
|
||||
type PerformanceMonitoringInfo struct {
|
||||
// VersionID (x86 only): Version ID of architectural performance monitoring.
|
||||
// A value of 0 means architectural performance monitoring is not supported or information is unavailable.
|
||||
VersionID uint8
|
||||
// NumGPPMC: Number of General-Purpose Performance Monitoring Counters per logical processor.
|
||||
// On ARM, this is derived from PMCR_EL0.N (number of event counters).
|
||||
NumGPCounters uint8
|
||||
// GPPMCWidth: Bit width of General-Purpose Performance Monitoring Counters.
|
||||
// On ARM, typically 64 for PMU event counters.
|
||||
GPPMCWidth uint8
|
||||
// NumFixedPMC: Number of Fixed-Function Performance Counters.
|
||||
// Valid on x86 if VersionID > 1. On ARM, this typically includes at least the cycle counter (PMCCNTR_EL0).
|
||||
NumFixedPMC uint8
|
||||
// FixedPMCWidth: Bit width of Fixed-Function Performance Counters.
|
||||
// Valid on x86 if VersionID > 1. On ARM, the cycle counter (PMCCNTR_EL0) is 64-bit.
|
||||
FixedPMCWidth uint8
|
||||
// Raw register output from CPUID leaf 0xAh.
|
||||
RawEBX uint32
|
||||
RawEAX uint32
|
||||
RawEDX uint32
|
||||
}
|
||||
|
||||
var cpuid func(op uint32) (eax, ebx, ecx, edx uint32)
|
||||
var cpuidex func(op, op2 uint32) (eax, ebx, ecx, edx uint32)
|
||||
var xgetbv func(index uint32) (eax, edx uint32)
|
||||
@@ -1358,6 +1394,11 @@ func support() flagSet {
|
||||
fs.setIf(edx&(1<<4) != 0, BHI_CTRL)
|
||||
fs.setIf(edx&(1<<5) != 0, MCDT_NO)
|
||||
|
||||
if fs.inSet(SGX) {
|
||||
eax, _, _, _ := cpuidex(0x12, 0)
|
||||
fs.setIf(eax&(1<<12) != 0, SGXPQC)
|
||||
}
|
||||
|
||||
// Add keylocker features.
|
||||
if fs.inSet(KEYLOCKER) && mfi >= 0x19 {
|
||||
_, ebx, _, _ := cpuidex(0x19, 0)
|
||||
@@ -1371,6 +1412,7 @@ func support() flagSet {
|
||||
fs.setIf(ebx&(1<<17) != 0, AVX10_256)
|
||||
fs.setIf(ebx&(1<<18) != 0, AVX10_512)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Processor Extended State Enumeration Sub-leaf (EAX = 0DH, ECX = 1)
|
||||
@@ -1514,12 +1556,28 @@ func support() flagSet {
|
||||
}
|
||||
|
||||
if maxExtendedFunction() >= 0x80000021 && vend == AMD {
|
||||
a, _, _, _ := cpuid(0x80000021)
|
||||
a, _, c, _ := cpuid(0x80000021)
|
||||
fs.setIf((a>>31)&1 == 1, SRSO_MSR_FIX)
|
||||
fs.setIf((a>>30)&1 == 1, SRSO_USER_KERNEL_NO)
|
||||
fs.setIf((a>>29)&1 == 1, SRSO_NO)
|
||||
fs.setIf((a>>28)&1 == 1, IBPB_BRTYPE)
|
||||
fs.setIf((a>>27)&1 == 1, SBPB)
|
||||
fs.setIf((c>>1)&1 == 1, TSA_L1_NO)
|
||||
fs.setIf((c>>2)&1 == 1, TSA_SQ_NO)
|
||||
fs.setIf((a>>5)&1 == 1, TSA_VERW_CLEAR)
|
||||
}
|
||||
if vend == AMD {
|
||||
if family < 0x19 {
|
||||
// AMD CPUs that are older than Family 19h are not vulnerable to TSA but do not set TSA_L1_NO or TSA_SQ_NO.
|
||||
// Source: https://www.amd.com/content/dam/amd/en/documents/resources/bulletin/technical-guidance-for-mitigating-transient-scheduler-attacks.pdf
|
||||
fs.set(TSA_L1_NO)
|
||||
fs.set(TSA_SQ_NO)
|
||||
} else if family == 0x1a {
|
||||
// AMD Family 1Ah models 00h-4Fh and 60h-7Fh are also not vulnerable to TSA but do not set TSA_L1_NO or TSA_SQ_NO.
|
||||
// Future AMD CPUs will set these CPUID bits if appropriate. CPUs will be designed to set these CPUID bits if appropriate.
|
||||
notVuln := model <= 0x4f || (model >= 0x60 && model <= 0x7f)
|
||||
fs.setIf(notVuln, TSA_L1_NO, TSA_SQ_NO)
|
||||
}
|
||||
}
|
||||
|
||||
if mfi >= 0x20 {
|
||||
@@ -1575,3 +1633,47 @@ func valAsString(values ...uint32) []byte {
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func parseLeaf0AH(c *CPUInfo, eax, ebx, edx uint32) (info PerformanceMonitoringInfo) {
|
||||
info.VersionID = uint8(eax & 0xFF)
|
||||
info.NumGPCounters = uint8((eax >> 8) & 0xFF)
|
||||
info.GPPMCWidth = uint8((eax >> 16) & 0xFF)
|
||||
|
||||
info.RawEBX = ebx
|
||||
info.RawEAX = eax
|
||||
info.RawEDX = edx
|
||||
|
||||
if info.VersionID > 1 { // This information is only valid if VersionID > 1
|
||||
info.NumFixedPMC = uint8(edx & 0x1F) // Bits 4:0
|
||||
info.FixedPMCWidth = uint8((edx >> 5) & 0xFF) // Bits 12:5
|
||||
}
|
||||
if info.VersionID > 0 {
|
||||
// first 4 fixed events are always instructions retired, cycles, ref cycles and topdown slots
|
||||
if ebx == 0x0 && info.NumFixedPMC == 3 {
|
||||
c.featureSet.set(PMU_FIXEDCOUNTER_INSTRUCTIONS)
|
||||
c.featureSet.set(PMU_FIXEDCOUNTER_CYCLES)
|
||||
c.featureSet.set(PMU_FIXEDCOUNTER_REFCYCLES)
|
||||
}
|
||||
if ebx == 0x0 && info.NumFixedPMC == 4 {
|
||||
c.featureSet.set(PMU_FIXEDCOUNTER_INSTRUCTIONS)
|
||||
c.featureSet.set(PMU_FIXEDCOUNTER_CYCLES)
|
||||
c.featureSet.set(PMU_FIXEDCOUNTER_REFCYCLES)
|
||||
c.featureSet.set(PMU_FIXEDCOUNTER_TOPDOWN_SLOTS)
|
||||
}
|
||||
if ebx != 0x0 {
|
||||
if ((ebx >> 0) & 1) == 0 {
|
||||
c.featureSet.set(PMU_FIXEDCOUNTER_INSTRUCTIONS)
|
||||
}
|
||||
if ((ebx >> 1) & 1) == 0 {
|
||||
c.featureSet.set(PMU_FIXEDCOUNTER_CYCLES)
|
||||
}
|
||||
if ((ebx >> 2) & 1) == 0 {
|
||||
c.featureSet.set(PMU_FIXEDCOUNTER_REFCYCLES)
|
||||
}
|
||||
if ((ebx >> 3) & 1) == 0 {
|
||||
c.featureSet.set(PMU_FIXEDCOUNTER_TOPDOWN_SLOTS)
|
||||
}
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
+4
@@ -36,6 +36,10 @@ func addInfo(c *CPUInfo, safe bool) {
|
||||
c.AVX10Level = c.supportAVX10()
|
||||
c.cacheSize()
|
||||
c.frequencies()
|
||||
if c.maxFunc >= 0x0A {
|
||||
eax, ebx, _, edx := cpuid(0x0A)
|
||||
c.PMU = parseLeaf0AH(c, eax, ebx, edx)
|
||||
}
|
||||
}
|
||||
|
||||
func getVectorLength() (vl, pl uint64) { return 0, 0 }
|
||||
|
||||
+93
-85
@@ -154,95 +154,103 @@ func _() {
|
||||
_ = x[SEV_SNP-144]
|
||||
_ = x[SGX-145]
|
||||
_ = x[SGXLC-146]
|
||||
_ = x[SHA-147]
|
||||
_ = x[SME-148]
|
||||
_ = x[SME_COHERENT-149]
|
||||
_ = x[SM3_X86-150]
|
||||
_ = x[SM4_X86-151]
|
||||
_ = x[SPEC_CTRL_SSBD-152]
|
||||
_ = x[SRBDS_CTRL-153]
|
||||
_ = x[SRSO_MSR_FIX-154]
|
||||
_ = x[SRSO_NO-155]
|
||||
_ = x[SRSO_USER_KERNEL_NO-156]
|
||||
_ = x[SSE-157]
|
||||
_ = x[SSE2-158]
|
||||
_ = x[SSE3-159]
|
||||
_ = x[SSE4-160]
|
||||
_ = x[SSE42-161]
|
||||
_ = x[SSE4A-162]
|
||||
_ = x[SSSE3-163]
|
||||
_ = x[STIBP-164]
|
||||
_ = x[STIBP_ALWAYSON-165]
|
||||
_ = x[STOSB_SHORT-166]
|
||||
_ = x[SUCCOR-167]
|
||||
_ = x[SVM-168]
|
||||
_ = x[SVMDA-169]
|
||||
_ = x[SVMFBASID-170]
|
||||
_ = x[SVML-171]
|
||||
_ = x[SVMNP-172]
|
||||
_ = x[SVMPF-173]
|
||||
_ = x[SVMPFT-174]
|
||||
_ = x[SYSCALL-175]
|
||||
_ = x[SYSEE-176]
|
||||
_ = x[TBM-177]
|
||||
_ = x[TDX_GUEST-178]
|
||||
_ = x[TLB_FLUSH_NESTED-179]
|
||||
_ = x[TME-180]
|
||||
_ = x[TOPEXT-181]
|
||||
_ = x[TSCRATEMSR-182]
|
||||
_ = x[TSXLDTRK-183]
|
||||
_ = x[VAES-184]
|
||||
_ = x[VMCBCLEAN-185]
|
||||
_ = x[VMPL-186]
|
||||
_ = x[VMSA_REGPROT-187]
|
||||
_ = x[VMX-188]
|
||||
_ = x[VPCLMULQDQ-189]
|
||||
_ = x[VTE-190]
|
||||
_ = x[WAITPKG-191]
|
||||
_ = x[WBNOINVD-192]
|
||||
_ = x[WRMSRNS-193]
|
||||
_ = x[X87-194]
|
||||
_ = x[XGETBV1-195]
|
||||
_ = x[XOP-196]
|
||||
_ = x[XSAVE-197]
|
||||
_ = x[XSAVEC-198]
|
||||
_ = x[XSAVEOPT-199]
|
||||
_ = x[XSAVES-200]
|
||||
_ = x[AESARM-201]
|
||||
_ = x[ARMCPUID-202]
|
||||
_ = x[ASIMD-203]
|
||||
_ = x[ASIMDDP-204]
|
||||
_ = x[ASIMDHP-205]
|
||||
_ = x[ASIMDRDM-206]
|
||||
_ = x[ATOMICS-207]
|
||||
_ = x[CRC32-208]
|
||||
_ = x[DCPOP-209]
|
||||
_ = x[EVTSTRM-210]
|
||||
_ = x[FCMA-211]
|
||||
_ = x[FHM-212]
|
||||
_ = x[FP-213]
|
||||
_ = x[FPHP-214]
|
||||
_ = x[GPA-215]
|
||||
_ = x[JSCVT-216]
|
||||
_ = x[LRCPC-217]
|
||||
_ = x[PMULL-218]
|
||||
_ = x[RNDR-219]
|
||||
_ = x[TLB-220]
|
||||
_ = x[TS-221]
|
||||
_ = x[SHA1-222]
|
||||
_ = x[SHA2-223]
|
||||
_ = x[SHA3-224]
|
||||
_ = x[SHA512-225]
|
||||
_ = x[SM3-226]
|
||||
_ = x[SM4-227]
|
||||
_ = x[SVE-228]
|
||||
_ = x[lastID-229]
|
||||
_ = x[SGXPQC-147]
|
||||
_ = x[SHA-148]
|
||||
_ = x[SME-149]
|
||||
_ = x[SME_COHERENT-150]
|
||||
_ = x[SM3_X86-151]
|
||||
_ = x[SM4_X86-152]
|
||||
_ = x[SPEC_CTRL_SSBD-153]
|
||||
_ = x[SRBDS_CTRL-154]
|
||||
_ = x[SRSO_MSR_FIX-155]
|
||||
_ = x[SRSO_NO-156]
|
||||
_ = x[SRSO_USER_KERNEL_NO-157]
|
||||
_ = x[SSE-158]
|
||||
_ = x[SSE2-159]
|
||||
_ = x[SSE3-160]
|
||||
_ = x[SSE4-161]
|
||||
_ = x[SSE42-162]
|
||||
_ = x[SSE4A-163]
|
||||
_ = x[SSSE3-164]
|
||||
_ = x[STIBP-165]
|
||||
_ = x[STIBP_ALWAYSON-166]
|
||||
_ = x[STOSB_SHORT-167]
|
||||
_ = x[SUCCOR-168]
|
||||
_ = x[SVM-169]
|
||||
_ = x[SVMDA-170]
|
||||
_ = x[SVMFBASID-171]
|
||||
_ = x[SVML-172]
|
||||
_ = x[SVMNP-173]
|
||||
_ = x[SVMPF-174]
|
||||
_ = x[SVMPFT-175]
|
||||
_ = x[SYSCALL-176]
|
||||
_ = x[SYSEE-177]
|
||||
_ = x[TBM-178]
|
||||
_ = x[TDX_GUEST-179]
|
||||
_ = x[TLB_FLUSH_NESTED-180]
|
||||
_ = x[TME-181]
|
||||
_ = x[TOPEXT-182]
|
||||
_ = x[TSA_L1_NO-183]
|
||||
_ = x[TSA_SQ_NO-184]
|
||||
_ = x[TSA_VERW_CLEAR-185]
|
||||
_ = x[TSCRATEMSR-186]
|
||||
_ = x[TSXLDTRK-187]
|
||||
_ = x[VAES-188]
|
||||
_ = x[VMCBCLEAN-189]
|
||||
_ = x[VMPL-190]
|
||||
_ = x[VMSA_REGPROT-191]
|
||||
_ = x[VMX-192]
|
||||
_ = x[VPCLMULQDQ-193]
|
||||
_ = x[VTE-194]
|
||||
_ = x[WAITPKG-195]
|
||||
_ = x[WBNOINVD-196]
|
||||
_ = x[WRMSRNS-197]
|
||||
_ = x[X87-198]
|
||||
_ = x[XGETBV1-199]
|
||||
_ = x[XOP-200]
|
||||
_ = x[XSAVE-201]
|
||||
_ = x[XSAVEC-202]
|
||||
_ = x[XSAVEOPT-203]
|
||||
_ = x[XSAVES-204]
|
||||
_ = x[AESARM-205]
|
||||
_ = x[ARMCPUID-206]
|
||||
_ = x[ASIMD-207]
|
||||
_ = x[ASIMDDP-208]
|
||||
_ = x[ASIMDHP-209]
|
||||
_ = x[ASIMDRDM-210]
|
||||
_ = x[ATOMICS-211]
|
||||
_ = x[CRC32-212]
|
||||
_ = x[DCPOP-213]
|
||||
_ = x[EVTSTRM-214]
|
||||
_ = x[FCMA-215]
|
||||
_ = x[FHM-216]
|
||||
_ = x[FP-217]
|
||||
_ = x[FPHP-218]
|
||||
_ = x[GPA-219]
|
||||
_ = x[JSCVT-220]
|
||||
_ = x[LRCPC-221]
|
||||
_ = x[PMULL-222]
|
||||
_ = x[RNDR-223]
|
||||
_ = x[TLB-224]
|
||||
_ = x[TS-225]
|
||||
_ = x[SHA1-226]
|
||||
_ = x[SHA2-227]
|
||||
_ = x[SHA3-228]
|
||||
_ = x[SHA512-229]
|
||||
_ = x[SM3-230]
|
||||
_ = x[SM4-231]
|
||||
_ = x[SVE-232]
|
||||
_ = x[PMU_FIXEDCOUNTER_CYCLES-233]
|
||||
_ = x[PMU_FIXEDCOUNTER_REFCYCLES-234]
|
||||
_ = x[PMU_FIXEDCOUNTER_INSTRUCTIONS-235]
|
||||
_ = x[PMU_FIXEDCOUNTER_TOPDOWN_SLOTS-236]
|
||||
_ = x[lastID-237]
|
||||
_ = x[firstID-0]
|
||||
}
|
||||
|
||||
const _FeatureID_name = "firstIDADXAESNIAMD3DNOWAMD3DNOWEXTAMXBF16AMXFP16AMXINT8AMXFP8AMXTILEAMXTF32AMXCOMPLEXAMXTRANSPOSEAPX_FAVXAVX10AVX10_128AVX10_256AVX10_512AVX2AVX512BF16AVX512BITALGAVX512BWAVX512CDAVX512DQAVX512ERAVX512FAVX512FP16AVX512IFMAAVX512PFAVX512VBMIAVX512VBMI2AVX512VLAVX512VNNIAVX512VP2INTERSECTAVX512VPOPCNTDQAVXIFMAAVXNECONVERTAVXSLOWAVXVNNIAVXVNNIINT8AVXVNNIINT16BHI_CTRLBMI1BMI2CETIBTCETSSCLDEMOTECLMULCLZEROCMOVCMPCCXADDCMPSB_SCADBS_SHORTCMPXCHG8CPBOOSTCPPCCX16EFER_LMSLE_UNSENQCMDERMSF16CFLUSH_L1DFMA3FMA4FP128FP256FSRMFXSRFXSROPTGFNIHLEHRESETHTTHWAHYBRID_CPUHYPERVISORIA32_ARCH_CAPIA32_CORE_CAPIBPBIBPB_BRTYPEIBRSIBRS_PREFERREDIBRS_PROVIDES_SMPIBSIBSBRNTRGTIBSFETCHSAMIBSFFVIBSOPCNTIBSOPCNTEXTIBSOPSAMIBSRDWROPCNTIBSRIPINVALIDCHKIBS_FETCH_CTLXIBS_OPDATA4IBS_OPFUSEIBS_PREVENTHOSTIBS_ZEN4IDPRED_CTRLINT_WBINVDINVLPGBKEYLOCKERKEYLOCKERWLAHFLAMLBRVIRTLZCNTMCAOVERFLOWMCDT_NOMCOMMITMD_CLEARMMXMMXEXTMOVBEMOVDIR64BMOVDIRIMOVSB_ZLMOVUMPXMSRIRCMSRLISTMSR_PAGEFLUSHNRIPSNXOSXSAVEPCONFIGPOPCNTPPINPREFETCHIPSFDRDPRURDRANDRDSEEDRDTSCPRRSBA_CTRLRTMRTM_ALWAYS_ABORTSBPBSERIALIZESEVSEV_64BITSEV_ALTERNATIVESEV_DEBUGSWAPSEV_ESSEV_RESTRICTEDSEV_SNPSGXSGXLCSHASMESME_COHERENTSM3_X86SM4_X86SPEC_CTRL_SSBDSRBDS_CTRLSRSO_MSR_FIXSRSO_NOSRSO_USER_KERNEL_NOSSESSE2SSE3SSE4SSE42SSE4ASSSE3STIBPSTIBP_ALWAYSONSTOSB_SHORTSUCCORSVMSVMDASVMFBASIDSVMLSVMNPSVMPFSVMPFTSYSCALLSYSEETBMTDX_GUESTTLB_FLUSH_NESTEDTMETOPEXTTSCRATEMSRTSXLDTRKVAESVMCBCLEANVMPLVMSA_REGPROTVMXVPCLMULQDQVTEWAITPKGWBNOINVDWRMSRNSX87XGETBV1XOPXSAVEXSAVECXSAVEOPTXSAVESAESARMARMCPUIDASIMDASIMDDPASIMDHPASIMDRDMATOMICSCRC32DCPOPEVTSTRMFCMAFHMFPFPHPGPAJSCVTLRCPCPMULLRNDRTLBTSSHA1SHA2SHA3SHA512SM3SM4SVElastID"
|
||||
const _FeatureID_name = "firstIDADXAESNIAMD3DNOWAMD3DNOWEXTAMXBF16AMXFP16AMXINT8AMXFP8AMXTILEAMXTF32AMXCOMPLEXAMXTRANSPOSEAPX_FAVXAVX10AVX10_128AVX10_256AVX10_512AVX2AVX512BF16AVX512BITALGAVX512BWAVX512CDAVX512DQAVX512ERAVX512FAVX512FP16AVX512IFMAAVX512PFAVX512VBMIAVX512VBMI2AVX512VLAVX512VNNIAVX512VP2INTERSECTAVX512VPOPCNTDQAVXIFMAAVXNECONVERTAVXSLOWAVXVNNIAVXVNNIINT8AVXVNNIINT16BHI_CTRLBMI1BMI2CETIBTCETSSCLDEMOTECLMULCLZEROCMOVCMPCCXADDCMPSB_SCADBS_SHORTCMPXCHG8CPBOOSTCPPCCX16EFER_LMSLE_UNSENQCMDERMSF16CFLUSH_L1DFMA3FMA4FP128FP256FSRMFXSRFXSROPTGFNIHLEHRESETHTTHWAHYBRID_CPUHYPERVISORIA32_ARCH_CAPIA32_CORE_CAPIBPBIBPB_BRTYPEIBRSIBRS_PREFERREDIBRS_PROVIDES_SMPIBSIBSBRNTRGTIBSFETCHSAMIBSFFVIBSOPCNTIBSOPCNTEXTIBSOPSAMIBSRDWROPCNTIBSRIPINVALIDCHKIBS_FETCH_CTLXIBS_OPDATA4IBS_OPFUSEIBS_PREVENTHOSTIBS_ZEN4IDPRED_CTRLINT_WBINVDINVLPGBKEYLOCKERKEYLOCKERWLAHFLAMLBRVIRTLZCNTMCAOVERFLOWMCDT_NOMCOMMITMD_CLEARMMXMMXEXTMOVBEMOVDIR64BMOVDIRIMOVSB_ZLMOVUMPXMSRIRCMSRLISTMSR_PAGEFLUSHNRIPSNXOSXSAVEPCONFIGPOPCNTPPINPREFETCHIPSFDRDPRURDRANDRDSEEDRDTSCPRRSBA_CTRLRTMRTM_ALWAYS_ABORTSBPBSERIALIZESEVSEV_64BITSEV_ALTERNATIVESEV_DEBUGSWAPSEV_ESSEV_RESTRICTEDSEV_SNPSGXSGXLCSGXPQCSHASMESME_COHERENTSM3_X86SM4_X86SPEC_CTRL_SSBDSRBDS_CTRLSRSO_MSR_FIXSRSO_NOSRSO_USER_KERNEL_NOSSESSE2SSE3SSE4SSE42SSE4ASSSE3STIBPSTIBP_ALWAYSONSTOSB_SHORTSUCCORSVMSVMDASVMFBASIDSVMLSVMNPSVMPFSVMPFTSYSCALLSYSEETBMTDX_GUESTTLB_FLUSH_NESTEDTMETOPEXTTSA_L1_NOTSA_SQ_NOTSA_VERW_CLEARTSCRATEMSRTSXLDTRKVAESVMCBCLEANVMPLVMSA_REGPROTVMXVPCLMULQDQVTEWAITPKGWBNOINVDWRMSRNSX87XGETBV1XOPXSAVEXSAVECXSAVEOPTXSAVESAESARMARMCPUIDASIMDASIMDDPASIMDHPASIMDRDMATOMICSCRC32DCPOPEVTSTRMFCMAFHMFPFPHPGPAJSCVTLRCPCPMULLRNDRTLBTSSHA1SHA2SHA3SHA512SM3SM4SVEPMU_FIXEDCOUNTER_CYCLESPMU_FIXEDCOUNTER_REFCYCLESPMU_FIXEDCOUNTER_INSTRUCTIONSPMU_FIXEDCOUNTER_TOPDOWN_SLOTSlastID"
|
||||
|
||||
var _FeatureID_index = [...]uint16{0, 7, 10, 15, 23, 34, 41, 48, 55, 61, 68, 75, 85, 97, 102, 105, 110, 119, 128, 137, 141, 151, 163, 171, 179, 187, 195, 202, 212, 222, 230, 240, 251, 259, 269, 287, 302, 309, 321, 328, 335, 346, 358, 366, 370, 374, 380, 385, 393, 398, 404, 408, 417, 435, 443, 450, 454, 458, 472, 478, 482, 486, 495, 499, 503, 508, 513, 517, 521, 528, 532, 535, 541, 544, 547, 557, 567, 580, 593, 597, 608, 612, 626, 643, 646, 656, 667, 673, 681, 692, 700, 712, 728, 742, 753, 763, 778, 786, 797, 807, 814, 823, 833, 837, 840, 847, 852, 863, 870, 877, 885, 888, 894, 899, 908, 915, 923, 927, 930, 936, 943, 956, 961, 963, 970, 977, 983, 987, 996, 1000, 1005, 1011, 1017, 1023, 1033, 1036, 1052, 1056, 1065, 1068, 1077, 1092, 1105, 1111, 1125, 1132, 1135, 1140, 1143, 1146, 1158, 1165, 1172, 1186, 1196, 1208, 1215, 1234, 1237, 1241, 1245, 1249, 1254, 1259, 1264, 1269, 1283, 1294, 1300, 1303, 1308, 1317, 1321, 1326, 1331, 1337, 1344, 1349, 1352, 1361, 1377, 1380, 1386, 1396, 1404, 1408, 1417, 1421, 1433, 1436, 1446, 1449, 1456, 1464, 1471, 1474, 1481, 1484, 1489, 1495, 1503, 1509, 1515, 1523, 1528, 1535, 1542, 1550, 1557, 1562, 1567, 1574, 1578, 1581, 1583, 1587, 1590, 1595, 1600, 1605, 1609, 1612, 1614, 1618, 1622, 1626, 1632, 1635, 1638, 1641, 1647}
|
||||
var _FeatureID_index = [...]uint16{0, 7, 10, 15, 23, 34, 41, 48, 55, 61, 68, 75, 85, 97, 102, 105, 110, 119, 128, 137, 141, 151, 163, 171, 179, 187, 195, 202, 212, 222, 230, 240, 251, 259, 269, 287, 302, 309, 321, 328, 335, 346, 358, 366, 370, 374, 380, 385, 393, 398, 404, 408, 417, 435, 443, 450, 454, 458, 472, 478, 482, 486, 495, 499, 503, 508, 513, 517, 521, 528, 532, 535, 541, 544, 547, 557, 567, 580, 593, 597, 608, 612, 626, 643, 646, 656, 667, 673, 681, 692, 700, 712, 728, 742, 753, 763, 778, 786, 797, 807, 814, 823, 833, 837, 840, 847, 852, 863, 870, 877, 885, 888, 894, 899, 908, 915, 923, 927, 930, 936, 943, 956, 961, 963, 970, 977, 983, 987, 996, 1000, 1005, 1011, 1017, 1023, 1033, 1036, 1052, 1056, 1065, 1068, 1077, 1092, 1105, 1111, 1125, 1132, 1135, 1140, 1146, 1149, 1152, 1164, 1171, 1178, 1192, 1202, 1214, 1221, 1240, 1243, 1247, 1251, 1255, 1260, 1265, 1270, 1275, 1289, 1300, 1306, 1309, 1314, 1323, 1327, 1332, 1337, 1343, 1350, 1355, 1358, 1367, 1383, 1386, 1392, 1401, 1410, 1424, 1434, 1442, 1446, 1455, 1459, 1471, 1474, 1484, 1487, 1494, 1502, 1509, 1512, 1519, 1522, 1527, 1533, 1541, 1547, 1553, 1561, 1566, 1573, 1580, 1588, 1595, 1600, 1605, 1612, 1616, 1619, 1621, 1625, 1628, 1633, 1638, 1643, 1647, 1650, 1652, 1656, 1660, 1664, 1670, 1673, 1676, 1679, 1702, 1728, 1757, 1787, 1793}
|
||||
|
||||
func (i FeatureID) String() string {
|
||||
if i < 0 || i >= FeatureID(len(_FeatureID_index)-1) {
|
||||
|
||||
Generated
Vendored
+21
-14
@@ -414,38 +414,45 @@ func SpaceEnabled(r *provider.UpdateStorageSpaceResponse, req *provider.UpdateSt
|
||||
}
|
||||
|
||||
// SpaceShared converts the response to an event
|
||||
// func SpaceShared(req *provider.AddGrantRequest, executant, sharer *user.UserId, grantee *provider.Grantee) events.SpaceShared {
|
||||
func SpaceShared(r *provider.AddGrantResponse, req *provider.AddGrantRequest, executant *user.User) events.SpaceShared {
|
||||
id := storagespace.FormatStorageID(req.Ref.ResourceId.StorageId, req.Ref.ResourceId.SpaceId)
|
||||
func SpaceShared(r *collaboration.CreateShareResponse, executant *user.User) events.SpaceShared {
|
||||
id := storagespace.FormatStorageID(r.GetShare().GetResourceId().GetStorageId(), r.GetShare().GetResourceId().GetSpaceId())
|
||||
return events.SpaceShared{
|
||||
Executant: executant.GetId(),
|
||||
Creator: req.Grant.Creator,
|
||||
GranteeUserID: req.Grant.GetGrantee().GetUserId(),
|
||||
GranteeGroupID: req.Grant.GetGrantee().GetGroupId(),
|
||||
Creator: r.Share.GetCreator(),
|
||||
GranteeUserID: r.Share.GetGrantee().GetUserId(),
|
||||
GranteeGroupID: r.Share.GetGrantee().GetGroupId(),
|
||||
ID: &provider.StorageSpaceId{OpaqueId: id},
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// SpaceShareUpdated converts the response to an events
|
||||
func SpaceShareUpdated(r *provider.UpdateGrantResponse, req *provider.UpdateGrantRequest, executant *user.User) events.SpaceShareUpdated {
|
||||
id := storagespace.FormatStorageID(req.Ref.ResourceId.StorageId, req.Ref.ResourceId.SpaceId)
|
||||
func SpaceShareUpdated(r *collaboration.UpdateShareResponse, executant *user.User) events.SpaceShareUpdated {
|
||||
id := storagespace.FormatStorageID(r.GetShare().GetResourceId().GetStorageId(), r.GetShare().GetResourceId().GetSpaceId())
|
||||
return events.SpaceShareUpdated{
|
||||
Executant: executant.GetId(),
|
||||
GranteeUserID: req.Grant.GetGrantee().GetUserId(),
|
||||
GranteeGroupID: req.Grant.GetGrantee().GetGroupId(),
|
||||
GranteeUserID: r.GetShare().GetGrantee().GetUserId(),
|
||||
GranteeGroupID: r.GetShare().GetGrantee().GetGroupId(),
|
||||
ID: &provider.StorageSpaceId{OpaqueId: id},
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// SpaceUnshared converts the response to an event
|
||||
func SpaceUnshared(r *provider.RemoveGrantResponse, req *provider.RemoveGrantRequest, executant *user.User) events.SpaceUnshared {
|
||||
id := storagespace.FormatStorageID(req.Ref.ResourceId.StorageId, req.Ref.ResourceId.SpaceId)
|
||||
func SpaceUnshared(r *collaboration.RemoveShareResponse, req *collaboration.RemoveShareRequest, executant *user.User) events.SpaceUnshared {
|
||||
var (
|
||||
userid *user.UserId
|
||||
groupid *group.GroupId
|
||||
rid *provider.ResourceId
|
||||
)
|
||||
_ = utils.ReadJSONFromOpaque(r.Opaque, "granteeuserid", &userid)
|
||||
_ = utils.ReadJSONFromOpaque(r.Opaque, "granteegroupid", &groupid)
|
||||
_ = utils.ReadJSONFromOpaque(r.Opaque, "resourceid", &rid)
|
||||
id := storagespace.FormatStorageID(rid.StorageId, rid.SpaceId)
|
||||
return events.SpaceUnshared{
|
||||
Executant: executant.GetId(),
|
||||
GranteeUserID: req.Grant.GetGrantee().GetUserId(),
|
||||
GranteeGroupID: req.Grant.GetGrantee().GetGroupId(),
|
||||
GranteeUserID: userid,
|
||||
GranteeGroupID: groupid,
|
||||
ID: &provider.StorageSpaceId{OpaqueId: id},
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
Generated
Vendored
+18
-21
@@ -81,15 +81,30 @@ func NewUnary(m map[string]interface{}) (grpc.UnaryServerInterceptor, int, error
|
||||
switch v := res.(type) {
|
||||
case *collaboration.CreateShareResponse:
|
||||
if isSuccess(v) {
|
||||
ev = ShareCreated(v, executant)
|
||||
if utils.ExistsInOpaque(v.Opaque, "noevent") {
|
||||
break
|
||||
}
|
||||
if utils.ExistsInOpaque(v.Opaque, "spacegrant") {
|
||||
ev = SpaceShared(v, executant)
|
||||
} else {
|
||||
ev = ShareCreated(v, executant)
|
||||
}
|
||||
}
|
||||
case *collaboration.RemoveShareResponse:
|
||||
if isSuccess(v) {
|
||||
ev = ShareRemoved(v, req.(*collaboration.RemoveShareRequest), executant)
|
||||
if utils.ExistsInOpaque(v.Opaque, "spacegrant") {
|
||||
ev = SpaceUnshared(v, req.(*collaboration.RemoveShareRequest), executant)
|
||||
} else {
|
||||
ev = ShareRemoved(v, req.(*collaboration.RemoveShareRequest), executant)
|
||||
}
|
||||
}
|
||||
case *collaboration.UpdateShareResponse:
|
||||
if isSuccess(v) {
|
||||
ev = ShareUpdated(v, req.(*collaboration.UpdateShareRequest), executant)
|
||||
if utils.ExistsInOpaque(v.Opaque, "spacegrant") {
|
||||
ev = SpaceShareUpdated(v, executant)
|
||||
} else {
|
||||
ev = ShareUpdated(v, req.(*collaboration.UpdateShareRequest), executant)
|
||||
}
|
||||
}
|
||||
case *collaboration.UpdateReceivedShareResponse:
|
||||
if isSuccess(v) {
|
||||
@@ -117,24 +132,6 @@ func NewUnary(m map[string]interface{}) (grpc.UnaryServerInterceptor, int, error
|
||||
if isSuccess(v) {
|
||||
ev = OCMCoreShareCreated(v, req.(*ocmcore.CreateOCMCoreShareRequest), executant)
|
||||
}
|
||||
case *provider.AddGrantResponse:
|
||||
// TODO: update CS3 APIs
|
||||
// FIXME these should be part of the RemoveGrantRequest object
|
||||
// https://github.com/owncloud/ocis/issues/4312
|
||||
r := req.(*provider.AddGrantRequest)
|
||||
if isSuccess(v) && utils.ExistsInOpaque(r.Opaque, "spacegrant") {
|
||||
ev = SpaceShared(v, r, executant)
|
||||
}
|
||||
case *provider.UpdateGrantResponse:
|
||||
r := req.(*provider.UpdateGrantRequest)
|
||||
if isSuccess(v) && utils.ExistsInOpaque(r.Opaque, "spacegrant") {
|
||||
ev = SpaceShareUpdated(v, r, executant)
|
||||
}
|
||||
case *provider.RemoveGrantResponse:
|
||||
r := req.(*provider.RemoveGrantRequest)
|
||||
if isSuccess(v) && utils.ExistsInOpaque(r.Opaque, "spacegrant") {
|
||||
ev = SpaceUnshared(v, req.(*provider.RemoveGrantRequest), executant)
|
||||
}
|
||||
case *provider.CreateContainerResponse:
|
||||
if isSuccess(v) {
|
||||
ev = ContainerCreated(v, req.(*provider.CreateContainerRequest), ownerID, executant)
|
||||
|
||||
-7
@@ -36,12 +36,6 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
_spaceTypePersonal = "personal"
|
||||
_spaceTypeProject = "project"
|
||||
_spaceTypeVirtual = "virtual"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rgrpc.Register("gateway", New)
|
||||
}
|
||||
@@ -77,7 +71,6 @@ type config struct {
|
||||
AllowedUserAgents map[string][]string `mapstructure:"allowed_user_agents"` // map[path][]user-agent
|
||||
CreatePersonalSpaceCacheConfig cache.Config `mapstructure:"create_personal_space_cache_config"`
|
||||
ProviderCacheConfig cache.Config `mapstructure:"provider_cache_config"`
|
||||
UseCommonSpaceRootShareLogic bool `mapstructure:"use_common_space_root_share_logic"`
|
||||
}
|
||||
|
||||
// sets defaults
|
||||
|
||||
Generated
Vendored
+48
@@ -36,6 +36,8 @@ import (
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
|
||||
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/grpc/codes"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@@ -226,6 +228,52 @@ func (s *svc) CreateStorageSpace(ctx context.Context, req *provider.CreateStorag
|
||||
}, nil
|
||||
}
|
||||
|
||||
// If the create space is not a "personal" space, we add a Share to the ShareProvider to give the creator
|
||||
// of the space "manager" access to it.
|
||||
// Note: the DecomposedFS storagadriver already adds a grant to the space root giving that access, the "CreateShare"
|
||||
// here is happeing so that that grant is also visible when listing permission (shares) on the spaceroot.
|
||||
if req.GetType() != "personal" && createRes.GetStatus().GetCode() == rpc.Code_CODE_OK {
|
||||
rollbackFn := func() {
|
||||
dsRes, dsErr := s.DeleteStorageSpace(ctx, &provider.DeleteStorageSpaceRequest{
|
||||
Id: createRes.GetStorageSpace().GetId(),
|
||||
})
|
||||
if dsErr != nil || dsRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
log.Error().Err(dsErr).Interface("status", dsRes.GetStatus()).Interface("space_id", createRes.GetStorageSpace().GetId()).Msg("failed to delete space during rollback")
|
||||
}
|
||||
}
|
||||
|
||||
u := ctxpkg.ContextMustGetUser(ctx)
|
||||
shareReq := &collaborationv1beta1.CreateShareRequest{
|
||||
ResourceInfo: &provider.ResourceInfo{
|
||||
Id: createRes.GetStorageSpace().GetRoot(),
|
||||
},
|
||||
Grant: &collaborationv1beta1.ShareGrant{
|
||||
Grantee: &provider.Grantee{
|
||||
Type: provider.GranteeType_GRANTEE_TYPE_USER,
|
||||
Id: &provider.Grantee_UserId{
|
||||
UserId: u.GetId(),
|
||||
},
|
||||
},
|
||||
Permissions: &collaborationv1beta1.SharePermissions{
|
||||
Permissions: conversions.NewManagerRole().CS3ResourcePermissions(),
|
||||
},
|
||||
},
|
||||
}
|
||||
csResp, err := s.CreateShare(ctx, shareReq)
|
||||
if err != nil || csResp.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
log.Error().Err(err).Interface("status", csResp.GetStatus()).Interface("space_id", createRes.GetStorageSpace().GetId()).Msg("failed to create initial share for space creator, rolling back space creation")
|
||||
rollbackFn()
|
||||
|
||||
shareErrMsg := "failed to create initial share for space creator"
|
||||
if err == nil {
|
||||
shareErrMsg = csResp.GetStatus().GetMessage()
|
||||
}
|
||||
return &provider.CreateStorageSpaceResponse{
|
||||
Status: status.NewInternal(ctx, shareErrMsg),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return createRes, nil
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+22
-276
@@ -20,7 +20,6 @@ package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
@@ -39,25 +38,14 @@ import (
|
||||
|
||||
// TODO(labkode): add multi-phase commit logic when commit share or commit ref is enabled.
|
||||
func (s *svc) CreateShare(ctx context.Context, req *collaboration.CreateShareRequest) (*collaboration.CreateShareResponse, error) {
|
||||
// Don't use the share manager when sharing a space root
|
||||
if !s.c.UseCommonSpaceRootShareLogic && refIsSpaceRoot(req.ResourceInfo.Id) {
|
||||
return s.addSpaceShare(ctx, req)
|
||||
}
|
||||
return s.addShare(ctx, req)
|
||||
}
|
||||
|
||||
func (s *svc) RemoveShare(ctx context.Context, req *collaboration.RemoveShareRequest) (*collaboration.RemoveShareResponse, error) {
|
||||
key := req.GetRef().GetKey()
|
||||
if !s.c.UseCommonSpaceRootShareLogic && shareIsSpaceRoot(key) {
|
||||
return s.removeSpaceShare(ctx, key.GetResourceId(), key.GetGrantee())
|
||||
}
|
||||
return s.removeShare(ctx, req)
|
||||
}
|
||||
|
||||
func (s *svc) UpdateShare(ctx context.Context, req *collaboration.UpdateShareRequest) (*collaboration.UpdateShareResponse, error) {
|
||||
if !s.c.UseCommonSpaceRootShareLogic && refIsSpaceRoot(req.GetShare().GetResourceId()) {
|
||||
return s.updateSpaceShare(ctx, req)
|
||||
}
|
||||
return s.updateShare(ctx, req)
|
||||
}
|
||||
|
||||
@@ -150,7 +138,12 @@ func (s *svc) updateShare(ctx context.Context, req *collaboration.UpdateShareReq
|
||||
Expiration: res.GetShare().GetExpiration(),
|
||||
Creator: creator.GetId(),
|
||||
}
|
||||
updateGrantStatus, err := s.updateGrant(ctx, res.GetShare().GetResourceId(), grant, nil)
|
||||
var opaque *typesv1beta1.Opaque
|
||||
if refIsSpaceRoot(res.GetShare().GetResourceId()) {
|
||||
opaque = utils.SpaceGrantOpaque()
|
||||
utils.AppendPlainToOpaque(opaque, "spacetype", utils.ReadPlainFromOpaque(req.GetOpaque(), "spacetype"))
|
||||
}
|
||||
updateGrantStatus, err := s.updateGrant(ctx, res.GetShare().GetResourceId(), grant, opaque)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gateway: error calling updateGrant")
|
||||
@@ -167,87 +160,6 @@ func (s *svc) updateShare(ctx context.Context, req *collaboration.UpdateShareReq
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *svc) updateSpaceShare(ctx context.Context, req *collaboration.UpdateShareRequest) (*collaboration.UpdateShareResponse, error) {
|
||||
if req.GetShare().GetGrantee() == nil {
|
||||
return &collaboration.UpdateShareResponse{Status: status.NewInvalid(ctx, "updating requires a received grantee object")}, nil
|
||||
}
|
||||
// If the share is a denial we call denyGrant instead.
|
||||
var st *rpc.Status
|
||||
var err error
|
||||
// TODO: change CS3 APIs
|
||||
opaque := &typesv1beta1.Opaque{
|
||||
Map: map[string]*typesv1beta1.OpaqueEntry{
|
||||
"spacegrant": {},
|
||||
},
|
||||
}
|
||||
utils.AppendPlainToOpaque(opaque, "spacetype", utils.ReadPlainFromOpaque(req.Opaque, "spacetype"))
|
||||
|
||||
if grants.PermissionsEqual(req.Share.GetPermissions().GetPermissions(), &provider.ResourcePermissions{}) {
|
||||
st, err = s.denyGrant(ctx, req.GetShare().GetResourceId(), req.GetShare().GetGrantee(), opaque)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gateway: error denying grant in storage")
|
||||
}
|
||||
} else {
|
||||
listGrantRes, err := s.listGrants(ctx, req.GetShare().GetResourceId())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gateway: error getting grant to remove from storage")
|
||||
}
|
||||
existsGrant := s.getGranteeGrant(listGrantRes.GetGrants(), req.GetShare().GetGrantee())
|
||||
|
||||
if !slices.Contains(req.GetUpdateMask().GetPaths(), "permissions") {
|
||||
req.Share.Permissions = &collaboration.SharePermissions{Permissions: existsGrant.GetPermissions()}
|
||||
}
|
||||
|
||||
if !slices.Contains(req.GetUpdateMask().GetPaths(), "expiration") {
|
||||
req.Share.Expiration = existsGrant.GetExpiration()
|
||||
}
|
||||
|
||||
u, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
return nil, errors.New("user not found in context")
|
||||
}
|
||||
|
||||
grant := &provider.Grant{
|
||||
Grantee: req.GetShare().GetGrantee(),
|
||||
Permissions: req.GetShare().GetPermissions().GetPermissions(),
|
||||
Expiration: req.GetShare().GetExpiration(),
|
||||
Creator: u.GetId(),
|
||||
}
|
||||
|
||||
if grant.GetPermissions() == nil {
|
||||
return &collaboration.UpdateShareResponse{Status: status.NewInvalid(ctx, "updating requires a received permission object")}, nil
|
||||
}
|
||||
|
||||
if !grant.GetPermissions().GetRemoveGrant() {
|
||||
// this request might remove Manager Permissions so we need to
|
||||
// check if there is at least one manager remaining of the
|
||||
// resource.
|
||||
if !isSpaceManagerRemaining(listGrantRes.GetGrants(), grant.GetGrantee()) {
|
||||
return &collaboration.UpdateShareResponse{
|
||||
Status: status.NewPermissionDenied(ctx, errtypes.PermissionDenied(""), "can't remove the last manager"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
}
|
||||
st, err = s.updateGrant(ctx, req.GetShare().GetResourceId(), grant, opaque)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gateway: error adding grant to storage")
|
||||
}
|
||||
}
|
||||
|
||||
res := &collaboration.UpdateShareResponse{
|
||||
Status: st,
|
||||
Share: req.Share,
|
||||
}
|
||||
|
||||
if st.Code != rpc.Code_CODE_OK {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
s.providerCache.RemoveListStorageProviders(req.GetShare().GetResourceId())
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// TODO(labkode): listing received shares just goes to the user share manager and gets the list of
|
||||
// received shares. The display name of the shares should be the a friendly name, like the basename
|
||||
// of the original file.
|
||||
@@ -526,53 +438,6 @@ func (s *svc) removeGrant(ctx context.Context, id *provider.ResourceId, g *provi
|
||||
return status.NewOK(ctx), nil
|
||||
}
|
||||
|
||||
func (s *svc) listGrants(ctx context.Context, id *provider.ResourceId) (*provider.ListGrantsResponse, error) {
|
||||
ref := &provider.Reference{
|
||||
ResourceId: id,
|
||||
}
|
||||
|
||||
grantReq := &provider.ListGrantsRequest{
|
||||
Ref: ref,
|
||||
}
|
||||
|
||||
c, _, err := s.find(ctx, ref)
|
||||
if err != nil {
|
||||
appctx.GetLogger(ctx).
|
||||
Err(err).
|
||||
Interface("reference", ref).
|
||||
Msg("listGrants: failed to get storage provider")
|
||||
if _, ok := err.(errtypes.IsNotFound); ok {
|
||||
return &provider.ListGrantsResponse{
|
||||
Status: status.NewNotFound(ctx, "storage provider not found"),
|
||||
}, nil
|
||||
}
|
||||
return &provider.ListGrantsResponse{
|
||||
Status: status.NewInternal(ctx, "error finding storage provider"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
grantRes, err := c.ListGrants(ctx, grantReq)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gateway: error calling ListGrants")
|
||||
}
|
||||
if grantRes.Status.Code != rpc.Code_CODE_OK {
|
||||
return &provider.ListGrantsResponse{Status: status.NewInternal(ctx,
|
||||
"error listing storage grants"),
|
||||
},
|
||||
nil
|
||||
}
|
||||
return grantRes, nil
|
||||
}
|
||||
|
||||
func (s *svc) getGranteeGrant(grants []*provider.Grant, grantee *provider.Grantee) *provider.Grant {
|
||||
for _, g := range grants {
|
||||
if isEqualGrantee(g.Grantee, grantee) {
|
||||
return g
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *svc) addShare(ctx context.Context, req *collaboration.CreateShareRequest) (*collaboration.CreateShareResponse, error) {
|
||||
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
|
||||
if err != nil {
|
||||
@@ -613,13 +478,18 @@ func (s *svc) addShare(ctx context.Context, req *collaboration.CreateShareReques
|
||||
if s.c.CommitShareToStorageGrant {
|
||||
// If the share is a denial we call denyGrant instead.
|
||||
var status *rpc.Status
|
||||
var opaque *typesv1beta1.Opaque
|
||||
if refIsSpaceRoot(req.ResourceInfo.Id) {
|
||||
opaque = utils.SpaceGrantOpaque()
|
||||
utils.AppendPlainToOpaque(opaque, "spacetype", req.ResourceInfo.GetSpace().GetSpaceType())
|
||||
}
|
||||
if grants.PermissionsEqual(req.Grant.Permissions.Permissions, &provider.ResourcePermissions{}) {
|
||||
status, err = s.denyGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee, nil)
|
||||
status, err = s.denyGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee, opaque)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gateway: error denying grant in storage")
|
||||
}
|
||||
} else {
|
||||
status, err = s.addGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee, req.Grant.Permissions.Permissions, req.Grant.Expiration, nil)
|
||||
status, err = s.addGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee, req.Grant.Permissions.Permissions, req.Grant.Expiration, opaque)
|
||||
if err != nil {
|
||||
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg(err.Error())
|
||||
rollBackFn(status)
|
||||
@@ -628,7 +498,7 @@ func (s *svc) addShare(ctx context.Context, req *collaboration.CreateShareReques
|
||||
}
|
||||
|
||||
switch status.Code {
|
||||
case rpc.Code_CODE_OK:
|
||||
case rpc.Code_CODE_OK, rpc.Code_CODE_ALREADY_EXISTS:
|
||||
// ok
|
||||
case rpc.Code_CODE_UNIMPLEMENTED:
|
||||
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants not supported, ignoring")
|
||||
@@ -644,58 +514,6 @@ func (s *svc) addShare(ctx context.Context, req *collaboration.CreateShareReques
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *svc) addSpaceShare(ctx context.Context, req *collaboration.CreateShareRequest) (*collaboration.CreateShareResponse, error) {
|
||||
if refIsSpaceRoot(req.GetResourceInfo().GetId()) &&
|
||||
(req.GetResourceInfo().GetSpace().GetSpaceType() == _spaceTypePersonal || req.GetResourceInfo().GetSpace().GetSpaceType() == _spaceTypeVirtual) {
|
||||
return &collaboration.CreateShareResponse{Status: status.NewInvalid(ctx, "space type is not eligible for sharing")}, nil
|
||||
}
|
||||
// If the share is a denial we call denyGrant instead.
|
||||
var st *rpc.Status
|
||||
var err error
|
||||
// TODO: change CS3 APIs
|
||||
opaque := &typesv1beta1.Opaque{
|
||||
Map: map[string]*typesv1beta1.OpaqueEntry{
|
||||
"spacegrant": {},
|
||||
},
|
||||
}
|
||||
utils.AppendPlainToOpaque(
|
||||
opaque,
|
||||
"spacetype",
|
||||
req.ResourceInfo.GetSpace().GetSpaceType(),
|
||||
)
|
||||
if grants.PermissionsEqual(req.Grant.Permissions.Permissions, &provider.ResourcePermissions{}) {
|
||||
st, err = s.denyGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee, opaque)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gateway: error denying grant in storage")
|
||||
}
|
||||
} else {
|
||||
st, err = s.addGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee, req.Grant.Permissions.Permissions, req.Grant.Expiration, opaque)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gateway: error adding grant to storage")
|
||||
}
|
||||
}
|
||||
|
||||
switch st.Code {
|
||||
case rpc.Code_CODE_OK:
|
||||
s.providerCache.RemoveListStorageProviders(req.ResourceInfo.Id)
|
||||
case rpc.Code_CODE_UNIMPLEMENTED:
|
||||
appctx.GetLogger(ctx).Debug().Interface("status", st).Interface("req", req).Msg("storing grants not supported, ignoring")
|
||||
default:
|
||||
return &collaboration.CreateShareResponse{
|
||||
Status: st,
|
||||
}, err
|
||||
}
|
||||
|
||||
return &collaboration.CreateShareResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Share: &collaboration.Share{
|
||||
ResourceId: req.ResourceInfo.Id,
|
||||
Permissions: &collaboration.SharePermissions{Permissions: req.Grant.Permissions.GetPermissions()},
|
||||
Grantee: req.Grant.Grantee,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *svc) removeShare(ctx context.Context, req *collaboration.RemoveShareRequest) (*collaboration.RemoveShareResponse, error) {
|
||||
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
|
||||
if err != nil {
|
||||
@@ -742,9 +560,16 @@ func (s *svc) removeShare(ctx context.Context, req *collaboration.RemoveShareReq
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gateway: error calling RemoveShare")
|
||||
}
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
if s.c.CommitShareToStorageGrant {
|
||||
removeGrantStatus, err := s.removeGrant(ctx, share.ResourceId, share.Grantee, share.Permissions.Permissions, nil)
|
||||
var opaque *typesv1beta1.Opaque
|
||||
if refIsSpaceRoot(share.ResourceId) {
|
||||
opaque = utils.SpaceGrantOpaque()
|
||||
}
|
||||
removeGrantStatus, err := s.removeGrant(ctx, share.ResourceId, share.Grantee, share.Permissions.Permissions, opaque)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gateway: error removing grant from storage")
|
||||
}
|
||||
@@ -758,58 +583,6 @@ func (s *svc) removeShare(ctx context.Context, req *collaboration.RemoveShareReq
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *svc) removeSpaceShare(ctx context.Context, ref *provider.ResourceId, grantee *provider.Grantee) (*collaboration.RemoveShareResponse, error) {
|
||||
listGrantRes, err := s.listGrants(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gateway: error getting grant to remove from storage")
|
||||
}
|
||||
var permissions *provider.ResourcePermissions
|
||||
for _, g := range listGrantRes.Grants {
|
||||
if isEqualGrantee(g.Grantee, grantee) {
|
||||
permissions = g.Permissions
|
||||
}
|
||||
}
|
||||
if permissions == nil {
|
||||
return nil, errors.New("gateway: error getting grant to remove from storage")
|
||||
}
|
||||
|
||||
if len(listGrantRes.Grants) == 1 || !isSpaceManagerRemaining(listGrantRes.Grants, grantee) {
|
||||
return &collaboration.RemoveShareResponse{
|
||||
Status: status.NewPermissionDenied(ctx, errtypes.PermissionDenied(""), "can't remove the last manager"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO: change CS3 APIs
|
||||
opaque := &typesv1beta1.Opaque{
|
||||
Map: map[string]*typesv1beta1.OpaqueEntry{
|
||||
"spacegrant": {},
|
||||
},
|
||||
}
|
||||
removeGrantStatus, err := s.removeGrant(ctx, ref, grantee, permissions, opaque)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "gateway: error removing grant from storage")
|
||||
}
|
||||
if removeGrantStatus.Code != rpc.Code_CODE_OK {
|
||||
return &collaboration.RemoveShareResponse{
|
||||
Status: removeGrantStatus,
|
||||
}, err
|
||||
}
|
||||
s.providerCache.RemoveListStorageProviders(ref)
|
||||
return &collaboration.RemoveShareResponse{Status: status.NewOK(ctx)}, nil
|
||||
}
|
||||
|
||||
func isSpaceManagerRemaining(grants []*provider.Grant, grantee *provider.Grantee) bool {
|
||||
for _, g := range grants {
|
||||
// RemoveGrant is currently the way to check for the manager role
|
||||
// If it is not set than the current grant is not for a manager and
|
||||
// we can just continue with the next one.
|
||||
if g.Permissions.RemoveGrant && !isEqualGrantee(g.Grantee, grantee) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *svc) checkLock(ctx context.Context, shareId *collaboration.ShareId) (*rpc.Status, error) {
|
||||
logger := appctx.GetLogger(ctx)
|
||||
getShareRes, err := s.GetShare(ctx, &collaboration.GetShareRequest{
|
||||
@@ -869,30 +642,3 @@ func refIsSpaceRoot(ref *provider.ResourceId) bool {
|
||||
|
||||
return ref.SpaceId == ref.OpaqueId
|
||||
}
|
||||
|
||||
func shareIsSpaceRoot(key *collaboration.ShareKey) bool {
|
||||
if key == nil {
|
||||
return false
|
||||
}
|
||||
return refIsSpaceRoot(key.ResourceId)
|
||||
}
|
||||
|
||||
func isEqualGrantee(a, b *provider.Grantee) bool {
|
||||
// Ideally we would want to use utils.GranteeEqual()
|
||||
// but the grants stored in the decomposedfs aren't complete (missing usertype and idp)
|
||||
// because of that the check would fail so we can only check the ... for now.
|
||||
if a.Type != b.Type {
|
||||
return false
|
||||
}
|
||||
|
||||
var aID, bID string
|
||||
switch a.Type {
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
aID = a.GetGroupId().GetOpaqueId()
|
||||
bID = b.GetGroupId().GetOpaqueId()
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
aID = a.GetUserId().GetOpaqueId()
|
||||
bID = b.GetUserId().GetOpaqueId()
|
||||
}
|
||||
return aID == bID
|
||||
}
|
||||
|
||||
Generated
Vendored
+5
-12
@@ -46,6 +46,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
@@ -1093,12 +1094,8 @@ func (s *service) resolveAcceptedShare(ctx context.Context, ref *provider.Refere
|
||||
if ref.ResourceId.OpaqueId == utils.ShareStorageProviderID && ref.Path != "." {
|
||||
lsRes, err := sharingCollaborationClient.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{
|
||||
Filters: []*collaboration.Filter{
|
||||
{
|
||||
Type: collaboration.Filter_TYPE_STATE,
|
||||
Term: &collaboration.Filter_State{
|
||||
State: collaboration.ShareState_SHARE_STATE_ACCEPTED,
|
||||
},
|
||||
},
|
||||
share.StateFilter(collaboration.ShareState_SHARE_STATE_ACCEPTED),
|
||||
share.SpaceRootFilter(false),
|
||||
// TODO filter by mountpoint?
|
||||
},
|
||||
})
|
||||
@@ -1179,12 +1176,8 @@ func (s *service) fetchAcceptedShares(ctx context.Context, opaque *typesv1beta1.
|
||||
|
||||
lsRes, err := sharingCollaborationClient.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{
|
||||
Filters: []*collaboration.Filter{
|
||||
{
|
||||
Type: collaboration.Filter_TYPE_STATE,
|
||||
Term: &collaboration.Filter_State{
|
||||
State: collaboration.ShareState_SHARE_STATE_ACCEPTED,
|
||||
},
|
||||
},
|
||||
share.StateFilter(collaboration.ShareState_SHARE_STATE_ACCEPTED),
|
||||
share.SpaceRootFilter(false),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/usershareprovider/usershareprovider.go
Generated
Vendored
+158
-26
@@ -27,10 +27,12 @@ import (
|
||||
"strings"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -54,6 +56,9 @@ const (
|
||||
_fieldMaskPathMountPoint = "mount_point"
|
||||
_fieldMaskPathPermissions = "permissions"
|
||||
_fieldMaskPathState = "state"
|
||||
_spaceTypePersonal = "personal"
|
||||
_spaceTypeProject = "project"
|
||||
_spaceTypeVirtual = "virtual"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -79,9 +84,9 @@ type service struct {
|
||||
allowedPathsForShares []*regexp.Regexp
|
||||
}
|
||||
|
||||
func getShareManager(c *config) (share.Manager, error) {
|
||||
func getShareManager(c *config, logger *zerolog.Logger) (share.Manager, error) {
|
||||
if f, ok := registry.NewFuncs[c.Driver]; ok {
|
||||
return f(c.Drivers[c.Driver])
|
||||
return f(c.Drivers[c.Driver], logger)
|
||||
}
|
||||
return nil, errtypes.NotFound("driver not found: " + c.Driver)
|
||||
}
|
||||
@@ -109,7 +114,7 @@ func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
}
|
||||
|
||||
// New creates a new user share provider svc initialized from defaults
|
||||
func NewDefault(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
|
||||
func NewDefault(m map[string]any, ss *grpc.Server, logger *zerolog.Logger) (rgrpc.Service, error) {
|
||||
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
@@ -118,7 +123,7 @@ func NewDefault(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (r
|
||||
|
||||
c.init()
|
||||
|
||||
sm, err := getShareManager(c)
|
||||
sm, err := getShareManager(c, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -166,13 +171,6 @@ func (s *service) isPathAllowed(path string) bool {
|
||||
func (s *service) CreateShare(ctx context.Context, req *collaboration.CreateShareRequest) (*collaboration.CreateShareResponse, error) {
|
||||
log := appctx.GetLogger(ctx)
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
// Grants must not allow grant permissions
|
||||
if HasGrantPermissions(req.GetGrant().GetPermissions().GetPermissions()) {
|
||||
return &collaboration.CreateShareResponse{
|
||||
Status: status.NewInvalidArg(ctx, "resharing not supported"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// check if the grantee is a user or group
|
||||
if req.GetGrant().GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_USER {
|
||||
// check if the tenantId of the user matches the tenantId of the target user
|
||||
@@ -202,8 +200,8 @@ func (s *service) CreateShare(ctx context.Context, req *collaboration.CreateShar
|
||||
}, nil
|
||||
}
|
||||
|
||||
// use logged in user Idp as default, if the Grantee does not have an IDP set.
|
||||
if req.GetGrant().GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_USER && req.GetGrant().GetGrantee().GetUserId().GetIdp() == "" {
|
||||
// use logged in user Idp as default.
|
||||
req.GetGrant().GetGrantee().Id = &provider.Grantee_UserId{
|
||||
UserId: &userpb.UserId{
|
||||
OpaqueId: req.GetGrant().GetGrantee().GetUserId().GetOpaqueId(),
|
||||
@@ -211,6 +209,16 @@ func (s *service) CreateShare(ctx context.Context, req *collaboration.CreateShar
|
||||
Type: userpb.UserType_USER_TYPE_PRIMARY},
|
||||
}
|
||||
}
|
||||
// some for group grantees
|
||||
if req.GetGrant().GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_GROUP && req.GetGrant().GetGrantee().GetGroupId().GetIdp() == "" {
|
||||
// use logged in user Idp as default.
|
||||
req.GetGrant().GetGrantee().Id = &provider.Grantee_GroupId{
|
||||
GroupId: &grouppb.GroupId{
|
||||
OpaqueId: req.GetGrant().GetGrantee().GetGroupId().GetOpaqueId(),
|
||||
Idp: user.GetId().GetIdp(),
|
||||
Type: grouppb.GroupType_GROUP_TYPE_REGULAR},
|
||||
}
|
||||
}
|
||||
|
||||
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: req.GetResourceInfo().GetId()}})
|
||||
if err != nil {
|
||||
@@ -225,6 +233,55 @@ func (s *service) CreateShare(ctx context.Context, req *collaboration.CreateShar
|
||||
Status: status.NewPermissionDenied(ctx, nil, "no permission to add grants on shared resource"),
|
||||
}, err
|
||||
}
|
||||
|
||||
isSpaceRoot := utils.IsSpaceRoot(sRes.GetInfo())
|
||||
var noEvent bool
|
||||
|
||||
if isSpaceRoot {
|
||||
if sRes.GetInfo().GetSpace().GetSpaceType() == _spaceTypePersonal || sRes.GetInfo().GetSpace().GetSpaceType() == _spaceTypeVirtual {
|
||||
return &collaboration.CreateShareResponse{Status: status.NewInvalid(ctx, "space type is not eligible for sharing")}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// do not allow share to myself or the owner if share is for a user
|
||||
if req.GetGrant().GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_USER &&
|
||||
(utils.UserEqual(req.GetGrant().GetGrantee().GetUserId(), user.Id) || utils.UserEqual(req.GetGrant().GetGrantee().GetUserId(), sRes.GetInfo().GetOwner())) {
|
||||
|
||||
denySelfShare := true
|
||||
// To allow adding the initial "mananger" share for the creator of a space when need to make an exception here
|
||||
// if the shared resource is a space root, that does not have any Share existin yet. Note, that we have already
|
||||
// verified that the user adding the share does really have "AddGrant" permissions on the affected resource, so
|
||||
// this "hack" should be ok.
|
||||
if isSpaceRoot {
|
||||
shares, err := s.sm.ListShares(
|
||||
ctx,
|
||||
[]*collaboration.Filter{share.ResourceIDFilter(req.GetResourceInfo().GetId())},
|
||||
)
|
||||
if err != nil {
|
||||
return &collaboration.CreateShareResponse{
|
||||
Status: status.NewInternal(ctx, "failed to list existing shares"),
|
||||
}, nil
|
||||
}
|
||||
if len(shares) == 0 {
|
||||
denySelfShare = false
|
||||
noEvent = true
|
||||
}
|
||||
}
|
||||
if denySelfShare {
|
||||
err := errtypes.BadRequest("jsoncs3: owner/creator and grantee are the same")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// resharing is forbidden for not space roots
|
||||
if !isSpaceRoot {
|
||||
// Resharing of Files/Directories is forbidden. So the grants must not allow the "grant" permissions
|
||||
if HasGrantPermissions(req.GetGrant().GetPermissions().GetPermissions()) {
|
||||
return &collaboration.CreateShareResponse{
|
||||
Status: status.NewInvalidArg(ctx, "resharing not supported"),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
// check if the share creator has sufficient permissions to do so.
|
||||
if shareCreationAllowed := conversions.SufficientCS3Permissions(
|
||||
sRes.GetInfo().GetPermissionSet(),
|
||||
@@ -256,10 +313,22 @@ func (s *service) CreateShare(ctx context.Context, req *collaboration.CreateShar
|
||||
}, nil
|
||||
}
|
||||
|
||||
var opaque *typesv1beta1.Opaque
|
||||
if isSpaceRoot {
|
||||
opaque = utils.SpaceGrantOpaque()
|
||||
}
|
||||
|
||||
if noEvent {
|
||||
opaque = &typesv1beta1.Opaque{
|
||||
Map: map[string]*typesv1beta1.OpaqueEntry{
|
||||
"noevent": {},
|
||||
},
|
||||
}
|
||||
}
|
||||
return &collaboration.CreateShareResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Share: createdShare,
|
||||
Opaque: utils.AppendPlainToOpaque(nil, "resourcename", sRes.GetInfo().GetName()),
|
||||
Opaque: utils.AppendPlainToOpaque(opaque, "resourcename", sRes.GetInfo().GetName()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -267,6 +336,30 @@ func HasGrantPermissions(p *provider.ResourcePermissions) bool {
|
||||
return p.GetAddGrant() || p.GetUpdateGrant() || p.GetRemoveGrant() || p.GetDenyGrant()
|
||||
}
|
||||
|
||||
// spaceRootHasRemainingManager returns true if the given resource has at least one share
|
||||
// (excluding the share with excludeShareOpaqueID) that grants both AddGrant and RemoveGrant.
|
||||
func (s *service) spaceRootHasRemainingManager(ctx context.Context, resourceID *provider.ResourceId, excludeShareOpaqueID string) (bool, error) {
|
||||
shares, err := s.sm.ListShares(ctx, []*collaboration.Filter{
|
||||
{
|
||||
Type: collaboration.Filter_TYPE_RESOURCE_ID,
|
||||
Term: &collaboration.Filter_ResourceId{ResourceId: resourceID},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, s := range shares {
|
||||
if s.GetId().GetOpaqueId() == excludeShareOpaqueID {
|
||||
continue
|
||||
}
|
||||
perms := s.GetPermissions().GetPermissions()
|
||||
if perms.GetAddGrant() && perms.GetRemoveGrant() {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *service) RemoveShare(ctx context.Context, req *collaboration.RemoveShareRequest) (*collaboration.RemoveShareResponse, error) {
|
||||
log := appctx.GetLogger(ctx)
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
@@ -300,6 +393,20 @@ func (s *service) RemoveShare(ctx context.Context, req *collaboration.RemoveShar
|
||||
}, err
|
||||
}
|
||||
|
||||
// For space root shares, ensure at least one share with both AddGrant and RemoveGrant will remain
|
||||
// so the space always has a manager who can administer grants.
|
||||
if utils.IsSpaceRoot(sRes.GetInfo()) {
|
||||
if ok, err := s.spaceRootHasRemainingManager(ctx, share.GetResourceId(), share.GetId().GetOpaqueId()); err != nil {
|
||||
return &collaboration.RemoveShareResponse{
|
||||
Status: status.NewInternal(ctx, "failed to list shares for space root"),
|
||||
}, nil
|
||||
} else if !ok {
|
||||
return &collaboration.RemoveShareResponse{
|
||||
Status: status.NewPermissionDenied(ctx, nil, "cannot remove the last share with manager permissions on a space root"),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
err = s.sm.Unshare(ctx, req.Ref)
|
||||
if err != nil {
|
||||
return &collaboration.RemoveShareResponse{
|
||||
@@ -307,16 +414,20 @@ func (s *service) RemoveShare(ctx context.Context, req *collaboration.RemoveShar
|
||||
}, nil
|
||||
}
|
||||
|
||||
o := utils.AppendJSONToOpaque(nil, "resourceid", share.GetResourceId())
|
||||
o = utils.AppendPlainToOpaque(o, "resourcename", sRes.GetInfo().GetName())
|
||||
var opaque *typesv1beta1.Opaque
|
||||
if utils.IsSpaceRoot(sRes.GetInfo()) {
|
||||
opaque = utils.SpaceGrantOpaque()
|
||||
}
|
||||
opaque = utils.AppendJSONToOpaque(opaque, "resourceid", share.GetResourceId())
|
||||
opaque = utils.AppendPlainToOpaque(opaque, "resourcename", sRes.GetInfo().GetName())
|
||||
if user := share.GetGrantee().GetUserId(); user != nil {
|
||||
o = utils.AppendJSONToOpaque(o, "granteeuserid", user)
|
||||
opaque = utils.AppendJSONToOpaque(opaque, "granteeuserid", user)
|
||||
} else {
|
||||
o = utils.AppendJSONToOpaque(o, "granteegroupid", share.GetGrantee().GetGroupId())
|
||||
opaque = utils.AppendJSONToOpaque(opaque, "granteegroupid", share.GetGrantee().GetGroupId())
|
||||
}
|
||||
|
||||
return &collaboration.RemoveShareResponse{
|
||||
Opaque: o,
|
||||
Opaque: opaque,
|
||||
Status: status.NewOK(ctx),
|
||||
}, nil
|
||||
}
|
||||
@@ -361,13 +472,6 @@ func (s *service) UpdateShare(ctx context.Context, req *collaboration.UpdateShar
|
||||
log := appctx.GetLogger(ctx)
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
// Grants must not allow grant permissions
|
||||
if HasGrantPermissions(req.GetShare().GetPermissions().GetPermissions()) {
|
||||
return &collaboration.UpdateShareResponse{
|
||||
Status: status.NewInvalidArg(ctx, "resharing not supported"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -427,6 +531,15 @@ func (s *service) UpdateShare(ctx context.Context, req *collaboration.UpdateShar
|
||||
}, err
|
||||
}
|
||||
|
||||
// resharing is forbidden for not space roots
|
||||
if !utils.IsSpaceRoot(sRes.GetInfo()) {
|
||||
// Resharing of Files/Directories is forbidden. So the grants must not allow the "grant" permissions
|
||||
if HasGrantPermissions(req.GetShare().GetPermissions().GetPermissions()) {
|
||||
return &collaboration.UpdateShareResponse{
|
||||
Status: status.NewInvalidArg(ctx, "resharing not supported"),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
// If this is a permissions update, check if user's permissions on the resource are sufficient to set the desired permissions
|
||||
var newPermissions *provider.ResourcePermissions
|
||||
if slices.Contains(req.GetUpdateMask().GetPaths(), _fieldMaskPathPermissions) {
|
||||
@@ -440,6 +553,20 @@ func (s *service) UpdateShare(ctx context.Context, req *collaboration.UpdateShar
|
||||
}, nil
|
||||
}
|
||||
|
||||
// For space root shares, if the new permissions would strip AddGrant or RemoveGrant from this share,
|
||||
// ensure at least one other share still has both so the space retains a manager.
|
||||
if utils.IsSpaceRoot(sRes.GetInfo()) && newPermissions != nil && (!newPermissions.GetAddGrant() || !newPermissions.GetRemoveGrant()) {
|
||||
if ok, err := s.spaceRootHasRemainingManager(ctx, currentShare.GetResourceId(), currentShare.GetId().GetOpaqueId()); err != nil {
|
||||
return &collaboration.UpdateShareResponse{
|
||||
Status: status.NewInternal(ctx, "failed to list shares for space root"),
|
||||
}, nil
|
||||
} else if !ok {
|
||||
return &collaboration.UpdateShareResponse{
|
||||
Status: status.NewPermissionDenied(ctx, nil, "cannot remove the last share with manager permissions on a space root"),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if the requested permission are plausible for the Resource
|
||||
// do we need more here?
|
||||
if sRes.GetInfo().GetType() == provider.ResourceType_RESOURCE_TYPE_FILE {
|
||||
@@ -457,10 +584,15 @@ func (s *service) UpdateShare(ctx context.Context, req *collaboration.UpdateShar
|
||||
}, nil
|
||||
}
|
||||
|
||||
var opaque *typesv1beta1.Opaque
|
||||
if utils.IsSpaceRoot(sRes.GetInfo()) {
|
||||
opaque = utils.SpaceGrantOpaque()
|
||||
}
|
||||
|
||||
res := &collaboration.UpdateShareResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Share: share,
|
||||
Opaque: utils.AppendPlainToOpaque(nil, "resourcename", sRes.GetInfo().GetName()),
|
||||
Opaque: utils.AppendPlainToOpaque(opaque, "resourcename", sRes.GetInfo().GetName()),
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
Generated
Vendored
+4
@@ -179,6 +179,10 @@ func (h *Handler) UpdateReceivedShare(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteOCSSuccess(w, r, []*conversions.ShareData{data})
|
||||
}
|
||||
|
||||
func (h *Handler) ReceivedShareNotFound(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "cannot find share", nil)
|
||||
}
|
||||
|
||||
func (h *Handler) updateReceivedShare(ctx context.Context, receivedShare *collaboration.ReceivedShare, fieldMask *fieldmaskpb.FieldMask) (*conversions.ShareData, response.Meta, error) {
|
||||
logger := appctx.GetLogger(ctx)
|
||||
|
||||
|
||||
Generated
Vendored
+3
-6
@@ -78,7 +78,6 @@ var (
|
||||
type Handler struct {
|
||||
gatewayAddr string
|
||||
machineAuthAPIKey string
|
||||
storageRegistryAddr string
|
||||
publicURL string
|
||||
sharePrefix string
|
||||
homeNamespace string
|
||||
@@ -129,7 +128,6 @@ type GatewayClientGetter func() (gateway.GatewayAPIClient, error)
|
||||
func (h *Handler) Init(c *config.Config) error {
|
||||
h.gatewayAddr = c.GatewaySvc
|
||||
h.machineAuthAPIKey = c.MachineAuthAPIKey
|
||||
h.storageRegistryAddr = c.StorageregistrySvc
|
||||
h.publicURL = c.Config.Host
|
||||
h.sharePrefix = c.SharePrefix
|
||||
h.homeNamespace = c.HomeNamespace
|
||||
@@ -941,12 +939,11 @@ func (h *Handler) RemoveShare(w http.ResponseWriter, r *http.Request) {
|
||||
h.removeFederatedShare(w, r, shareID)
|
||||
return
|
||||
}
|
||||
|
||||
if prov, ok := h.isSpaceShare(r, shareID); ok {
|
||||
// The request is a remove space member request.
|
||||
h.removeSpaceMember(w, r, shareID, prov)
|
||||
if h.isSpaceShare(r, shareID) {
|
||||
h.removeSpaceMember(w, r, shareID)
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "cannot find share", nil)
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+70
-143
@@ -29,18 +29,12 @@ import (
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
sdk "github.com/opencloud-eu/reva/v2/pkg/sdk/common"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
)
|
||||
|
||||
@@ -131,45 +125,34 @@ func (h *Handler) addSpaceMember(w http.ResponseWriter, r *http.Request, info *p
|
||||
fieldmask = append(fieldmask, "expiration")
|
||||
}
|
||||
|
||||
ref := provider.Reference{ResourceId: info.GetId()}
|
||||
p, err := h.findProvider(ctx, &ref)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting storage provider", err)
|
||||
lsRes, err := client.ListShares(ctx, &collaborationv1beta1.ListSharesRequest{
|
||||
Filters: []*collaborationv1beta1.Filter{
|
||||
{
|
||||
Type: collaborationv1beta1.Filter_TYPE_RESOURCE_ID,
|
||||
Term: &collaborationv1beta1.Filter_ResourceId{
|
||||
ResourceId: info.GetId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil || lsRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error listing space members", err)
|
||||
return
|
||||
}
|
||||
|
||||
providerClient, err := h.getStorageProviderClient(p)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting storage provider client", err)
|
||||
return
|
||||
}
|
||||
|
||||
lgRes, err := providerClient.ListGrants(ctx, &provider.ListGrantsRequest{Ref: &ref})
|
||||
if err != nil || lgRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error listing space grants", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !isSpaceManagerRemaining(lgRes.Grants, &grantee) {
|
||||
if !isSpaceManagerRemainingInShares(lsRes.Shares, &grantee) {
|
||||
response.WriteOCSError(w, r, http.StatusForbidden, "the space must have at least one manager", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// we have to send the update request to the gateway to give it a chance to invalidate its cache
|
||||
// TODO the gateway no longer should cache stuff because invalidation is to expensive. The decomposedfs already has a better cache.
|
||||
if granteeExists(lgRes.Grants, &grantee) {
|
||||
if existingShare := findShareByGrantee(lsRes.Shares, &grantee); existingShare != nil {
|
||||
if permissions != nil {
|
||||
fieldmask = append(fieldmask, "permissions")
|
||||
}
|
||||
updateShareReq := &collaborationv1beta1.UpdateShareRequest{
|
||||
// TODO: change CS3 APIs
|
||||
Opaque: &types.Opaque{
|
||||
Map: map[string]*types.OpaqueEntry{
|
||||
"spacegrant": {},
|
||||
},
|
||||
},
|
||||
Opaque: utils.AppendPlainToOpaque(nil, "spacetype", info.GetSpace().GetSpaceType()),
|
||||
Share: &collaborationv1beta1.Share{
|
||||
ResourceId: ref.GetResourceId(),
|
||||
Id: existingShare.Id,
|
||||
Permissions: &collaborationv1beta1.SharePermissions{
|
||||
Permissions: permissions,
|
||||
},
|
||||
@@ -180,10 +163,9 @@ func (h *Handler) addSpaceMember(w http.ResponseWriter, r *http.Request, info *p
|
||||
Paths: fieldmask,
|
||||
},
|
||||
}
|
||||
updateShareReq.Opaque = utils.AppendPlainToOpaque(updateShareReq.Opaque, "spacetype", info.GetSpace().GetSpaceType())
|
||||
updateShareRes, err := client.UpdateShare(ctx, updateShareReq)
|
||||
if err != nil || updateShareRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not update space member grant", err)
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not update space member", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -198,7 +180,7 @@ func (h *Handler) addSpaceMember(w http.ResponseWriter, r *http.Request, info *p
|
||||
},
|
||||
})
|
||||
if err != nil || createShareRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not add space member grant", err)
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not add space member", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -206,21 +188,22 @@ func (h *Handler) addSpaceMember(w http.ResponseWriter, r *http.Request, info *p
|
||||
response.WriteOCSSuccess(w, r, nil)
|
||||
}
|
||||
|
||||
func (h *Handler) isSpaceShare(r *http.Request, spaceID string) (*registry.ProviderInfo, bool) {
|
||||
ref, err := storagespace.ParseReference(spaceID)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
func (h *Handler) isSpaceShare(r *http.Request, shareID string) bool {
|
||||
ref, err := storagespace.ParseReference(shareID)
|
||||
// NOTE: we ignore the 'Path' part of the reference here as we're just interested in the space root
|
||||
switch {
|
||||
case err != nil:
|
||||
return false
|
||||
case ref.GetResourceId().GetSpaceId() == "":
|
||||
return false
|
||||
case ref.GetResourceId().GetOpaqueId() == "" || ref.GetResourceId().GetSpaceId() == ref.GetResourceId().GetOpaqueId():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
if ref.ResourceId.OpaqueId == "" {
|
||||
ref.ResourceId.OpaqueId = ref.ResourceId.SpaceId
|
||||
}
|
||||
|
||||
p, err := h.findProvider(r.Context(), &ref)
|
||||
return p, err == nil
|
||||
}
|
||||
|
||||
func (h *Handler) removeSpaceMember(w http.ResponseWriter, r *http.Request, spaceID string, prov *registry.ProviderInfo) {
|
||||
func (h *Handler) removeSpaceMember(w http.ResponseWriter, r *http.Request, spaceID string) {
|
||||
ctx := r.Context()
|
||||
|
||||
shareWith := r.URL.Query().Get("shareWith")
|
||||
@@ -245,129 +228,73 @@ func (h *Handler) removeSpaceMember(w http.ResponseWriter, r *http.Request, spac
|
||||
ref.ResourceId.OpaqueId = ref.ResourceId.SpaceId
|
||||
}
|
||||
|
||||
providerClient, err := h.getStorageProviderClient(prov)
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting storage provider client", err)
|
||||
return
|
||||
}
|
||||
|
||||
lgRes, err := providerClient.ListGrants(ctx, &provider.ListGrantsRequest{Ref: &ref})
|
||||
if err != nil || lgRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error listing space grants", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(lgRes.Grants) == 1 || !isSpaceManagerRemaining(lgRes.Grants, &grantee) {
|
||||
response.WriteOCSError(w, r, http.StatusForbidden, "can't remove the last manager", nil)
|
||||
return
|
||||
}
|
||||
|
||||
gatewayClient, err := h.getClient()
|
||||
client, err := h.getClient()
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting gateway client", err)
|
||||
return
|
||||
}
|
||||
|
||||
removeShareRes, err := gatewayClient.RemoveShare(ctx, &collaborationv1beta1.RemoveShareRequest{
|
||||
Ref: &collaborationv1beta1.ShareReference{
|
||||
Spec: &collaborationv1beta1.ShareReference_Key{
|
||||
Key: &collaborationv1beta1.ShareKey{
|
||||
lsRes, err := client.ListShares(ctx, &collaborationv1beta1.ListSharesRequest{
|
||||
Filters: []*collaborationv1beta1.Filter{
|
||||
{
|
||||
Type: collaborationv1beta1.Filter_TYPE_RESOURCE_ID,
|
||||
Term: &collaborationv1beta1.Filter_ResourceId{
|
||||
ResourceId: ref.ResourceId,
|
||||
Grantee: &grantee,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil || lsRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error listing space members", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(lsRes.Shares) == 1 || !isSpaceManagerRemainingInShares(lsRes.Shares, &grantee) {
|
||||
response.WriteOCSError(w, r, http.StatusForbidden, "can't remove the last manager", nil)
|
||||
return
|
||||
}
|
||||
|
||||
s := findShareByGrantee(lsRes.Shares, &grantee)
|
||||
if s == nil {
|
||||
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "cannot find share", nil)
|
||||
return
|
||||
}
|
||||
|
||||
removeShareRes, err := client.RemoveShare(ctx, &collaborationv1beta1.RemoveShareRequest{
|
||||
Ref: &collaborationv1beta1.ShareReference{
|
||||
Spec: &collaborationv1beta1.ShareReference_Id{
|
||||
Id: s.Id,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error removing grant", err)
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error removing space member", err)
|
||||
return
|
||||
}
|
||||
if removeShareRes.Status.Code != rpc.Code_CODE_OK {
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error removing grant", err)
|
||||
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error removing space member", nil)
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteOCSSuccess(w, r, nil)
|
||||
}
|
||||
|
||||
func (h *Handler) getStorageProviderClient(p *registry.ProviderInfo) (provider.ProviderAPIClient, error) {
|
||||
c, err := pool.GetStorageProviderServiceClient(p.Address)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "shares spaces: error getting a storage provider client")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (h *Handler) findProvider(ctx context.Context, ref *provider.Reference) (*registry.ProviderInfo, error) {
|
||||
c, err := pool.GetStorageRegistryClient(h.storageRegistryAddr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "shares spaces: error getting storage registry client")
|
||||
}
|
||||
|
||||
filters := map[string]string{}
|
||||
if ref.Path != "" {
|
||||
filters["path"] = ref.Path
|
||||
}
|
||||
if ref.ResourceId != nil {
|
||||
filters["storage_id"] = ref.ResourceId.StorageId
|
||||
filters["space_id"] = ref.ResourceId.SpaceId
|
||||
filters["opaque_id"] = ref.ResourceId.OpaqueId
|
||||
}
|
||||
|
||||
listReq := ®istry.ListStorageProvidersRequest{
|
||||
Opaque: &types.Opaque{},
|
||||
}
|
||||
sdk.EncodeOpaqueMap(listReq.Opaque, filters)
|
||||
|
||||
res, err := c.ListStorageProviders(ctx, listReq)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "shares spaces: error calling ListStorageProviders")
|
||||
}
|
||||
|
||||
if res.Status.Code != rpc.Code_CODE_OK {
|
||||
switch res.Status.Code {
|
||||
case rpc.Code_CODE_NOT_FOUND:
|
||||
return nil, errtypes.NotFound("shares spaces: storage provider not found for reference:" + ref.String())
|
||||
case rpc.Code_CODE_PERMISSION_DENIED:
|
||||
return nil, errtypes.PermissionDenied("shares spaces: " + res.Status.Message + " for " + ref.String() + " with code " + res.Status.Code.String())
|
||||
case rpc.Code_CODE_INVALID_ARGUMENT, rpc.Code_CODE_FAILED_PRECONDITION, rpc.Code_CODE_OUT_OF_RANGE:
|
||||
return nil, errtypes.BadRequest("shares spaces: " + res.Status.Message + " for " + ref.String() + " with code " + res.Status.Code.String())
|
||||
case rpc.Code_CODE_UNIMPLEMENTED:
|
||||
return nil, errtypes.NotSupported("shares spaces: " + res.Status.Message + " for " + ref.String() + " with code " + res.Status.Code.String())
|
||||
default:
|
||||
return nil, status.NewErrorFromCode(res.Status.Code, "shares spaces")
|
||||
}
|
||||
}
|
||||
|
||||
if len(res.Providers) < 1 {
|
||||
return nil, errtypes.NotFound("shares spaces: no provider found")
|
||||
}
|
||||
|
||||
return res.Providers[0], nil
|
||||
}
|
||||
|
||||
func isSpaceManagerRemaining(grants []*provider.Grant, grantee *provider.Grantee) bool {
|
||||
for _, g := range grants {
|
||||
// RemoveGrant is currently the way to check for the manager role
|
||||
// If it is not set than the current grant is not for a manager and
|
||||
// we can just continue with the next one.
|
||||
if g.Permissions.RemoveGrant && !isEqualGrantee(g.Grantee, grantee) {
|
||||
func isSpaceManagerRemainingInShares(shares []*collaborationv1beta1.Share, grantee *provider.Grantee) bool {
|
||||
for _, s := range shares {
|
||||
if s.GetPermissions().GetPermissions().GetRemoveGrant() && !isEqualGrantee(s.Grantee, grantee) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func granteeExists(grants []*provider.Grant, grantee *provider.Grantee) bool {
|
||||
for _, g := range grants {
|
||||
if isEqualGrantee(g.Grantee, grantee) {
|
||||
return true
|
||||
func findShareByGrantee(shares []*collaborationv1beta1.Share, grantee *provider.Grantee) *collaborationv1beta1.Share {
|
||||
for _, s := range shares {
|
||||
if isEqualGrantee(s.Grantee, grantee) {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
func isEqualGrantee(a, b *provider.Grantee) bool {
|
||||
|
||||
Generated
Vendored
+2
-1
@@ -34,6 +34,7 @@ import (
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/permission"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -330,7 +331,7 @@ func (h *Handler) listUserShares(r *http.Request, filters []*collaboration.Filte
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
lsUserSharesRequest := collaboration.ListSharesRequest{
|
||||
Filters: filters,
|
||||
Filters: append(filters, share.SpaceRootFilter(false)),
|
||||
}
|
||||
|
||||
ocsDataPayload := make([]*conversions.ShareData, 0)
|
||||
|
||||
Generated
Vendored
+7
@@ -123,6 +123,13 @@ func (s *svc) routerInit(log *zerolog.Logger) error {
|
||||
r.Delete("/", sharesHandler.RejectReceivedShare)
|
||||
r.Put("/", sharesHandler.UpdateReceivedShare)
|
||||
})
|
||||
r.Route("/pending", func(r chi.Router) {
|
||||
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
})
|
||||
r.Delete("/", sharesHandler.ReceivedShareNotFound)
|
||||
r.Put("/", sharesHandler.ReceivedShareNotFound)
|
||||
})
|
||||
r.Route("/remote_shares", func(r chi.Router) {
|
||||
r.Get("/", sharesHandler.ListFederatedShares)
|
||||
r.Get("/{shareid}", sharesHandler.GetFederatedShare)
|
||||
|
||||
+170
-77
@@ -36,9 +36,9 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/logger"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
migration "github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/migrations"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/providercache"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/receivedsharecache"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/sharecache"
|
||||
@@ -48,6 +48,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"google.golang.org/genproto/protobuf/field_mask"
|
||||
@@ -122,14 +123,20 @@ var (
|
||||
)
|
||||
|
||||
type config struct {
|
||||
GatewayAddr string `mapstructure:"gateway_addr"`
|
||||
MaxConcurrency int `mapstructure:"max_concurrency"`
|
||||
ProviderAddr string `mapstructure:"provider_addr"`
|
||||
ServiceUserID string `mapstructure:"service_user_id"`
|
||||
ServiceUserIdp string `mapstructure:"service_user_idp"`
|
||||
MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"`
|
||||
CacheTTL int `mapstructure:"ttl"`
|
||||
Events EventOptions `mapstructure:"events"`
|
||||
GatewayAddr string `mapstructure:"gateway_addr"`
|
||||
MaxConcurrency int `mapstructure:"max_concurrency"`
|
||||
ProviderAddr string `mapstructure:"provider_addr"`
|
||||
SystemUserID string `mapstructure:"system_user_id"`
|
||||
SystemUserIdp string `mapstructure:"system_user_idp"`
|
||||
MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"`
|
||||
ServiceAccountID string `mapstructure:"service_account_id"`
|
||||
ServiceAccountSecret string `mapstructure:"service_account_secret"`
|
||||
// ProviderRegistryAddr is the address of the storage registry used during
|
||||
// migrations. Defaults to GatewayAddr when empty, because in the default
|
||||
// OpenCloud deployment the registry is co-located with the gateway.
|
||||
ProviderRegistryAddr string `mapstructure:"provider_registry_addr"`
|
||||
CacheTTL int `mapstructure:"ttl"`
|
||||
Events EventOptions `mapstructure:"events"`
|
||||
}
|
||||
|
||||
// EventOptions are the configurable options for events
|
||||
@@ -145,8 +152,6 @@ type EventOptions struct {
|
||||
|
||||
// Manager implements a share manager using a cs3 storage backend with local caching
|
||||
type Manager struct {
|
||||
sync.RWMutex
|
||||
|
||||
Cache providercache.Cache // holds all shares, sharded by provider id and space id
|
||||
CreatedCache sharecache.Cache // holds the list of shares a user has created, sharded by user id
|
||||
GroupReceivedCache sharecache.Cache // holds the list of shares a group has access to, sharded by group id
|
||||
@@ -155,23 +160,25 @@ type Manager struct {
|
||||
storage metadata.Storage
|
||||
SpaceRoot *provider.ResourceId
|
||||
|
||||
initialized bool
|
||||
ready chan struct{} // closed once initialize() has completed successfully
|
||||
migrationsDone chan struct{} // closed once doMigrations() has returned on this instance
|
||||
|
||||
MaxConcurrency int
|
||||
|
||||
gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient]
|
||||
eventStream events.Stream
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewDefault returns a new manager instance with default dependencies
|
||||
func NewDefault(m map[string]interface{}) (share.Manager, error) {
|
||||
func NewDefault(m map[string]interface{}, logger *zerolog.Logger) (share.Manager, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
err = errors.Wrap(err, "error creating a new manager")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, err := metadata.NewCS3Storage(c.ProviderAddr, c.ProviderAddr, c.ServiceUserID, c.ServiceUserIdp, c.MachineAuthAPIKey)
|
||||
s, err := metadata.NewCS3Storage(c.ProviderAddr, c.ProviderAddr, c.SystemUserID, c.SystemUserIdp, c.MachineAuthAPIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -189,11 +196,34 @@ func NewDefault(m map[string]interface{}) (share.Manager, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return New(s, gatewaySelector, c.CacheTTL, es, c.MaxConcurrency)
|
||||
mgr, err := New(s, logger, gatewaySelector, c.CacheTTL, es, c.MaxConcurrency)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providerRegistryAddr := c.ProviderRegistryAddr
|
||||
if providerRegistryAddr == "" {
|
||||
providerRegistryAddr = c.GatewayAddr
|
||||
}
|
||||
mgr.RunMigrations(migration.MigrationConfig{
|
||||
ServiceAccountID: c.ServiceAccountID,
|
||||
ServiceAccountSecret: c.ServiceAccountSecret,
|
||||
ProviderRegistryAddr: providerRegistryAddr,
|
||||
})
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
// New returns a new manager instance.
|
||||
func New(s metadata.Storage, gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient], ttlSeconds int, es events.Stream, maxconcurrency int) (*Manager, error) {
|
||||
func New(s metadata.Storage,
|
||||
logger *zerolog.Logger,
|
||||
gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient],
|
||||
ttlSeconds int,
|
||||
es events.Stream,
|
||||
maxconcurrency int,
|
||||
) (*Manager, error) {
|
||||
if logger == nil {
|
||||
nop := zerolog.Nop()
|
||||
logger = &nop
|
||||
}
|
||||
ttl := time.Duration(ttlSeconds) * time.Second
|
||||
|
||||
m := &Manager{
|
||||
@@ -205,13 +235,38 @@ func New(s metadata.Storage, gatewaySelector pool.Selectable[gatewayv1beta1.Gate
|
||||
gatewaySelector: gatewaySelector,
|
||||
eventStream: es,
|
||||
MaxConcurrency: maxconcurrency,
|
||||
logger: logger,
|
||||
ready: make(chan struct{}),
|
||||
// migrationsDone is open (blocking) by default. It is closed by
|
||||
// doMigrations when all migrations complete, or by SkipMigrations for
|
||||
// callers (e.g. tests) that do not run migrations at all.
|
||||
migrationsDone: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Initialize the metadata storage connection in the background, retrying
|
||||
// with exponential backoff if the backend is not yet available.
|
||||
go func() {
|
||||
backoff := time.Second
|
||||
for {
|
||||
if err := m.initialize(context.Background()); err != nil {
|
||||
logger.Info().Err(err).Dur("backoff", backoff).Msg("share manager: metadata storage initialization failed, retrying")
|
||||
time.Sleep(backoff)
|
||||
if backoff < 30*time.Second {
|
||||
backoff *= 2
|
||||
}
|
||||
continue
|
||||
}
|
||||
logger.Debug().Msg("share manager: initialization succeeded")
|
||||
close(m.ready)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// listen for events
|
||||
if m.eventStream != nil {
|
||||
ch, err := events.Consume(m.eventStream, "jsoncs3sharemanager", _registeredEvents...)
|
||||
if err != nil {
|
||||
appctx.GetLogger(context.Background()).Error().Err(err).Msg("error consuming events")
|
||||
logger.Error().Err(err).Msg("error consuming events")
|
||||
}
|
||||
go m.ProcessEvents(ch)
|
||||
}
|
||||
@@ -219,23 +274,13 @@ func New(s metadata.Storage, gatewaySelector pool.Selectable[gatewayv1beta1.Gate
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// initialize connects to the metadata storage backend and ensures the required
|
||||
// directory structure exists. It is called once at startup from a background
|
||||
// goroutine (see New) and must not be called concurrently.
|
||||
func (m *Manager) initialize(ctx context.Context) error {
|
||||
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "initialize")
|
||||
defer span.End()
|
||||
if m.initialized {
|
||||
span.SetStatus(codes.Ok, "already initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if m.initialized { // check if initialization happened while grabbing the lock
|
||||
span.SetStatus(codes.Ok, "initialized while grabbing lock")
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx = context.Background()
|
||||
err := m.storage.Init(ctx, "jsoncs3-share-manager-metadata")
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
@@ -261,21 +306,85 @@ func (m *Manager) initialize(ctx context.Context) error {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return err
|
||||
}
|
||||
err = m.storage.MakeDirIfNotExist(ctx, "migrations")
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
m.initialized = true
|
||||
span.SetStatus(codes.Ok, "initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitForInit blocks until the background initialization goroutine has
|
||||
// successfully completed, or until ctx is cancelled.
|
||||
func (m *Manager) waitForInit(ctx context.Context) error {
|
||||
select {
|
||||
case <-m.ready:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return errors.Wrap(ctx.Err(), "share manager not yet initialized")
|
||||
}
|
||||
}
|
||||
|
||||
// waitForMigrations blocks until both storage initialization and all data
|
||||
// migrations have completed on this instance, or until ctx is cancelled.
|
||||
// It is a strict superset of waitForInit and should be used by write operations
|
||||
// to ensure no writes race with an in-progress migration.
|
||||
func (m *Manager) waitForMigrations(ctx context.Context) error {
|
||||
select {
|
||||
case <-m.ready:
|
||||
case <-ctx.Done():
|
||||
return errors.Wrap(ctx.Err(), "share manager not yet initialized")
|
||||
}
|
||||
select {
|
||||
case <-m.migrationsDone:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return errors.Wrap(ctx.Err(), "share manager migrations not yet complete")
|
||||
}
|
||||
}
|
||||
|
||||
// RunMigrations starts data migrations in a background goroutine. It should be
|
||||
// called once after New() in production server startup. Callers that do not
|
||||
// need migrations should call SkipMigrations instead to unblock write operations.
|
||||
func (m *Manager) RunMigrations(cfg migration.MigrationConfig) {
|
||||
go m.doMigrations(cfg)
|
||||
}
|
||||
|
||||
// SkipMigrations unblocks write operations on this instance without running
|
||||
// any migrations. It must be called when RunMigrations will not be called,
|
||||
// for example in tests.
|
||||
func (m *Manager) SkipMigrations() {
|
||||
close(m.migrationsDone)
|
||||
}
|
||||
|
||||
func (m *Manager) doMigrations(cfg migration.MigrationConfig) {
|
||||
// Always close migrationsDone when this goroutine exits, whether migrations
|
||||
// ran, were skipped, or failed. This unblocks write operations on this
|
||||
// instance. Non-winning instances are held here by acquireLock until the
|
||||
// winning instance finishes, so the close happens only after the storage
|
||||
// state is fully migrated.
|
||||
defer close(m.migrationsDone)
|
||||
if err := m.waitForInit(context.Background()); err != nil {
|
||||
m.logger.Error().Err(err).Msg("share manager: aborting migrations, manager did not initialize")
|
||||
return
|
||||
}
|
||||
m.logger.Debug().Msg("migrations start")
|
||||
migrations := migration.New(*m.logger, m.gatewaySelector, m.storage, cfg, m, m)
|
||||
migrations.RunMigrations()
|
||||
}
|
||||
|
||||
func (m *Manager) ProcessEvents(ch <-chan events.Event) {
|
||||
log := logger.New()
|
||||
log := m.logger
|
||||
ctx := context.Background()
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
log.Error().Err(err).Msg("share manager: error waiting for initialization")
|
||||
return
|
||||
}
|
||||
for event := range ch {
|
||||
ctx := context.Background()
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
log.Error().Err(err).Msg("error initializing manager")
|
||||
}
|
||||
|
||||
if ev, ok := event.Event.(events.SpaceDeleted); ok {
|
||||
log.Debug().Msgf("space deleted event: %v", ev)
|
||||
go func() { m.purgeSpace(ctx, ev.ID) }()
|
||||
@@ -287,7 +396,7 @@ func (m *Manager) ProcessEvents(ch <-chan events.Event) {
|
||||
func (m *Manager) Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) {
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Share")
|
||||
defer span.End()
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
@@ -296,16 +405,6 @@ func (m *Manager) Share(ctx context.Context, md *provider.ResourceInfo, g *colla
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
ts := utils.TSNow()
|
||||
|
||||
// do not allow share to myself or the owner if share is for a user
|
||||
// TODO: should this not already be caught at the gw level?
|
||||
if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER &&
|
||||
(utils.UserEqual(g.Grantee.GetUserId(), user.Id) || utils.UserEqual(g.Grantee.GetUserId(), md.Owner)) {
|
||||
err := errtypes.BadRequest("jsoncs3: owner/creator and grantee are the same")
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check if share already exists.
|
||||
key := &collaboration.ShareKey{
|
||||
// Owner: md.Owner, owner no longer matters as it belongs to the space
|
||||
@@ -446,7 +545,7 @@ func (m *Manager) GetShare(ctx context.Context, ref *collaboration.ShareReferenc
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetShare")
|
||||
defer span.End()
|
||||
sublog := appctx.GetLogger(ctx).With().Str("id", ref.GetId().GetOpaqueId()).Str("key", ref.GetKey().String()).Str("driver", "jsoncs3").Str("handler", "GetShare").Logger()
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -504,21 +603,14 @@ func (m *Manager) Unshare(ctx context.Context, ref *collaboration.ShareReference
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Unshare")
|
||||
defer span.End()
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
s, err := m.get(ctx, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO allow manager to unshare shares in a space created by other users
|
||||
if !share.IsCreatedByUser(s, user) {
|
||||
// TODO why not permission denied?
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
return m.removeShare(ctx, s, false)
|
||||
}
|
||||
@@ -528,7 +620,7 @@ func (m *Manager) UpdateShare(ctx context.Context, ref *collaboration.ShareRefer
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "UpdateShare")
|
||||
defer span.End()
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -616,7 +708,7 @@ func (m *Manager) ListShares(ctx context.Context, filters []*collaboration.Filte
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "ListShares")
|
||||
defer span.End()
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -833,7 +925,7 @@ func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaborati
|
||||
defer span.End()
|
||||
sublog := appctx.GetLogger(ctx).With().Str("driver", "jsoncs3").Str("handler", "ListReceivedShares").Logger()
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1029,7 +1121,7 @@ func (m *Manager) convert(ctx context.Context, userID string, s *collaboration.S
|
||||
|
||||
// GetReceivedShare returns the information for a received share.
|
||||
func (m *Manager) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1073,7 +1165,7 @@ func (m *Manager) UpdateReceivedShare(ctx context.Context, receivedShare *collab
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "UpdateReceivedShare")
|
||||
defer span.End()
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1120,8 +1212,8 @@ func updateShareID(share *collaboration.Share) {
|
||||
|
||||
// Load imports shares and received shares from channels (e.g. during migration)
|
||||
func (m *Manager) Load(ctx context.Context, shareChan <-chan *collaboration.Share, receivedShareChan <-chan share.ReceivedShareWithUser) error {
|
||||
log := appctx.GetLogger(ctx)
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
l := m.logger
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1136,14 +1228,14 @@ func (m *Manager) Load(ctx context.Context, shareChan <-chan *collaboration.Shar
|
||||
updateShareID(s)
|
||||
}
|
||||
if err := m.Cache.Add(context.Background(), s.GetResourceId().GetStorageId(), s.GetResourceId().GetSpaceId(), s.Id.OpaqueId, s); err != nil {
|
||||
log.Error().Err(err).Interface("share", s).Msg("error persisting share")
|
||||
l.Error().Err(err).Interface("share", s).Msg("error persisting share")
|
||||
} else {
|
||||
log.Debug().Str("storageid", s.GetResourceId().GetStorageId()).Str("spaceid", s.GetResourceId().GetSpaceId()).Str("shareid", s.Id.OpaqueId).Msg("imported share")
|
||||
l.Debug().Str("storageid", s.GetResourceId().GetStorageId()).Str("spaceid", s.GetResourceId().GetSpaceId()).Str("shareid", s.Id.OpaqueId).Msg("imported share")
|
||||
}
|
||||
if err := m.CreatedCache.Add(ctx, s.GetCreator().GetOpaqueId(), s.Id.OpaqueId); err != nil {
|
||||
log.Error().Err(err).Interface("share", s).Msg("error persisting created cache")
|
||||
l.Error().Err(err).Interface("share", s).Msg("error persisting created cache")
|
||||
} else {
|
||||
log.Debug().Str("creatorid", s.GetCreator().GetOpaqueId()).Str("shareid", s.Id.OpaqueId).Msg("updated created cache")
|
||||
l.Debug().Str("creatorid", s.GetCreator().GetOpaqueId()).Str("shareid", s.Id.OpaqueId).Msg("updated created cache")
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
@@ -1154,18 +1246,19 @@ func (m *Manager) Load(ctx context.Context, shareChan <-chan *collaboration.Shar
|
||||
if !shareIsRoutable(s.ReceivedShare.GetShare()) {
|
||||
updateShareID(s.ReceivedShare.GetShare())
|
||||
}
|
||||
switch s.ReceivedShare.Share.Grantee.Type {
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
if err := m.UserReceivedStates.Add(context.Background(), s.ReceivedShare.GetShare().GetGrantee().GetUserId().GetOpaqueId(), s.ReceivedShare.GetShare().GetResourceId().GetSpaceId(), s.ReceivedShare); err != nil {
|
||||
log.Error().Err(err).Interface("received share", s).Msg("error persisting received share for user")
|
||||
if s.UserID != nil {
|
||||
spaceid := s.ReceivedShare.GetShare().GetResourceId().GetStorageId() + shareid.IDDelimiter + s.ReceivedShare.GetShare().GetResourceId().GetSpaceId()
|
||||
if err := m.UserReceivedStates.Add(context.Background(), s.UserID.GetOpaqueId(), spaceid, s.ReceivedShare); err != nil {
|
||||
l.Error().Err(err).Interface("received share", s).Msg("error persisting received share for user")
|
||||
} else {
|
||||
log.Debug().Str("userid", s.ReceivedShare.GetShare().GetGrantee().GetUserId().GetOpaqueId()).Str("spaceid", s.ReceivedShare.GetShare().GetResourceId().GetSpaceId()).Str("shareid", s.ReceivedShare.GetShare().Id.OpaqueId).Msg("updated received share userdata")
|
||||
l.Debug().Str("userid", s.UserID.GetOpaqueId()).Str("spaceid", spaceid).Str("shareid", s.ReceivedShare.GetShare().Id.OpaqueId).Msg("updated received share userdata")
|
||||
}
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
}
|
||||
if s.ReceivedShare.Share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP && s.UserID == nil {
|
||||
if err := m.GroupReceivedCache.Add(context.Background(), s.ReceivedShare.GetShare().GetGrantee().GetGroupId().GetOpaqueId(), s.ReceivedShare.GetShare().GetId().GetOpaqueId()); err != nil {
|
||||
log.Error().Err(err).Interface("received share", s).Msg("error persisting received share to group cache")
|
||||
l.Error().Err(err).Interface("received share", s).Msg("error persisting received share to group cache")
|
||||
} else {
|
||||
log.Debug().Str("groupid", s.ReceivedShare.GetShare().GetGrantee().GetGroupId().GetOpaqueId()).Str("shareid", s.ReceivedShare.GetShare().Id.OpaqueId).Msg("updated received share group cache")
|
||||
l.Debug().Str("groupid", s.ReceivedShare.GetShare().GetGrantee().GetGroupId().GetOpaqueId()).Str("shareid", s.ReceivedShare.GetShare().Id.OpaqueId).Msg("updated received share group cache")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1237,7 +1330,7 @@ func (m *Manager) removeShare(ctx context.Context, s *collaboration.Share, skipS
|
||||
func (m *Manager) CleanupStaleShares(ctx context.Context) {
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+435
@@ -0,0 +1,435 @@
|
||||
// Copyright 2026 OpenCloud GmbH
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff"
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
|
||||
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/google/uuid"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/shareid"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// storageProvider is the narrow subset of provider.ProviderAPIClient that the
|
||||
// migration actually uses. Keeping it narrow makes test stubs trivial to write.
|
||||
type storageProvider interface {
|
||||
ListGrants(ctx context.Context, in *provider.ListGrantsRequest, opts ...grpc.CallOption) (*provider.ListGrantsResponse, error)
|
||||
}
|
||||
|
||||
type ImportSpaceMembersMigration struct {
|
||||
cfg config
|
||||
sharesChan chan *collaboration.Share
|
||||
receivedChan chan share.ReceivedShareWithUser
|
||||
userCache map[string]*userpb.UserId
|
||||
groupCache map[string]*grouppb.GroupId
|
||||
providerResolver func(context.Context, *provider.StorageSpace) (storageProvider, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerMigration(&ImportSpaceMembersMigration{})
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) Initialize(cfg config) {
|
||||
m.cfg = cfg
|
||||
m.sharesChan = make(chan *collaboration.Share)
|
||||
m.receivedChan = make(chan share.ReceivedShareWithUser)
|
||||
m.userCache = make(map[string]*userpb.UserId)
|
||||
m.groupCache = make(map[string]*grouppb.GroupId)
|
||||
m.providerResolver = func(ctx context.Context, space *provider.StorageSpace) (storageProvider, error) {
|
||||
return m.storageProviderForSpace(ctx, space)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) Name() string {
|
||||
return "import_space_members"
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) Version() int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) Migrate() error {
|
||||
gwc, err := m.cfg.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svcCtx, err := utils.GetServiceUserContextWithContext(context.Background(), gwc, m.cfg.serviceAccountID, m.cfg.serviceAccountSecret)
|
||||
if err != nil {
|
||||
m.cfg.logger.Error().Err(err).Msg("failed to get service user context for migration")
|
||||
return err
|
||||
}
|
||||
// List all project spaces.
|
||||
listRes, err := gwc.ListStorageSpaces(svcCtx, &provider.ListStorageSpacesRequest{
|
||||
Opaque: utils.AppendPlainToOpaque(nil, "unrestricted", "true"),
|
||||
Filters: []*provider.ListStorageSpacesRequest_Filter{
|
||||
{
|
||||
Type: provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE,
|
||||
Term: &provider.ListStorageSpacesRequest_Filter_SpaceType{SpaceType: "project"},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
m.cfg.logger.Error().Err(err).Msg("space-membership migration: failed to list storage spaces")
|
||||
return err
|
||||
}
|
||||
|
||||
if listRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
m.cfg.logger.Error().Str("status", listRes.GetStatus().GetMessage()).Msg("space-membership migration: ListStorageSpaces returned non-OK status")
|
||||
return errtypes.InternalError("ListStorageSpaces")
|
||||
}
|
||||
|
||||
spaces := listRes.GetStorageSpaces()
|
||||
m.cfg.logger.Info().Int("spaces", len(spaces)).Msg("Starting migration")
|
||||
|
||||
// loadCtx is cancelled when the producer finishes (or fails) so that the
|
||||
// Load goroutine — which blocks reading from the channels — is not left
|
||||
// waiting forever if we return early from an error.
|
||||
loadCtx, cancelLoad := context.WithCancel(svcCtx)
|
||||
defer cancelLoad()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var loaderError error
|
||||
wg.Go(func() {
|
||||
loaderError = m.cfg.loader.Load(loadCtx, m.sharesChan, m.receivedChan)
|
||||
})
|
||||
|
||||
migrated := 0
|
||||
for _, space := range spaces {
|
||||
sharesCreated, err := m.migrateSpace(loadCtx, space)
|
||||
if err != nil {
|
||||
m.cfg.logger.Error().Err(err).Str("space", space.GetId().GetOpaqueId()).Msg("failed to migrate space; continuing with remaining spaces")
|
||||
continue
|
||||
}
|
||||
migrated++
|
||||
m.cfg.logger.Debug().
|
||||
Str("space", space.GetId().GetOpaqueId()).
|
||||
Int("shares_created", sharesCreated).
|
||||
Msg("space migrated")
|
||||
if migrated%10 == 0 {
|
||||
m.cfg.logger.Info().
|
||||
Int("migrated", migrated).
|
||||
Int("total", len(spaces)).
|
||||
Msg("migration progress")
|
||||
}
|
||||
}
|
||||
close(m.receivedChan)
|
||||
close(m.sharesChan)
|
||||
|
||||
wg.Wait()
|
||||
m.cfg.logger.Info().Err(loaderError).Int("migrated", migrated).Int("total", len(spaces)).Msg("Migration finished")
|
||||
return loaderError
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) migrateSpace(ctx context.Context, space *provider.StorageSpace) (int, error) {
|
||||
spClient, err := m.providerResolver(ctx, space)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
ref := &provider.Reference{ResourceId: space.GetRoot()}
|
||||
grantsRes, err := spClient.ListGrants(ctx, &provider.ListGrantsRequest{Ref: ref})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if grantsRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return 0, errtypes.NewErrtypeFromStatus(grantsRes.GetStatus())
|
||||
}
|
||||
|
||||
sharesCreated := 0
|
||||
for _, grant := range grantsRes.GetGrants() {
|
||||
share, receivedShares, err := m.spaceGrantToShares(ctx, grant, space)
|
||||
if err != nil {
|
||||
m.cfg.logger.Error().Err(err).
|
||||
Interface("grant", grant).
|
||||
Msg("Failed to convert grant to shares")
|
||||
continue
|
||||
}
|
||||
if share == nil {
|
||||
// share already existed; nothing to import for this grant
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case m.sharesChan <- share:
|
||||
case <-ctx.Done():
|
||||
return sharesCreated, ctx.Err()
|
||||
}
|
||||
for _, rs := range receivedShares {
|
||||
select {
|
||||
case m.receivedChan <- rs:
|
||||
case <-ctx.Done():
|
||||
return sharesCreated, ctx.Err()
|
||||
}
|
||||
}
|
||||
sharesCreated++
|
||||
}
|
||||
return sharesCreated, nil
|
||||
}
|
||||
|
||||
// resolveRetries is the maximum number of times resolveUserID / resolveGroupID
|
||||
// will retry after receiving an errtypes.Unavailable response (LDAP down).
|
||||
const resolveRetries = 10
|
||||
|
||||
// retryOnUnavailable calls op, retrying with exponential backoff whenever op
|
||||
// returns errtypes.Unavailable. Any other error (including context
|
||||
// cancellation) stops the loop immediately and is returned as-is.
|
||||
// Retries are capped at resolveRetries attempts and respect ctx cancellation.
|
||||
func retryOnUnavailable(ctx context.Context, log zerolog.Logger, op func() error) error {
|
||||
b := backoff.WithContext(
|
||||
backoff.WithMaxRetries(backoff.NewExponentialBackOff(), resolveRetries),
|
||||
ctx,
|
||||
)
|
||||
notify := func(err error, d time.Duration) {
|
||||
log.Warn().Err(err).Dur("retry_in", d).Msg("identity provider temporarily unavailable, retrying")
|
||||
}
|
||||
return backoff.RetryNotify(func() error {
|
||||
err := op()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if _, ok := err.(errtypes.Unavailable); ok {
|
||||
return err // transient — keep retrying
|
||||
}
|
||||
return backoff.Permanent(err) // permanent — stop immediately
|
||||
}, b, notify)
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) resolveUserID(ctx context.Context, opaqueID string) (*userpb.UserId, error) {
|
||||
if id, ok := m.userCache[opaqueID]; ok {
|
||||
return id, nil
|
||||
}
|
||||
var id *userpb.UserId
|
||||
err := retryOnUnavailable(ctx, m.cfg.logger, func() error {
|
||||
gwc, err := m.cfg.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := gwc.GetUser(ctx, &userpb.GetUserRequest{
|
||||
UserId: &userpb.UserId{OpaqueId: opaqueID},
|
||||
SkipFetchingUserGroups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
// errtypes.NewErrtypeFromStatus maps CODE_UNAVAILABLE → errtypes.Unavailable,
|
||||
// which retryOnUnavailable will retry; all other codes are treated as permanent.
|
||||
return errtypes.NewErrtypeFromStatus(res.GetStatus())
|
||||
}
|
||||
id = res.GetUser().GetId()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.userCache[opaqueID] = id
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) resolveGroupID(ctx context.Context, opaqueID string) (*grouppb.GroupId, error) {
|
||||
if id, ok := m.groupCache[opaqueID]; ok {
|
||||
return id, nil
|
||||
}
|
||||
var id *grouppb.GroupId
|
||||
err := retryOnUnavailable(ctx, m.cfg.logger, func() error {
|
||||
gwc, err := m.cfg.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := gwc.GetGroup(ctx, &grouppb.GetGroupRequest{
|
||||
GroupId: &grouppb.GroupId{OpaqueId: opaqueID},
|
||||
SkipFetchingMembers: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return errtypes.NewErrtypeFromStatus(res.GetStatus())
|
||||
}
|
||||
id = res.GetGroup().GetId()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.groupCache[opaqueID] = id
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) spaceGrantToShares(ctx context.Context, grant *provider.Grant, space *provider.StorageSpace) (*collaboration.Share, []share.ReceivedShareWithUser, error) {
|
||||
// The grantee ids as persisted on disk do not have an IDP or type stored as
|
||||
// part of the userid/groupid. Resolve them via the gateway so we get the
|
||||
// full userid
|
||||
switch grant.GetGrantee().GetType() {
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
groupID, err := m.resolveGroupID(ctx, grant.GetGrantee().GetGroupId().GetOpaqueId())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("resolve group %s: %w", grant.GetGrantee().GetGroupId().GetOpaqueId(), err)
|
||||
}
|
||||
grant.Grantee.Id = &provider.Grantee_GroupId{GroupId: groupID}
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
userID, err := m.resolveUserID(ctx, grant.GetGrantee().GetUserId().GetOpaqueId())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("resolve user %s: %w", grant.GetGrantee().GetUserId().GetOpaqueId(), err)
|
||||
}
|
||||
grant.Grantee.Id = &provider.Grantee_UserId{UserId: userID}
|
||||
}
|
||||
|
||||
ref := &collaboration.ShareReference{
|
||||
Spec: &collaboration.ShareReference_Key{
|
||||
Key: &collaboration.ShareKey{
|
||||
ResourceId: space.GetRoot(),
|
||||
Grantee: grant.GetGrantee(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx = ctxpkg.ContextSetUser(ctx, &userpb.User{Id: grant.Creator})
|
||||
if s, err := m.cfg.manager.GetShare(ctx, ref); err == nil {
|
||||
// FIXME: Verify the actual grants?
|
||||
m.cfg.logger.Debug().Interface("share", s).Msg("share already exists")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
ts := utils.TSNow()
|
||||
shareID := shareid.Encode(space.GetRoot().GetStorageId(), space.GetRoot().GetSpaceId(), uuid.NewString())
|
||||
|
||||
creator := grant.GetCreator()
|
||||
if creator.Type == userpb.UserType_USER_TYPE_INVALID {
|
||||
creator = nil
|
||||
}
|
||||
newShare := &collaboration.Share{
|
||||
Id: &collaboration.ShareId{OpaqueId: shareID},
|
||||
ResourceId: space.GetRoot(),
|
||||
Permissions: &collaboration.SharePermissions{Permissions: grant.GetPermissions()},
|
||||
Grantee: grant.GetGrantee(),
|
||||
Expiration: grant.GetExpiration(),
|
||||
Owner: creator,
|
||||
Creator: creator,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}
|
||||
|
||||
var newReceivedShares []share.ReceivedShareWithUser
|
||||
switch grant.GetGrantee().GetType() {
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
gwc, err := m.cfg.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
m.cfg.logger.Error().Err(err).Msg("Failed to get gateway client")
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
gr, err := gwc.GetMembers(ctx, &grouppb.GetMembersRequest{
|
||||
GroupId: grant.GetGrantee().GetGroupId(),
|
||||
})
|
||||
if err != nil {
|
||||
m.cfg.logger.Error().Err(err).Msg("Failed to expand group membership")
|
||||
return nil, nil, err
|
||||
}
|
||||
if gr.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
m.cfg.logger.Error().Str("Status", gr.GetStatus().GetMessage()).Msg("Failed to expand group membership")
|
||||
return nil, nil, errtypes.NewErrtypeFromStatus(gr.GetStatus())
|
||||
}
|
||||
for _, u := range gr.GetMembers() {
|
||||
newReceivedShares = append(newReceivedShares, share.ReceivedShareWithUser{
|
||||
UserID: u,
|
||||
ReceivedShare: &collaboration.ReceivedShare{
|
||||
Share: newShare,
|
||||
State: collaboration.ShareState_SHARE_STATE_ACCEPTED,
|
||||
},
|
||||
})
|
||||
}
|
||||
// Also add a group-level entry (UserID == nil) so the group cache is populated.
|
||||
newReceivedShares = append(newReceivedShares, share.ReceivedShareWithUser{
|
||||
UserID: nil,
|
||||
ReceivedShare: &collaboration.ReceivedShare{
|
||||
Share: newShare,
|
||||
State: collaboration.ShareState_SHARE_STATE_ACCEPTED,
|
||||
},
|
||||
})
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
newReceivedShares = append(newReceivedShares, share.ReceivedShareWithUser{
|
||||
UserID: grant.GetGrantee().GetUserId(),
|
||||
ReceivedShare: &collaboration.ReceivedShare{
|
||||
Share: newShare,
|
||||
State: collaboration.ShareState_SHARE_STATE_ACCEPTED,
|
||||
},
|
||||
})
|
||||
}
|
||||
return newShare, newReceivedShares, nil
|
||||
}
|
||||
|
||||
// storageProviderForSpace resolves the storageprovider responsible for the
|
||||
// given storage space and returns a dialled client. In the default opencloud
|
||||
// deployment the storage registry is co-located with the gateway, so
|
||||
// the GatewayAddr is used as the registry address.
|
||||
func (m *ImportSpaceMembersMigration) storageProviderForSpace(ctx context.Context, space *provider.StorageSpace) (provider.ProviderAPIClient, error) {
|
||||
|
||||
srClient, err := pool.GetStorageRegistryClient(m.cfg.providerRegistryAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get storage registry client: %w", err)
|
||||
}
|
||||
|
||||
spaceJSON, err := json.Marshal(space)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal space: %w", err)
|
||||
}
|
||||
|
||||
res, err := srClient.GetStorageProviders(ctx, ®istry.GetStorageProvidersRequest{
|
||||
Opaque: &typesv1beta1.Opaque{
|
||||
Map: map[string]*typesv1beta1.OpaqueEntry{
|
||||
"space": {
|
||||
Decoder: "json",
|
||||
Value: spaceJSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetStorageProviders: %w", err)
|
||||
}
|
||||
if len(res.GetProviders()) == 0 {
|
||||
return nil, fmt.Errorf("no storage provider found for space %s", space.GetId().GetOpaqueId())
|
||||
}
|
||||
|
||||
c, err := pool.GetStorageProviderServiceClient(res.GetProviders()[0].GetAddress())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial storage provider: %w", err)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
Generated
Vendored
+353
@@ -0,0 +1,353 @@
|
||||
// Copyright 2026 OpenCloud GmbH
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const stateFile = "migrations/state.json"
|
||||
|
||||
const (
|
||||
lockFile = "migrations/lock.json"
|
||||
lockTTL = time.Minute
|
||||
lockHeartbeatInterval = 20 * time.Second
|
||||
)
|
||||
|
||||
// lockPollInterval is how long acquireLock sleeps between retries when the
|
||||
// lock is held by another instance. Declared as a variable so tests can
|
||||
// shorten it without rebuilding.
|
||||
var lockPollInterval = 5 * time.Second
|
||||
|
||||
// lockData is the content written to the lock file.
|
||||
type lockData struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
InstanceID string `json:"instance_id"`
|
||||
}
|
||||
|
||||
type migration interface {
|
||||
Name() string
|
||||
Version() int
|
||||
Initialize(config)
|
||||
Migrate() error
|
||||
}
|
||||
|
||||
// persistedState is the on-disk representation of the migration state.
|
||||
type persistedState struct {
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
type state struct {
|
||||
version int
|
||||
}
|
||||
|
||||
// MigrationConfig holds all caller-supplied options for a migration run.
|
||||
// It is intentionally a plain struct so that new fields can be added without
|
||||
// changing function signatures throughout the call chain.
|
||||
type MigrationConfig struct {
|
||||
ServiceAccountID string
|
||||
ServiceAccountSecret string
|
||||
ProviderRegistryAddr string
|
||||
}
|
||||
|
||||
type config struct {
|
||||
logger zerolog.Logger
|
||||
gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient]
|
||||
storage metadata.Storage
|
||||
serviceAccountID string
|
||||
serviceAccountSecret string
|
||||
providerRegistryAddr string
|
||||
manager share.Manager
|
||||
loader share.LoadableManager
|
||||
}
|
||||
|
||||
type Migrations struct {
|
||||
config
|
||||
state state
|
||||
instanceID string
|
||||
}
|
||||
|
||||
var migrations []migration
|
||||
|
||||
// registerMigration is only supposed to be call from init(), which runs sequentially
|
||||
// so we don't need ot protect migrations with a lock
|
||||
func registerMigration(m migration) {
|
||||
migrations = append(migrations, m)
|
||||
}
|
||||
|
||||
func New(logger zerolog.Logger,
|
||||
gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient],
|
||||
storage metadata.Storage,
|
||||
cfg MigrationConfig,
|
||||
manager share.Manager,
|
||||
loader share.LoadableManager,
|
||||
) Migrations {
|
||||
|
||||
slices.SortFunc(migrations, func(a, b migration) int {
|
||||
return cmp.Compare(a.Version(), b.Version())
|
||||
})
|
||||
|
||||
b := make([]byte, 8)
|
||||
_, _ = rand.Read(b)
|
||||
instanceID := fmt.Sprintf("%x", b)
|
||||
|
||||
return Migrations{
|
||||
config{
|
||||
logger: logger.With().Str("jsoncs3", "migrations").Logger(),
|
||||
gatewaySelector: gatewaySelector,
|
||||
storage: storage,
|
||||
serviceAccountID: cfg.ServiceAccountID,
|
||||
serviceAccountSecret: cfg.ServiceAccountSecret,
|
||||
providerRegistryAddr: cfg.ProviderRegistryAddr,
|
||||
manager: manager,
|
||||
loader: loader,
|
||||
},
|
||||
state{},
|
||||
instanceID,
|
||||
}
|
||||
}
|
||||
|
||||
// acquireLock tries to atomically create the lock file, blocking until the lock
|
||||
// is obtained. It returns the etag of the lock file on success. It retries
|
||||
// indefinitely until ctx is cancelled. A lock whose timestamp is older than
|
||||
// lockTTL is considered stale and will be taken over.
|
||||
func (m *Migrations) acquireLock(ctx context.Context) (string, error) {
|
||||
m.logger.Debug().Str("instance", m.instanceID).Msg("acquiring migration lock")
|
||||
for {
|
||||
// Fast path: create the lock file only if it does not exist yet.
|
||||
data, err := json.Marshal(lockData{Timestamp: time.Now(), InstanceID: m.instanceID})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res, err := m.storage.Upload(ctx, metadata.UploadRequest{
|
||||
Path: lockFile,
|
||||
Content: data,
|
||||
IfNoneMatch: []string{"*"},
|
||||
})
|
||||
if err == nil {
|
||||
m.logger.Debug().Str("instance", m.instanceID).Msg("migration lock acquired")
|
||||
return res.Etag, nil
|
||||
}
|
||||
|
||||
// Propagate context cancellation immediately.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// Any error other than a conflict means something unexpected happened.
|
||||
if !isConflict(err) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Lock file already exists — read it to decide whether it is stale.
|
||||
dl, err := m.storage.Download(ctx, metadata.DownloadRequest{Path: lockFile})
|
||||
if err != nil {
|
||||
if _, ok := err.(errtypes.IsNotFound); ok {
|
||||
// Lock was released between our upload attempt and the download;
|
||||
// retry acquiring it immediately.
|
||||
m.logger.Debug().Str("instance", m.instanceID).Msg("migration lock vanished during read; retrying")
|
||||
continue
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
var existing lockData
|
||||
stale := true
|
||||
if err := json.Unmarshal(dl.Content, &existing); err == nil {
|
||||
stale = time.Since(existing.Timestamp) > lockTTL
|
||||
}
|
||||
|
||||
if stale {
|
||||
m.logger.Debug().
|
||||
Str("instance", m.instanceID).
|
||||
Str("held_by", existing.InstanceID).
|
||||
Time("lock_timestamp", existing.Timestamp).
|
||||
Msg("migration lock is stale; attempting takeover")
|
||||
|
||||
// Atomically take over the stale lock using the etag we just read.
|
||||
newData, err := json.Marshal(lockData{Timestamp: time.Now(), InstanceID: m.instanceID})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res, err := m.storage.Upload(ctx, metadata.UploadRequest{
|
||||
Path: lockFile,
|
||||
Content: newData,
|
||||
IfMatchEtag: dl.Etag,
|
||||
})
|
||||
if err == nil {
|
||||
m.logger.Debug().Str("instance", m.instanceID).Msg("migration lock acquired via stale takeover")
|
||||
return res.Etag, nil
|
||||
}
|
||||
// Another instance took the stale lock before us; loop and retry.
|
||||
m.logger.Debug().Str("instance", m.instanceID).Err(err).Msg("stale lock takeover lost race; retrying")
|
||||
continue
|
||||
}
|
||||
|
||||
m.logger.Debug().
|
||||
Str("instance", m.instanceID).
|
||||
Str("held_by", existing.InstanceID).
|
||||
Time("lock_timestamp", existing.Timestamp).
|
||||
Dur("poll_interval", lockPollInterval).
|
||||
Msg("migration lock held by another instance; waiting")
|
||||
|
||||
// Lock is fresh and held by another instance; wait before retrying.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
case <-time.After(lockPollInterval):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startHeartbeat spawns a goroutine that periodically renews the lock file so
|
||||
// that it is not considered stale while a long migration is running. Call the
|
||||
// returned cancel function to stop the heartbeat.
|
||||
func (m *Migrations) startHeartbeat(ctx context.Context, etag string) context.CancelFunc {
|
||||
hbCtx, cancel := context.WithCancel(ctx)
|
||||
go func() {
|
||||
ticker := time.NewTicker(lockHeartbeatInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-hbCtx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
data, err := json.Marshal(lockData{Timestamp: time.Now(), InstanceID: m.instanceID})
|
||||
if err != nil {
|
||||
m.logger.Warn().Err(err).Msg("failed to marshal heartbeat data for migration lock")
|
||||
return
|
||||
}
|
||||
res, err := m.storage.Upload(hbCtx, metadata.UploadRequest{
|
||||
Path: lockFile,
|
||||
Content: data,
|
||||
IfMatchEtag: etag,
|
||||
})
|
||||
if err != nil {
|
||||
m.logger.Warn().Err(err).Msg("failed to renew migration lock; another instance may take over")
|
||||
return
|
||||
}
|
||||
etag = res.Etag
|
||||
}
|
||||
}
|
||||
}()
|
||||
return cancel
|
||||
}
|
||||
|
||||
// releaseLock deletes the lock file unconditionally.
|
||||
func (m *Migrations) releaseLock(ctx context.Context) {
|
||||
if err := m.storage.Delete(ctx, lockFile); err != nil {
|
||||
m.logger.Warn().Err(err).Msg("failed to release migration lock")
|
||||
}
|
||||
}
|
||||
|
||||
// isConflict returns true for errors that signal a conditional-upload conflict,
|
||||
// i.e. the lock file already exists or the etag did not match.
|
||||
func isConflict(err error) bool {
|
||||
switch err.(type) {
|
||||
case errtypes.IsAlreadyExists, errtypes.IsAborted, errtypes.IsPreconditionFailed:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// loadState reads the persisted migration version from storage. If no state
|
||||
// file exists yet (fresh deployment) it returns version 0 without error.
|
||||
func (m *Migrations) loadState(ctx context.Context) error {
|
||||
data, err := m.storage.SimpleDownload(ctx, stateFile)
|
||||
if err != nil {
|
||||
if _, ok := err.(errtypes.IsNotFound); ok {
|
||||
m.state = state{version: 0}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
var ps persistedState
|
||||
if err := json.Unmarshal(data, &ps); err != nil {
|
||||
return err
|
||||
}
|
||||
m.state = state{version: ps.Version}
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveState writes the current migration version to storage so that already-
|
||||
// applied migrations are not re-run on the next server start.
|
||||
func (m *Migrations) saveState(ctx context.Context) error {
|
||||
data, err := json.Marshal(persistedState{Version: m.state.version})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.storage.SimpleUpload(ctx, stateFile, data)
|
||||
}
|
||||
|
||||
func (m *Migrations) RunMigrations() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
etag, err := m.acquireLock(ctx)
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msg("failed to acquire migration lock; skipping migrations")
|
||||
return
|
||||
}
|
||||
cancelHB := m.startHeartbeat(ctx, etag)
|
||||
defer cancelHB()
|
||||
defer m.releaseLock(ctx)
|
||||
|
||||
if err := m.loadState(ctx); err != nil {
|
||||
m.logger.Error().Err(err).Msg("failed to load migration state; skipping migrations")
|
||||
return
|
||||
}
|
||||
|
||||
m.logger.Info().Int("current state", m.state.version).Msg("checking migrations")
|
||||
|
||||
for _, mig := range migrations {
|
||||
if mig.Version() > m.state.version {
|
||||
m.logger.Info().Str("migration", mig.Name()).Int("version", mig.Version()).Msg("running migration")
|
||||
mig.Initialize(m.config)
|
||||
if err := mig.Migrate(); err != nil {
|
||||
m.logger.Error().Err(err).Str("migration", mig.Name()).Msg("migration failed; stopping")
|
||||
return
|
||||
}
|
||||
m.state.version = mig.Version()
|
||||
if err := m.saveState(ctx); err != nil {
|
||||
m.logger.Error().Err(err).Msg("failed to save migration state; stopping")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
m.logger.Info().Str("migration", mig.Name()).Int("version", mig.Version()).Msg("skipping migration")
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-6
@@ -28,6 +28,7 @@ import (
|
||||
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/genproto/protobuf/field_mask"
|
||||
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
@@ -46,7 +47,7 @@ func init() {
|
||||
}
|
||||
|
||||
// New returns a new manager.
|
||||
func New(c map[string]interface{}) (share.Manager, error) {
|
||||
func New(c map[string]any, _ *zerolog.Logger) (share.Manager, error) {
|
||||
state := map[string]map[*collaboration.ShareId]collaboration.ShareState{}
|
||||
mp := map[string]map[*collaboration.ShareId]*provider.Reference{}
|
||||
return &manager{
|
||||
@@ -82,11 +83,6 @@ func (m *manager) Share(ctx context.Context, md *provider.ResourceInfo, g *colla
|
||||
Nanos: uint32(now % 1000000000),
|
||||
}
|
||||
|
||||
if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER &&
|
||||
(utils.UserEqual(g.Grantee.GetUserId(), user.Id) || utils.UserEqual(g.Grantee.GetUserId(), md.Owner)) {
|
||||
return nil, errtypes.BadRequest("memory: owner/creator and grantee are the same")
|
||||
}
|
||||
|
||||
// check if share already exists.
|
||||
key := &collaboration.ShareKey{
|
||||
Owner: md.Owner,
|
||||
|
||||
+5
-2
@@ -18,11 +18,14 @@
|
||||
|
||||
package registry
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// NewFunc is the function that share managers
|
||||
// should register at init time.
|
||||
type NewFunc func(map[string]interface{}) (share.Manager, error)
|
||||
type NewFunc func(map[string]any, *zerolog.Logger) (share.Manager, error)
|
||||
|
||||
// NewFuncs is a map containing all the registered share managers.
|
||||
var NewFuncs = map[string]NewFunc{}
|
||||
|
||||
+15
@@ -136,6 +136,18 @@ func StateFilter(state collaboration.ShareState) *collaboration.Filter {
|
||||
}
|
||||
}
|
||||
|
||||
// SpaceRootFilter is an abstraction for filtering shares by whether the shared
|
||||
// resource is a space root. Pass true to include only space-root shares (space
|
||||
// membership), false to exclude them (file/folder shares only).
|
||||
func SpaceRootFilter(spaceRoot bool) *collaboration.Filter {
|
||||
return &collaboration.Filter{
|
||||
Type: collaboration.Filter_TYPE_SPACE_ROOT,
|
||||
Term: &collaboration.Filter_SpaceRoot{
|
||||
SpaceRoot: spaceRoot,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// IsCreatedByUser checks if the user is the owner or creator of the share.
|
||||
func IsCreatedByUser(share *collaboration.Share, user *userv1beta1.User) bool {
|
||||
return utils.UserEqual(user.Id, share.Owner) || utils.UserEqual(user.Id, share.Creator)
|
||||
@@ -172,6 +184,9 @@ func MatchesFilter(share *collaboration.Share, state collaboration.ShareState, f
|
||||
return share.ResourceId.SpaceId == filter.GetSpaceId()
|
||||
case collaboration.Filter_TYPE_STATE:
|
||||
return state == filter.GetState()
|
||||
case collaboration.Filter_TYPE_SPACE_ROOT:
|
||||
isSpaceRoot := share.ResourceId.SpaceId == share.ResourceId.OpaqueId
|
||||
return isSpaceRoot == filter.GetSpaceRoot()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
Generated
Vendored
+1
@@ -101,6 +101,7 @@ func ServiceAccountPermissions() *provider.ResourcePermissions {
|
||||
Delete: true, // for cli restore command with replace option
|
||||
CreateContainer: true, // for space provisioning
|
||||
AddGrant: true, // for initial project space member assignment
|
||||
ListGrants: true, // for initial project space member assignment
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+38
-1
@@ -93,6 +93,40 @@ func (disk *Disk) SimpleUpload(ctx context.Context, uploadpath string, content [
|
||||
// Upload stores a file on disk
|
||||
func (disk *Disk) Upload(_ context.Context, req UploadRequest) (*UploadResponse, error) {
|
||||
p := disk.targetPath(req.Path)
|
||||
|
||||
// IfNoneMatch: ["*"] means create the file only if it does not already
|
||||
// exist. Use O_EXCL so the check and the create are atomic on the local
|
||||
// filesystem.
|
||||
for _, tag := range req.IfNoneMatch {
|
||||
if tag != "*" {
|
||||
continue
|
||||
}
|
||||
f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
return nil, errtypes.AlreadyExists(p)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if _, err := f.Write(req.Content); err != nil {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &UploadResponse{}
|
||||
res.Etag, err = calcEtag(info.ModTime(), info.Size())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
if req.IfMatchEtag != "" {
|
||||
info, err := os.Stat(p)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
@@ -170,7 +204,10 @@ func (disk *Disk) Download(_ context.Context, req DownloadRequest) (*DownloadRes
|
||||
// SimpleDownload reads a file from disk
|
||||
func (disk *Disk) SimpleDownload(ctx context.Context, downloadpath string) ([]byte, error) {
|
||||
res, err := disk.Download(ctx, DownloadRequest{Path: downloadpath})
|
||||
return res.Content, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.Content, nil
|
||||
}
|
||||
|
||||
// Delete deletes a path
|
||||
|
||||
+10
@@ -470,6 +470,16 @@ func ExistsInOpaque(o *types.Opaque, key string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// SpaceGrantOpaque returns an Opaque with the "spacegrant" key set, which
|
||||
// signals to storage and event middleware that a grant targets a space root.
|
||||
func SpaceGrantOpaque() *types.Opaque {
|
||||
return &types.Opaque{
|
||||
Map: map[string]*types.OpaqueEntry{
|
||||
"spacegrant": {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MergeOpaques will merge the opaques. If a key exists in both opaques
|
||||
// the values from the first opaque will be taken
|
||||
func MergeOpaques(o *types.Opaque, p *types.Opaque) *types.Opaque {
|
||||
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
// Copyright 2018-2022 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
// Code generated by mockery v2.53.2. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
grpc "google.golang.org/grpc"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
)
|
||||
|
||||
// SpacesAPIClient is an autogenerated mock type for the SpacesAPIClient type
|
||||
type SpacesAPIClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type SpacesAPIClient_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *SpacesAPIClient) EXPECT() *SpacesAPIClient_Expecter {
|
||||
return &SpacesAPIClient_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// CreateStorageSpace provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *SpacesAPIClient) CreateStorageSpace(ctx context.Context, in *providerv1beta1.CreateStorageSpaceRequest, opts ...grpc.CallOption) (*providerv1beta1.CreateStorageSpaceResponse, error) {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _m.Called(ctx, in, opts)
|
||||
} else {
|
||||
tmpRet = _m.Called(ctx, in)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CreateStorageSpace")
|
||||
}
|
||||
|
||||
var r0 *providerv1beta1.CreateStorageSpaceResponse
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.CreateStorageSpaceRequest, ...grpc.CallOption) (*providerv1beta1.CreateStorageSpaceResponse, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.CreateStorageSpaceRequest, ...grpc.CallOption) *providerv1beta1.CreateStorageSpaceResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*providerv1beta1.CreateStorageSpaceResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.CreateStorageSpaceRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SpacesAPIClient_CreateStorageSpace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateStorageSpace'
|
||||
type SpacesAPIClient_CreateStorageSpace_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// CreateStorageSpace is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *providerv1beta1.CreateStorageSpaceRequest
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *SpacesAPIClient_Expecter) CreateStorageSpace(ctx interface{}, in interface{}, opts ...interface{}) *SpacesAPIClient_CreateStorageSpace_Call {
|
||||
return &SpacesAPIClient_CreateStorageSpace_Call{Call: _e.mock.On("CreateStorageSpace",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *SpacesAPIClient_CreateStorageSpace_Call) Run(run func(ctx context.Context, in *providerv1beta1.CreateStorageSpaceRequest, opts ...grpc.CallOption)) *SpacesAPIClient_CreateStorageSpace_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]grpc.CallOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(grpc.CallOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(*providerv1beta1.CreateStorageSpaceRequest), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SpacesAPIClient_CreateStorageSpace_Call) Return(_a0 *providerv1beta1.CreateStorageSpaceResponse, _a1 error) *SpacesAPIClient_CreateStorageSpace_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SpacesAPIClient_CreateStorageSpace_Call) RunAndReturn(run func(context.Context, *providerv1beta1.CreateStorageSpaceRequest, ...grpc.CallOption) (*providerv1beta1.CreateStorageSpaceResponse, error)) *SpacesAPIClient_CreateStorageSpace_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// DeleteStorageSpace provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *SpacesAPIClient) DeleteStorageSpace(ctx context.Context, in *providerv1beta1.DeleteStorageSpaceRequest, opts ...grpc.CallOption) (*providerv1beta1.DeleteStorageSpaceResponse, error) {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _m.Called(ctx, in, opts)
|
||||
} else {
|
||||
tmpRet = _m.Called(ctx, in)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteStorageSpace")
|
||||
}
|
||||
|
||||
var r0 *providerv1beta1.DeleteStorageSpaceResponse
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.DeleteStorageSpaceRequest, ...grpc.CallOption) (*providerv1beta1.DeleteStorageSpaceResponse, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.DeleteStorageSpaceRequest, ...grpc.CallOption) *providerv1beta1.DeleteStorageSpaceResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*providerv1beta1.DeleteStorageSpaceResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.DeleteStorageSpaceRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SpacesAPIClient_DeleteStorageSpace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteStorageSpace'
|
||||
type SpacesAPIClient_DeleteStorageSpace_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// DeleteStorageSpace is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *providerv1beta1.DeleteStorageSpaceRequest
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *SpacesAPIClient_Expecter) DeleteStorageSpace(ctx interface{}, in interface{}, opts ...interface{}) *SpacesAPIClient_DeleteStorageSpace_Call {
|
||||
return &SpacesAPIClient_DeleteStorageSpace_Call{Call: _e.mock.On("DeleteStorageSpace",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *SpacesAPIClient_DeleteStorageSpace_Call) Run(run func(ctx context.Context, in *providerv1beta1.DeleteStorageSpaceRequest, opts ...grpc.CallOption)) *SpacesAPIClient_DeleteStorageSpace_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]grpc.CallOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(grpc.CallOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(*providerv1beta1.DeleteStorageSpaceRequest), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SpacesAPIClient_DeleteStorageSpace_Call) Return(_a0 *providerv1beta1.DeleteStorageSpaceResponse, _a1 error) *SpacesAPIClient_DeleteStorageSpace_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SpacesAPIClient_DeleteStorageSpace_Call) RunAndReturn(run func(context.Context, *providerv1beta1.DeleteStorageSpaceRequest, ...grpc.CallOption) (*providerv1beta1.DeleteStorageSpaceResponse, error)) *SpacesAPIClient_DeleteStorageSpace_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListStorageSpaces provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *SpacesAPIClient) ListStorageSpaces(ctx context.Context, in *providerv1beta1.ListStorageSpacesRequest, opts ...grpc.CallOption) (*providerv1beta1.ListStorageSpacesResponse, error) {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _m.Called(ctx, in, opts)
|
||||
} else {
|
||||
tmpRet = _m.Called(ctx, in)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListStorageSpaces")
|
||||
}
|
||||
|
||||
var r0 *providerv1beta1.ListStorageSpacesResponse
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ListStorageSpacesRequest, ...grpc.CallOption) (*providerv1beta1.ListStorageSpacesResponse, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ListStorageSpacesRequest, ...grpc.CallOption) *providerv1beta1.ListStorageSpacesResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*providerv1beta1.ListStorageSpacesResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ListStorageSpacesRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SpacesAPIClient_ListStorageSpaces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListStorageSpaces'
|
||||
type SpacesAPIClient_ListStorageSpaces_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListStorageSpaces is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *providerv1beta1.ListStorageSpacesRequest
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *SpacesAPIClient_Expecter) ListStorageSpaces(ctx interface{}, in interface{}, opts ...interface{}) *SpacesAPIClient_ListStorageSpaces_Call {
|
||||
return &SpacesAPIClient_ListStorageSpaces_Call{Call: _e.mock.On("ListStorageSpaces",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *SpacesAPIClient_ListStorageSpaces_Call) Run(run func(ctx context.Context, in *providerv1beta1.ListStorageSpacesRequest, opts ...grpc.CallOption)) *SpacesAPIClient_ListStorageSpaces_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]grpc.CallOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(grpc.CallOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(*providerv1beta1.ListStorageSpacesRequest), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SpacesAPIClient_ListStorageSpaces_Call) Return(_a0 *providerv1beta1.ListStorageSpacesResponse, _a1 error) *SpacesAPIClient_ListStorageSpaces_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SpacesAPIClient_ListStorageSpaces_Call) RunAndReturn(run func(context.Context, *providerv1beta1.ListStorageSpacesRequest, ...grpc.CallOption) (*providerv1beta1.ListStorageSpacesResponse, error)) *SpacesAPIClient_ListStorageSpaces_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateStorageSpace provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *SpacesAPIClient) UpdateStorageSpace(ctx context.Context, in *providerv1beta1.UpdateStorageSpaceRequest, opts ...grpc.CallOption) (*providerv1beta1.UpdateStorageSpaceResponse, error) {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _m.Called(ctx, in, opts)
|
||||
} else {
|
||||
tmpRet = _m.Called(ctx, in)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateStorageSpace")
|
||||
}
|
||||
|
||||
var r0 *providerv1beta1.UpdateStorageSpaceResponse
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.UpdateStorageSpaceRequest, ...grpc.CallOption) (*providerv1beta1.UpdateStorageSpaceResponse, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.UpdateStorageSpaceRequest, ...grpc.CallOption) *providerv1beta1.UpdateStorageSpaceResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*providerv1beta1.UpdateStorageSpaceResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.UpdateStorageSpaceRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SpacesAPIClient_UpdateStorageSpace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateStorageSpace'
|
||||
type SpacesAPIClient_UpdateStorageSpace_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateStorageSpace is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *providerv1beta1.UpdateStorageSpaceRequest
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *SpacesAPIClient_Expecter) UpdateStorageSpace(ctx interface{}, in interface{}, opts ...interface{}) *SpacesAPIClient_UpdateStorageSpace_Call {
|
||||
return &SpacesAPIClient_UpdateStorageSpace_Call{Call: _e.mock.On("UpdateStorageSpace",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *SpacesAPIClient_UpdateStorageSpace_Call) Run(run func(ctx context.Context, in *providerv1beta1.UpdateStorageSpaceRequest, opts ...grpc.CallOption)) *SpacesAPIClient_UpdateStorageSpace_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]grpc.CallOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(grpc.CallOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(*providerv1beta1.UpdateStorageSpaceRequest), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SpacesAPIClient_UpdateStorageSpace_Call) Return(_a0 *providerv1beta1.UpdateStorageSpaceResponse, _a1 error) *SpacesAPIClient_UpdateStorageSpace_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SpacesAPIClient_UpdateStorageSpace_Call) RunAndReturn(run func(context.Context, *providerv1beta1.UpdateStorageSpaceRequest, ...grpc.CallOption) (*providerv1beta1.UpdateStorageSpaceResponse, error)) *SpacesAPIClient_UpdateStorageSpace_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewSpacesAPIClient creates a new instance of SpacesAPIClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewSpacesAPIClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *SpacesAPIClient {
|
||||
mock := &SpacesAPIClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
Generated
Vendored
+277
@@ -0,0 +1,277 @@
|
||||
// Copyright 2018-2022 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
// Code generated by mockery v2.53.2. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
grpc "google.golang.org/grpc"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
registryv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
|
||||
)
|
||||
|
||||
// StorageRegistryAPIClient is an autogenerated mock type for the StorageRegistryAPIClient type
|
||||
type StorageRegistryAPIClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type StorageRegistryAPIClient_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *StorageRegistryAPIClient) EXPECT() *StorageRegistryAPIClient_Expecter {
|
||||
return &StorageRegistryAPIClient_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// GetHome provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *StorageRegistryAPIClient) GetHome(ctx context.Context, in *registryv1beta1.GetHomeRequest, opts ...grpc.CallOption) (*registryv1beta1.GetHomeResponse, error) {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _m.Called(ctx, in, opts)
|
||||
} else {
|
||||
tmpRet = _m.Called(ctx, in)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetHome")
|
||||
}
|
||||
|
||||
var r0 *registryv1beta1.GetHomeResponse
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *registryv1beta1.GetHomeRequest, ...grpc.CallOption) (*registryv1beta1.GetHomeResponse, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *registryv1beta1.GetHomeRequest, ...grpc.CallOption) *registryv1beta1.GetHomeResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*registryv1beta1.GetHomeResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *registryv1beta1.GetHomeRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// StorageRegistryAPIClient_GetHome_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetHome'
|
||||
type StorageRegistryAPIClient_GetHome_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetHome is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *registryv1beta1.GetHomeRequest
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *StorageRegistryAPIClient_Expecter) GetHome(ctx interface{}, in interface{}, opts ...interface{}) *StorageRegistryAPIClient_GetHome_Call {
|
||||
return &StorageRegistryAPIClient_GetHome_Call{Call: _e.mock.On("GetHome",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *StorageRegistryAPIClient_GetHome_Call) Run(run func(ctx context.Context, in *registryv1beta1.GetHomeRequest, opts ...grpc.CallOption)) *StorageRegistryAPIClient_GetHome_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]grpc.CallOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(grpc.CallOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(*registryv1beta1.GetHomeRequest), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *StorageRegistryAPIClient_GetHome_Call) Return(_a0 *registryv1beta1.GetHomeResponse, _a1 error) *StorageRegistryAPIClient_GetHome_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *StorageRegistryAPIClient_GetHome_Call) RunAndReturn(run func(context.Context, *registryv1beta1.GetHomeRequest, ...grpc.CallOption) (*registryv1beta1.GetHomeResponse, error)) *StorageRegistryAPIClient_GetHome_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetStorageProviders provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *StorageRegistryAPIClient) GetStorageProviders(ctx context.Context, in *registryv1beta1.GetStorageProvidersRequest, opts ...grpc.CallOption) (*registryv1beta1.GetStorageProvidersResponse, error) {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _m.Called(ctx, in, opts)
|
||||
} else {
|
||||
tmpRet = _m.Called(ctx, in)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetStorageProviders")
|
||||
}
|
||||
|
||||
var r0 *registryv1beta1.GetStorageProvidersResponse
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *registryv1beta1.GetStorageProvidersRequest, ...grpc.CallOption) (*registryv1beta1.GetStorageProvidersResponse, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *registryv1beta1.GetStorageProvidersRequest, ...grpc.CallOption) *registryv1beta1.GetStorageProvidersResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*registryv1beta1.GetStorageProvidersResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *registryv1beta1.GetStorageProvidersRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// StorageRegistryAPIClient_GetStorageProviders_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetStorageProviders'
|
||||
type StorageRegistryAPIClient_GetStorageProviders_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetStorageProviders is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *registryv1beta1.GetStorageProvidersRequest
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *StorageRegistryAPIClient_Expecter) GetStorageProviders(ctx interface{}, in interface{}, opts ...interface{}) *StorageRegistryAPIClient_GetStorageProviders_Call {
|
||||
return &StorageRegistryAPIClient_GetStorageProviders_Call{Call: _e.mock.On("GetStorageProviders",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *StorageRegistryAPIClient_GetStorageProviders_Call) Run(run func(ctx context.Context, in *registryv1beta1.GetStorageProvidersRequest, opts ...grpc.CallOption)) *StorageRegistryAPIClient_GetStorageProviders_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]grpc.CallOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(grpc.CallOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(*registryv1beta1.GetStorageProvidersRequest), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *StorageRegistryAPIClient_GetStorageProviders_Call) Return(_a0 *registryv1beta1.GetStorageProvidersResponse, _a1 error) *StorageRegistryAPIClient_GetStorageProviders_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *StorageRegistryAPIClient_GetStorageProviders_Call) RunAndReturn(run func(context.Context, *registryv1beta1.GetStorageProvidersRequest, ...grpc.CallOption) (*registryv1beta1.GetStorageProvidersResponse, error)) *StorageRegistryAPIClient_GetStorageProviders_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListStorageProviders provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *StorageRegistryAPIClient) ListStorageProviders(ctx context.Context, in *registryv1beta1.ListStorageProvidersRequest, opts ...grpc.CallOption) (*registryv1beta1.ListStorageProvidersResponse, error) {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _m.Called(ctx, in, opts)
|
||||
} else {
|
||||
tmpRet = _m.Called(ctx, in)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListStorageProviders")
|
||||
}
|
||||
|
||||
var r0 *registryv1beta1.ListStorageProvidersResponse
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *registryv1beta1.ListStorageProvidersRequest, ...grpc.CallOption) (*registryv1beta1.ListStorageProvidersResponse, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *registryv1beta1.ListStorageProvidersRequest, ...grpc.CallOption) *registryv1beta1.ListStorageProvidersResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*registryv1beta1.ListStorageProvidersResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *registryv1beta1.ListStorageProvidersRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// StorageRegistryAPIClient_ListStorageProviders_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListStorageProviders'
|
||||
type StorageRegistryAPIClient_ListStorageProviders_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListStorageProviders is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *registryv1beta1.ListStorageProvidersRequest
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *StorageRegistryAPIClient_Expecter) ListStorageProviders(ctx interface{}, in interface{}, opts ...interface{}) *StorageRegistryAPIClient_ListStorageProviders_Call {
|
||||
return &StorageRegistryAPIClient_ListStorageProviders_Call{Call: _e.mock.On("ListStorageProviders",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *StorageRegistryAPIClient_ListStorageProviders_Call) Run(run func(ctx context.Context, in *registryv1beta1.ListStorageProvidersRequest, opts ...grpc.CallOption)) *StorageRegistryAPIClient_ListStorageProviders_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]grpc.CallOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(grpc.CallOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(*registryv1beta1.ListStorageProvidersRequest), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *StorageRegistryAPIClient_ListStorageProviders_Call) Return(_a0 *registryv1beta1.ListStorageProvidersResponse, _a1 error) *StorageRegistryAPIClient_ListStorageProviders_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *StorageRegistryAPIClient_ListStorageProviders_Call) RunAndReturn(run func(context.Context, *registryv1beta1.ListStorageProvidersRequest, ...grpc.CallOption) (*registryv1beta1.ListStorageProvidersResponse, error)) *StorageRegistryAPIClient_ListStorageProviders_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewStorageRegistryAPIClient creates a new instance of StorageRegistryAPIClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewStorageRegistryAPIClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *StorageRegistryAPIClient {
|
||||
mock := &StorageRegistryAPIClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.23@sha256:51a6466e8dbf3e00e422eb0f7a97ac450b2d57b33617bbe8d2ee0bddcd9d0d37
|
||||
FROM golang:1.25@sha256:31c1e53dfc1cc2d269deec9c83f58729fa3c53dc9a576f6426109d1e319e9e9a
|
||||
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=arm
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.23@sha256:51a6466e8dbf3e00e422eb0f7a97ac450b2d57b33617bbe8d2ee0bddcd9d0d37
|
||||
FROM golang:1.25@sha256:31c1e53dfc1cc2d269deec9c83f58729fa3c53dc9a576f6426109d1e319e9e9a
|
||||
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=arm64
|
||||
|
||||
+2
-5
@@ -4,7 +4,7 @@ export CGO_ENABLED := 1
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test ./...
|
||||
go test -race -timeout 15s ./...
|
||||
|
||||
.PHONY: bench
|
||||
bench:
|
||||
@@ -31,9 +31,6 @@ build-nocgo:
|
||||
# Run cross-compilation to assure supported architectures.
|
||||
cross-build: build-arm build-arm64 build-nocgo
|
||||
|
||||
generate:
|
||||
go generate -x ./...
|
||||
|
||||
verify: generate
|
||||
verify:
|
||||
git diff --exit-code
|
||||
go vet ./...
|
||||
|
||||
+1
-2
@@ -6,8 +6,7 @@ collision attacks.
|
||||
The `cgo/lib` code is a carbon copy of the [original code], based on
|
||||
the award winning [white paper] by Marc Stevens.
|
||||
|
||||
The Go implementation is largely based off Go's generic sha1.
|
||||
At present no SIMD optimisations have been implemented.
|
||||
The Go and native implementations are largely based off upstream Go.
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
+15
-32
@@ -12,7 +12,6 @@ package sha1cd
|
||||
// Original: https://github.com/golang/go/blob/master/src/crypto/sha1/sha1.go
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"hash"
|
||||
@@ -20,12 +19,6 @@ import (
|
||||
shared "github.com/pjbgf/sha1cd/internal"
|
||||
)
|
||||
|
||||
//go:generate go run -C asm . -out ../sha1cdblock_amd64.s -pkg $GOPACKAGE
|
||||
|
||||
func init() {
|
||||
crypto.RegisterHash(crypto.SHA1, New)
|
||||
}
|
||||
|
||||
// The size of a SHA-1 checksum in bytes.
|
||||
const Size = shared.Size
|
||||
|
||||
@@ -40,8 +33,7 @@ type digest struct {
|
||||
len uint64
|
||||
|
||||
// col defines whether a collision has been found.
|
||||
col bool
|
||||
blockFunc func(dig *digest, p []byte)
|
||||
col bool
|
||||
}
|
||||
|
||||
func (d *digest) MarshalBinary() ([]byte, error) {
|
||||
@@ -101,7 +93,7 @@ func (d *digest) UnmarshalBinary(b []byte) error {
|
||||
|
||||
func consumeUint64(b []byte) ([]byte, uint64) {
|
||||
_ = b[7]
|
||||
x := uint64(b[7]) | uint64(b[6])<<8 | uint64(b[shared.WordBuffers])<<16 | uint64(b[4])<<24 |
|
||||
x := uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
|
||||
uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
|
||||
return b[8:], x
|
||||
}
|
||||
@@ -128,21 +120,9 @@ func (d *digest) Reset() {
|
||||
// implements encoding.BinaryMarshaler and encoding.BinaryUnmarshaler to
|
||||
// marshal and unmarshal the internal state of the hash.
|
||||
func New() hash.Hash {
|
||||
d := new(digest)
|
||||
|
||||
d.blockFunc = block
|
||||
var d digest
|
||||
d.Reset()
|
||||
return d
|
||||
}
|
||||
|
||||
// NewGeneric is equivalent to New but uses the Go generic implementation,
|
||||
// avoiding any processor-specific optimizations.
|
||||
func NewGeneric() hash.Hash {
|
||||
d := new(digest)
|
||||
|
||||
d.blockFunc = blockGeneric
|
||||
d.Reset()
|
||||
return d
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d *digest) Size() int { return Size }
|
||||
@@ -160,14 +140,14 @@ func (d *digest) Write(p []byte) (nn int, err error) {
|
||||
n := copy(d.x[d.nx:], p)
|
||||
d.nx += n
|
||||
if d.nx == shared.Chunk {
|
||||
d.blockFunc(d, d.x[:])
|
||||
block(d, d.x[:])
|
||||
d.nx = 0
|
||||
}
|
||||
p = p[n:]
|
||||
}
|
||||
if len(p) >= shared.Chunk {
|
||||
n := len(p) &^ (shared.Chunk - 1)
|
||||
d.blockFunc(d, p[:n])
|
||||
block(d, p[:n])
|
||||
p = p[n:]
|
||||
}
|
||||
if len(p) > 0 {
|
||||
@@ -186,18 +166,20 @@ func (d *digest) Sum(in []byte) []byte {
|
||||
func (d *digest) checkSum() [Size]byte {
|
||||
len := d.len
|
||||
// Padding. Add a 1 bit and 0 bits until 56 bytes mod 64.
|
||||
var tmp [64]byte
|
||||
var tmp [64 + 8]byte
|
||||
tmp[0] = 0x80
|
||||
var t uint64
|
||||
if len%64 < 56 {
|
||||
d.Write(tmp[0 : 56-len%64])
|
||||
t = 56 - len%64
|
||||
} else {
|
||||
d.Write(tmp[0 : 64+56-len%64])
|
||||
t = 64 + 56 - len%64
|
||||
}
|
||||
|
||||
// Length in bits.
|
||||
len <<= 3
|
||||
binary.BigEndian.PutUint64(tmp[:], len)
|
||||
d.Write(tmp[0:8])
|
||||
padlen := tmp[:t+8]
|
||||
binary.BigEndian.PutUint64(tmp[t:], len)
|
||||
d.Write(padlen)
|
||||
|
||||
if d.nx != 0 {
|
||||
panic("d.nx != 0")
|
||||
@@ -216,7 +198,8 @@ func (d *digest) checkSum() [Size]byte {
|
||||
|
||||
// Sum returns the SHA-1 checksum of the data.
|
||||
func Sum(data []byte) ([Size]byte, bool) {
|
||||
d := New().(*digest)
|
||||
var d digest
|
||||
d.Reset()
|
||||
d.Write(data)
|
||||
return d.checkSum(), d.col
|
||||
}
|
||||
|
||||
+24
-22
@@ -1,48 +1,50 @@
|
||||
//go:build !noasm && gc && amd64
|
||||
// +build !noasm,gc,amd64
|
||||
//go:build !noasm && gc && amd64 && !arm64
|
||||
// +build !noasm,gc,amd64,!arm64
|
||||
|
||||
package sha1cd
|
||||
|
||||
import (
|
||||
"math"
|
||||
"unsafe"
|
||||
"runtime"
|
||||
|
||||
"github.com/klauspost/cpuid/v2"
|
||||
shared "github.com/pjbgf/sha1cd/internal"
|
||||
)
|
||||
|
||||
type sliceHeader struct {
|
||||
base uintptr
|
||||
len int
|
||||
cap int
|
||||
}
|
||||
var hasSHANI = (runtime.GOARCH == "amd64" &&
|
||||
cpuid.CPU.Supports(cpuid.AVX) &&
|
||||
cpuid.CPU.Supports(cpuid.SHA) &&
|
||||
cpuid.CPU.Supports(cpuid.SSE3) &&
|
||||
cpuid.CPU.Supports(cpuid.SSE4))
|
||||
|
||||
// blockAMD64 hashes the message p into the current state in dig.
|
||||
// blockAMD64 hashes the message p into the current state in h.
|
||||
// Both m1 and cs are used to store intermediate results which are used by the collision detection logic.
|
||||
//
|
||||
//go:noescape
|
||||
func blockAMD64(dig *digest, p sliceHeader, m1 []uint32, cs [][5]uint32)
|
||||
func blockAMD64(h []uint32, p []byte, m1 []uint32, cs [][5]uint32)
|
||||
|
||||
func block(dig *digest, p []byte) {
|
||||
if forceGeneric || !hasSHANI {
|
||||
blockGeneric(dig, p)
|
||||
return
|
||||
}
|
||||
|
||||
m1 := [shared.Rounds]uint32{}
|
||||
cs := [shared.PreStepState][shared.WordBuffers]uint32{}
|
||||
|
||||
for len(p) >= shared.Chunk {
|
||||
// Only send a block to be processed, as the collission detection
|
||||
// works on a block by block basis.
|
||||
ips := sliceHeader{
|
||||
base: uintptr(unsafe.Pointer(&p[0])),
|
||||
len: int(math.Min(float64(len(p)), float64(shared.Chunk))),
|
||||
cap: shared.Chunk,
|
||||
}
|
||||
// The assembly code only supports processing a block at a time,
|
||||
// so adjust the chunk accordingly.
|
||||
chunk := p[:shared.Chunk]
|
||||
|
||||
blockAMD64(dig, ips, m1[:], cs[:])
|
||||
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
|
||||
rectifyCompressionState(&m1, &cs)
|
||||
|
||||
col := checkCollision(m1, cs, dig.h)
|
||||
col := checkCollision(&m1, &cs, &dig.h)
|
||||
if col {
|
||||
dig.col = true
|
||||
|
||||
blockAMD64(dig, ips, m1[:], cs[:])
|
||||
blockAMD64(dig, ips, m1[:], cs[:])
|
||||
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
|
||||
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
|
||||
}
|
||||
|
||||
p = p[shared.Chunk:]
|
||||
|
||||
+266
-2265
File diff suppressed because it is too large
Load Diff
+48
@@ -0,0 +1,48 @@
|
||||
//go:build !noasm && gc && arm64 && !amd64
|
||||
// +build !noasm,gc,arm64,!amd64
|
||||
|
||||
package sha1cd
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/klauspost/cpuid/v2"
|
||||
shared "github.com/pjbgf/sha1cd/internal"
|
||||
)
|
||||
|
||||
var hasSHA1 = (runtime.GOARCH == "arm64" && cpuid.CPU.Supports(cpuid.SHA1))
|
||||
|
||||
// blockARM64 hashes the message p into the current state in h.
|
||||
// Both m1 and cs are used to store intermediate results which are used by the collision detection logic.
|
||||
//
|
||||
//go:noescape
|
||||
func blockARM64(h []uint32, p []byte, m1 []uint32, cs [][5]uint32)
|
||||
|
||||
func block(dig *digest, p []byte) {
|
||||
if forceGeneric || !hasSHA1 {
|
||||
blockGeneric(dig, p)
|
||||
return
|
||||
}
|
||||
|
||||
m1 := [shared.Rounds]uint32{}
|
||||
cs := [shared.PreStepState][shared.WordBuffers]uint32{}
|
||||
|
||||
for len(p) >= shared.Chunk {
|
||||
// The assembly code only supports processing a block at a time,
|
||||
// so adjust the chunk accordingly.
|
||||
chunk := p[:shared.Chunk]
|
||||
|
||||
blockARM64(dig.h[:], chunk, m1[:], cs[:])
|
||||
|
||||
rectifyCompressionState(&m1, &cs)
|
||||
col := checkCollision(&m1, &cs, &dig.h)
|
||||
if col {
|
||||
dig.col = true
|
||||
|
||||
blockARM64(dig.h[:], chunk, m1[:], cs[:])
|
||||
blockARM64(dig.h[:], chunk, m1[:], cs[:])
|
||||
}
|
||||
|
||||
p = p[shared.Chunk:]
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
//go:build !noasm && gc && arm64 && !amd64
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// License information for the original SHA1 arm64 implemention:
|
||||
// Copyright 2017 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found at:
|
||||
// - https://github.com/golang/go/blob/master/LICENSE
|
||||
//
|
||||
// Reference implementations:
|
||||
// - https://github.com/noloader/SHA-Intrinsics/blob/master/sha1-arm.c
|
||||
// - https://github.com/golang/go/blob/master/src/crypto/sha1/sha1block_arm64.s
|
||||
|
||||
#define HASHUPDATECHOOSE \
|
||||
SHA1C V16.S4, V1, V2 \
|
||||
SHA1H V3, V1 \
|
||||
VMOV V2.B16, V3.B16
|
||||
|
||||
#define HASHUPDATEPARITY \
|
||||
SHA1P V16.S4, V1, V2 \
|
||||
SHA1H V3, V1 \
|
||||
VMOV V2.B16, V3.B16
|
||||
|
||||
#define HASHUPDATEMAJ \
|
||||
SHA1M V16.S4, V1, V2 \
|
||||
SHA1H V3, V1 \
|
||||
VMOV V2.B16, V3.B16
|
||||
|
||||
// func blockARM64(h []uint32, p []byte, m1 []uint32, cs [][5]uint32)
|
||||
TEXT ·blockARM64(SB), NOSPLIT, $80-96
|
||||
MOVD h_base+0(FP), R0
|
||||
MOVD p_base+24(FP), R1
|
||||
MOVD p_len+32(FP), R2
|
||||
MOVD m1_base+48(FP), R3
|
||||
MOVD cs_base+72(FP), R4
|
||||
|
||||
LSR $6, R2, R2
|
||||
LSL $6, R2, R2
|
||||
ADD R16, R2, R21
|
||||
|
||||
VLD1.P 16(R0), [V0.S4]
|
||||
FMOVS (R0), F20
|
||||
SUB $16, R0, R0
|
||||
|
||||
loop:
|
||||
CMP R16, R21
|
||||
BLS end
|
||||
|
||||
// Load block (p) into 16-bytes vectors.
|
||||
VLD1.P 16(R1), [V4.B16]
|
||||
VLD1.P 16(R1), [V5.B16]
|
||||
VLD1.P 16(R1), [V6.B16]
|
||||
VLD1.P 16(R1), [V7.B16]
|
||||
|
||||
// Load K constants to V19
|
||||
MOVD $·sha1Ks(SB), R22
|
||||
VLD1 (R22), [V19.S4]
|
||||
|
||||
VMOV V0.B16, V2.B16
|
||||
VMOV V20.S[0], V1
|
||||
VMOV V2.B16, V3.B16
|
||||
VDUP V19.S[0], V17.S4
|
||||
|
||||
// Little Endian
|
||||
VREV32 V4.B16, V4.B16
|
||||
VREV32 V5.B16, V5.B16
|
||||
VREV32 V6.B16, V6.B16
|
||||
VREV32 V7.B16, V7.B16
|
||||
|
||||
// LOAD M1 rounds 0-15
|
||||
VST1.P [V4.S4], (R3)
|
||||
VST1.P [V5.S4], (R3)
|
||||
VST1.P [V6.S4], (R3)
|
||||
VST1.P [V7.S4], (R3)
|
||||
|
||||
// LOAD CS 0
|
||||
VST1.P [V0.S4], (R4) // ABCD pre-round 0
|
||||
VST1.P V1.S[0], 4(R4) // E pre-round 0
|
||||
|
||||
// Rounds 0-3
|
||||
VDUP V19.S[1], V18.S4
|
||||
VADD V17.S4, V4.S4, V16.S4
|
||||
SHA1SU0 V6.S4, V5.S4, V4.S4
|
||||
HASHUPDATECHOOSE
|
||||
SHA1SU1 V7.S4, V4.S4
|
||||
|
||||
// Rounds 4-7
|
||||
VADD V17.S4, V5.S4, V16.S4
|
||||
SHA1SU0 V7.S4, V6.S4, V5.S4
|
||||
HASHUPDATECHOOSE
|
||||
SHA1SU1 V4.S4, V5.S4
|
||||
// LOAD M1 rounds 16-19
|
||||
VST1.P [V4.S4], (R3)
|
||||
|
||||
// Rounds 8-11
|
||||
VADD V17.S4, V6.S4, V16.S4
|
||||
SHA1SU0 V4.S4, V7.S4, V6.S4
|
||||
HASHUPDATECHOOSE
|
||||
SHA1SU1 V5.S4, V6.S4
|
||||
// LOAD M1 rounds 20-23
|
||||
VST1.P [V5.S4], (R3)
|
||||
|
||||
// Rounds 12-15
|
||||
VADD V17.S4, V7.S4, V16.S4
|
||||
SHA1SU0 V5.S4, V4.S4, V7.S4
|
||||
HASHUPDATECHOOSE
|
||||
SHA1SU1 V6.S4, V7.S4
|
||||
// LOAD M1 rounds 24-27
|
||||
VST1.P [V6.S4], (R3)
|
||||
|
||||
// Rounds 16-19
|
||||
VADD V17.S4, V4.S4, V16.S4
|
||||
SHA1SU0 V6.S4, V5.S4, V4.S4
|
||||
HASHUPDATECHOOSE
|
||||
SHA1SU1 V7.S4, V4.S4
|
||||
// LOAD M1 rounds 28-31
|
||||
VST1.P [V7.S4], (R3)
|
||||
|
||||
// Rounds 20-23
|
||||
VDUP V19.S[2], V17.S4
|
||||
VADD V18.S4, V5.S4, V16.S4
|
||||
SHA1SU0 V7.S4, V6.S4, V5.S4
|
||||
HASHUPDATEPARITY
|
||||
SHA1SU1 V4.S4, V5.S4
|
||||
// LOAD M1 rounds 32-35
|
||||
VST1.P [V4.S4], (R3)
|
||||
|
||||
// Rounds 24-27
|
||||
VADD V18.S4, V6.S4, V16.S4
|
||||
SHA1SU0 V4.S4, V7.S4, V6.S4
|
||||
HASHUPDATEPARITY
|
||||
SHA1SU1 V5.S4, V6.S4
|
||||
// LOAD M1 rounds 36-39
|
||||
VST1.P [V5.S4], (R3)
|
||||
|
||||
// Rounds 28-31
|
||||
VADD V18.S4, V7.S4, V16.S4
|
||||
SHA1SU0 V5.S4, V4.S4, V7.S4
|
||||
HASHUPDATEPARITY
|
||||
SHA1SU1 V6.S4, V7.S4
|
||||
// LOAD M1 rounds 40-43
|
||||
VST1.P [V6.S4], (R3)
|
||||
|
||||
// Rounds 32-35
|
||||
VADD V18.S4, V4.S4, V16.S4
|
||||
SHA1SU0 V6.S4, V5.S4, V4.S4
|
||||
HASHUPDATEPARITY
|
||||
SHA1SU1 V7.S4, V4.S4
|
||||
// LOAD M1 rounds 44-47
|
||||
VST1.P [V7.S4], (R3)
|
||||
|
||||
// Rounds 36-39
|
||||
VADD V18.S4, V5.S4, V16.S4
|
||||
SHA1SU0 V7.S4, V6.S4, V5.S4
|
||||
HASHUPDATEPARITY
|
||||
SHA1SU1 V4.S4, V5.S4
|
||||
// LOAD M1 rounds 48-51
|
||||
VST1.P [V4.S4], (R3)
|
||||
|
||||
// Rounds 44-47
|
||||
VDUP V19.S[3], V18.S4
|
||||
VADD V17.S4, V6.S4, V16.S4
|
||||
SHA1SU0 V4.S4, V7.S4, V6.S4
|
||||
HASHUPDATEMAJ
|
||||
SHA1SU1 V5.S4, V6.S4
|
||||
// LOAD M1 rounds 52-55
|
||||
VST1.P [V5.S4], (R3)
|
||||
|
||||
// Rounds 44-47
|
||||
VADD V17.S4, V7.S4, V16.S4
|
||||
SHA1SU0 V5.S4, V4.S4, V7.S4
|
||||
HASHUPDATEMAJ
|
||||
SHA1SU1 V6.S4, V7.S4
|
||||
// LOAD M1 rounds 56-59
|
||||
VST1.P [V6.S4], (R3)
|
||||
|
||||
// Rounds 48-51
|
||||
VADD V17.S4, V4.S4, V16.S4
|
||||
SHA1SU0 V6.S4, V5.S4, V4.S4
|
||||
HASHUPDATEMAJ
|
||||
SHA1SU1 V7.S4, V4.S4
|
||||
// LOAD M1 rounds 60-63
|
||||
VST1.P [V7.S4], (R3)
|
||||
|
||||
// Rounds 52-55
|
||||
VADD V17.S4, V5.S4, V16.S4
|
||||
SHA1SU0 V7.S4, V6.S4, V5.S4
|
||||
HASHUPDATEMAJ
|
||||
SHA1SU1 V4.S4, V5.S4
|
||||
|
||||
// LOAD CS 58
|
||||
VST1.P [V3.S4], (R4) // ABCD pre-round 56
|
||||
VST1.P V1.S[0], 4(R4) // E pre-round 56
|
||||
|
||||
// Rounds 56-59
|
||||
VADD V17.S4, V6.S4, V16.S4
|
||||
SHA1SU0 V4.S4, V7.S4, V6.S4
|
||||
HASHUPDATEMAJ
|
||||
SHA1SU1 V5.S4, V6.S4
|
||||
|
||||
// Rounds 60-63
|
||||
VADD V18.S4, V7.S4, V16.S4
|
||||
SHA1SU0 V5.S4, V4.S4, V7.S4
|
||||
HASHUPDATEPARITY
|
||||
SHA1SU1 V6.S4, V7.S4
|
||||
|
||||
// LOAD CS 65
|
||||
VST1.P [V3.S4], (R4) // ABCD pre-round 64
|
||||
VST1.P V1.S[0], 4(R4) // E pre-round 64
|
||||
|
||||
// Rounds 64-67
|
||||
VADD V18.S4, V4.S4, V16.S4
|
||||
HASHUPDATEPARITY
|
||||
|
||||
// LOAD M1 rounds 68-79
|
||||
VST1.P [V4.S4], (R3)
|
||||
VST1.P [V5.S4], (R3)
|
||||
VST1.P [V6.S4], (R3)
|
||||
VST1.P [V7.S4], (R3)
|
||||
|
||||
// Rounds 68-71
|
||||
VADD V18.S4, V5.S4, V16.S4
|
||||
HASHUPDATEPARITY
|
||||
|
||||
// Rounds 72-75
|
||||
VADD V18.S4, V6.S4, V16.S4
|
||||
HASHUPDATEPARITY
|
||||
|
||||
// Rounds 76-79
|
||||
VADD V18.S4, V7.S4, V16.S4
|
||||
HASHUPDATEPARITY
|
||||
|
||||
// Add working registers to hash state.
|
||||
VADD V2.S4, V0.S4, V0.S4
|
||||
VADD V1.S4, V20.S4, V20.S4
|
||||
|
||||
end:
|
||||
// Update h with final hash values.
|
||||
VST1.P [V0.S4], (R0)
|
||||
FMOVS F20, (R0)
|
||||
|
||||
RET
|
||||
|
||||
DATA ·sha1Ks+0(SB)/4, $0x5A827999 // K0
|
||||
DATA ·sha1Ks+4(SB)/4, $0x6ED9EBA1 // K1
|
||||
DATA ·sha1Ks+8(SB)/4, $0x8F1BBCDC // K2
|
||||
DATA ·sha1Ks+12(SB)/4, $0xCA62C1D6 // K3
|
||||
GLOBL ·sha1Ks(SB), RODATA, $16
|
||||
+57
-13
@@ -15,6 +15,8 @@ import (
|
||||
"github.com/pjbgf/sha1cd/ubc"
|
||||
)
|
||||
|
||||
var forceGeneric bool
|
||||
|
||||
// blockGeneric is a portable, pure Go version of the SHA-1 block step.
|
||||
// It's used by sha1block_generic.go and tests.
|
||||
func blockGeneric(dig *digest, p []byte) {
|
||||
@@ -125,7 +127,8 @@ func blockGeneric(dig *digest, p []byte) {
|
||||
}
|
||||
|
||||
if hi == 1 {
|
||||
col := checkCollision(m1, cs, [shared.WordBuffers]uint32{h0, h1, h2, h3, h4})
|
||||
h := [shared.WordBuffers]uint32{h0, h1, h2, h3, h4}
|
||||
col := checkCollision(&m1, &cs, &h)
|
||||
if col {
|
||||
dig.col = true
|
||||
hi++
|
||||
@@ -139,24 +142,25 @@ func blockGeneric(dig *digest, p []byte) {
|
||||
dig.h[0], dig.h[1], dig.h[2], dig.h[3], dig.h[4] = h0, h1, h2, h3, h4
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func checkCollision(
|
||||
m1 [shared.Rounds]uint32,
|
||||
cs [shared.PreStepState][shared.WordBuffers]uint32,
|
||||
state [shared.WordBuffers]uint32) bool {
|
||||
|
||||
m1 *[shared.Rounds]uint32,
|
||||
cs *[shared.PreStepState][shared.WordBuffers]uint32,
|
||||
h *[shared.WordBuffers]uint32,
|
||||
) bool {
|
||||
if mask := ubc.CalculateDvMask(m1); mask != 0 {
|
||||
dvs := ubc.SHA1_dvs()
|
||||
|
||||
for i := 0; dvs[i].DvType != 0; i++ {
|
||||
if (mask & ((uint32)(1) << uint32(dvs[i].MaskB))) != 0 {
|
||||
var csState [shared.WordBuffers]uint32
|
||||
var csState *[shared.WordBuffers]uint32
|
||||
switch dvs[i].TestT {
|
||||
case 58:
|
||||
csState = cs[1]
|
||||
csState = &cs[1]
|
||||
case 65:
|
||||
csState = cs[2]
|
||||
csState = &cs[2]
|
||||
case 0:
|
||||
csState = cs[0]
|
||||
csState = &cs[0]
|
||||
default:
|
||||
panic(fmt.Sprintf("dvs data is trying to use a testT that isn't available: %d", dvs[i].TestT))
|
||||
}
|
||||
@@ -165,9 +169,9 @@ func checkCollision(
|
||||
dvs[i].TestT, // testT is the step number
|
||||
// m2 is a secondary message created XORing with
|
||||
// ubc's DM prior to the SHA recompression step.
|
||||
m1, dvs[i].Dm,
|
||||
m1, &dvs[i].Dm,
|
||||
csState,
|
||||
state)
|
||||
h)
|
||||
|
||||
if col {
|
||||
return true
|
||||
@@ -178,8 +182,9 @@ func checkCollision(
|
||||
return false
|
||||
}
|
||||
|
||||
func hasCollided(step uint32, m1, dm [shared.Rounds]uint32,
|
||||
state [shared.WordBuffers]uint32, h [shared.WordBuffers]uint32) bool {
|
||||
//go:nosplit
|
||||
func hasCollided(step uint32, m1, dm *[shared.Rounds]uint32,
|
||||
state *[shared.WordBuffers]uint32, h *[shared.WordBuffers]uint32) bool {
|
||||
// Intermediary Hash Value.
|
||||
ihv := [shared.WordBuffers]uint32{}
|
||||
|
||||
@@ -266,3 +271,42 @@ func hasCollided(step uint32, m1, dm [shared.Rounds]uint32,
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// rectifyCompressionState fixes the compression state when using the
|
||||
// SIMD implementations.
|
||||
//
|
||||
// Due to the way that hardware acceleration works, the rounds
|
||||
// are executed 4 at a time. Therefore, the state for cs58 and cs65
|
||||
// are not available directly through the assembly logic. The states
|
||||
// returned are for cs56 and cs64. This function updates indexes 1 and 2
|
||||
// of cs to contain the respective CS values for rounds 58 and 65.
|
||||
//
|
||||
//go:nosplit
|
||||
func rectifyCompressionState(
|
||||
m1 *[shared.Rounds]uint32,
|
||||
cs *[shared.PreStepState][shared.WordBuffers]uint32,
|
||||
) {
|
||||
if cs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
func3 := func(state [shared.WordBuffers]uint32, i int) [shared.WordBuffers]uint32 {
|
||||
a, b, c, d, e := state[0], state[1], state[2], state[3], state[4]
|
||||
|
||||
f := ((b | c) & d) | (b & c)
|
||||
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + shared.K2
|
||||
a, b, c, d, e = t, a, bits.RotateLeft32(b, 30), c, d
|
||||
return [shared.WordBuffers]uint32{a, b, c, d, e}
|
||||
}
|
||||
func4 := func(state [shared.WordBuffers]uint32, i int) [shared.WordBuffers]uint32 {
|
||||
a, b, c, d, e := state[0], state[1], state[2], state[3], state[4]
|
||||
f := b ^ c ^ d
|
||||
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + shared.K3
|
||||
a, b, c, d, e = t, a, bits.RotateLeft32(b, 30), c, d
|
||||
return [shared.WordBuffers]uint32{a, b, c, d, e}
|
||||
}
|
||||
|
||||
cs57 := func3(cs[1], 56)
|
||||
cs[1] = func3(cs57, 57)
|
||||
cs[2] = func4(cs[2], 64)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user