1
0
mirror of https://github.com/owncloud/ocis.git synced 2026-10-23 23:29:02 +03:00

feat: [full-ci] [OCISDEV-1563] remove the owncloudsql driver (#13191)

Remove the deprecated owncloudsql driver configuration from the storage-users,
auth-basic, users, groups and sharing services and from the migrate command.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Roman Perekhod
2026-10-21 09:53:26 +02:00
committed by GitHub
parent 4f7de832a8
commit 00567fa575
52 changed files with 143 additions and 6782 deletions

View File

@@ -0,0 +1,9 @@
Change: Remove the owncloudsql driver
We removed the deprecated `owncloudsql` driver from oCIS.
The services `storage-users`, `auth-basic`, `users`, `groups`, and `sharing` no
longer accept the value `owncloudsql` for their driver. The related
configuration sections and environment variables are removed.
https://github.com/owncloud/ocis/pull/13191

2
go.mod
View File

@@ -67,7 +67,7 @@ require (
github.com/open-policy-agent/opa v1.21.1
github.com/orcaman/concurrent-map v1.0.0
github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245
github.com/owncloud/reva/v2 v2.0.0-20261012083840-8b649a4c3899
github.com/owncloud/reva/v2 v2.0.0-20261019155447-90c24c1b3575
github.com/pkg/errors v0.9.1
github.com/pkg/xattr v0.4.12
github.com/prometheus/client_golang v1.25.0

4
go.sum
View File

@@ -736,8 +736,8 @@ github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HD
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245 h1:JRidLTAKhnvyLMRtVtSF4lhBa0NSAOs6fof+d6JnKII=
github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245/go.mod h1:z61VMGAJRtR1nbgXWiNoCkxUXP1B3Je9rMuJbnGd+Og=
github.com/owncloud/reva/v2 v2.0.0-20261012083840-8b649a4c3899 h1:IMu076AtnFEdabpBbowcGDYOeFLtm9ycNY1Z43YPYZU=
github.com/owncloud/reva/v2 v2.0.0-20261012083840-8b649a4c3899/go.mod h1:cgqg+UTe4ekjMbJj8hgoXSTxQ9fQramKcOUdvsFUrzc=
github.com/owncloud/reva/v2 v2.0.0-20261019155447-90c24c1b3575 h1:GOsblJHW/dvNdxxXmCb/PZOGRIg1ci4SVs+pA7hw6t4=
github.com/owncloud/reva/v2 v2.0.0-20261019155447-90c24c1b3575/go.mod h1:cgqg+UTe4ekjMbJj8hgoXSTxQ9fQramKcOUdvsFUrzc=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pablodz/inotifywaitgo v0.0.12 h1:DVJJTFMDlSAdO46yisPF2vzozs/r+4k6ih8MVghq78M=

View File

@@ -501,15 +501,6 @@ func revaShareConfig(cfg *sharing.Config) map[string]interface{} {
"enable_expired_shares_cleanup": cfg.UserSharingDrivers.SQL.EnableExpiredSharesCleanup,
"janitor_run_interval": cfg.UserSharingDrivers.SQL.JanitorRunInterval,
},
"owncloudsql": map[string]interface{}{
"gateway_addr": cfg.Reva.Address,
"storage_mount_id": cfg.UserSharingDrivers.OwnCloudSQL.UserStorageMountID,
"db_username": cfg.UserSharingDrivers.OwnCloudSQL.DBUsername,
"db_password": cfg.UserSharingDrivers.OwnCloudSQL.DBPassword,
"db_host": cfg.UserSharingDrivers.OwnCloudSQL.DBHost,
"db_port": cfg.UserSharingDrivers.OwnCloudSQL.DBPort,
"db_name": cfg.UserSharingDrivers.OwnCloudSQL.DBName,
},
"cs3": map[string]interface{}{
"gateway_addr": cfg.UserSharingDrivers.CS3.ProviderAddr,
"provider_addr": cfg.UserSharingDrivers.CS3.ProviderAddr,

View File

@@ -52,9 +52,8 @@ type GRPCConfig struct {
}
type AuthProviders struct {
LDAP LDAPProvider `yaml:"ldap"`
OwnCloudSQL OwnCloudSQLProvider `yaml:"owncloudsql"`
JSON JSONProvider `yaml:"json,omitempty"` // not supported by the oCIS product, therefore not part of docs
LDAP LDAPProvider `yaml:"ldap"`
JSON JSONProvider `yaml:"json,omitempty"` // not supported by the oCIS product, therefore not part of docs
}
type JSONProvider struct {
@@ -110,15 +109,3 @@ type LDAPGroupSchema struct {
Groupname string `yaml:"group_name" env:"OCIS_LDAP_GROUP_SCHEMA_GROUPNAME;AUTH_BASIC_LDAP_GROUP_SCHEMA_GROUPNAME" desc:"LDAP Attribute to use for the name of groups." introductionVersion:"pre5.0"`
Member string `yaml:"member" env:"OCIS_LDAP_GROUP_SCHEMA_MEMBER;AUTH_BASIC_LDAP_GROUP_SCHEMA_MEMBER" desc:"LDAP Attribute that is used for group members." introductionVersion:"pre5.0"`
}
type OwnCloudSQLProvider struct {
DBUsername string `yaml:"db_username" env:"AUTH_BASIC_OWNCLOUDSQL_DB_USERNAME" desc:"Database user to use for authenticating with the owncloud database." introductionVersion:"pre5.0"`
DBPassword string `yaml:"db_password" env:"AUTH_BASIC_OWNCLOUDSQL_DB_PASSWORD" desc:"Password for the database user." introductionVersion:"pre5.0"`
DBHost string `yaml:"db_host" env:"AUTH_BASIC_OWNCLOUDSQL_DB_HOST" desc:"Hostname of the database server." introductionVersion:"pre5.0"`
DBPort int `yaml:"db_port" env:"AUTH_BASIC_OWNCLOUDSQL_DB_PORT" desc:"Network port to use for the database connection." introductionVersion:"pre5.0"`
DBName string `yaml:"db_name" env:"AUTH_BASIC_OWNCLOUDSQL_DB_NAME" desc:"Name of the owncloud database." introductionVersion:"pre5.0"`
IDP string `yaml:"idp" env:"AUTH_BASIC_OWNCLOUDSQL_IDP" desc:"The identity provider value to set in the userids of the CS3 user objects for users returned by this user provider." introductionVersion:"pre5.0"`
Nobody int64 `yaml:"nobody" env:"AUTH_BASIC_OWNCLOUDSQL_NOBODY" desc:"Fallback number if no numeric UID and GID properties are provided." introductionVersion:"pre5.0"`
JoinUsername bool `yaml:"join_username" env:"AUTH_BASIC_OWNCLOUDSQL_JOIN_USERNAME" desc:"Join the user properties table to read usernames" introductionVersion:"pre5.0"`
JoinOwnCloudUUID bool `yaml:"join_owncloud_uuid" env:"AUTH_BASIC_OWNCLOUDSQL_JOIN_OWNCLOUD_UUID" desc:"Join the user properties table to read user ID's." introductionVersion:"pre5.0"`
}

View File

@@ -76,16 +76,6 @@ func DefaultConfig() *config.Config {
},
},
JSON: config.JSONProvider{},
OwnCloudSQL: config.OwnCloudSQLProvider{
DBUsername: "owncloud",
DBHost: "mysql",
DBPort: 3306,
DBName: "owncloud",
IDP: "https://localhost:9200",
Nobody: 90,
JoinUsername: false,
JoinOwnCloudUUID: false,
},
},
}
}

View File

@@ -30,17 +30,6 @@ func AuthBasicConfigFromStruct(cfg *config.Config) map[string]interface{} {
"users": cfg.AuthProviders.JSON.File,
},
"ldap": ldapConfigFromString(cfg.AuthProviders.LDAP),
"owncloudsql": map[string]interface{}{
"dbusername": cfg.AuthProviders.OwnCloudSQL.DBUsername,
"dbpassword": cfg.AuthProviders.OwnCloudSQL.DBPassword,
"dbhost": cfg.AuthProviders.OwnCloudSQL.DBHost,
"dbport": cfg.AuthProviders.OwnCloudSQL.DBPort,
"dbname": cfg.AuthProviders.OwnCloudSQL.DBName,
"idp": cfg.AuthProviders.OwnCloudSQL.IDP,
"nobody": cfg.AuthProviders.OwnCloudSQL.Nobody,
"join_username": cfg.AuthProviders.OwnCloudSQL.JoinUsername,
"join_ownclouduuid": cfg.AuthProviders.OwnCloudSQL.JoinOwnCloudUUID,
},
},
},
},

View File

@@ -21,7 +21,7 @@ type Config struct {
SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"GROUPS_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the loading of user's group memberships from the reva access token." introductionVersion:"pre5.0"`
Driver string `yaml:"driver" env:"GROUPS_DRIVER" desc:"The driver which should be used by the groups service. Supported values are 'ldap' and 'owncloudsql'." introductionVersion:"pre5.0"`
Driver string `yaml:"driver" env:"GROUPS_DRIVER" desc:"The driver which should be used by the groups service. Supported value is 'ldap'." introductionVersion:"pre5.0"`
Drivers Drivers `yaml:"drivers"`
Context context.Context `yaml:"-"`
@@ -53,8 +53,7 @@ type GRPCConfig struct {
}
type Drivers struct {
LDAP LDAPDriver `yaml:"ldap"`
OwnCloudSQL OwnCloudSQLDriver `yaml:"owncloudsql"`
LDAP LDAPDriver `yaml:"ldap"`
JSON JSONDriver `yaml:"json,omitempty"` // not supported by the oCIS product, therefore not part of docs
REST RESTProvider `yaml:"rest,omitempty"` // not supported by the oCIS product, therefore not part of docs
@@ -107,19 +106,6 @@ type LDAPGroupSchema struct {
Member string `yaml:"member" env:"OCIS_LDAP_GROUP_SCHEMA_MEMBER;GROUPS_LDAP_GROUP_SCHEMA_MEMBER" desc:"LDAP Attribute that is used for group members." introductionVersion:"pre5.0"`
}
type OwnCloudSQLDriver struct {
DBUsername string `yaml:"db_username" env:"GROUPS_OWNCLOUDSQL_DB_USERNAME" desc:"Database user to use for authenticating with the owncloud database." introductionVersion:"pre5.0"`
DBPassword string `yaml:"db_password" env:"GROUPS_OWNCLOUDSQL_DB_PASSWORD" desc:"Password for the database user." introductionVersion:"pre5.0"`
DBHost string `yaml:"db_host" env:"GROUPS_OWNCLOUDSQL_DB_HOST" desc:"Hostname of the database server." introductionVersion:"pre5.0"`
DBPort int `yaml:"db_port" env:"GROUPS_OWNCLOUDSQL_DB_PORT" desc:"Network port to use for the database connection." introductionVersion:"pre5.0"`
DBName string `yaml:"db_name" env:"GROUPS_OWNCLOUDSQL_DB_NAME" desc:"Name of the owncloud database." introductionVersion:"pre5.0"`
IDP string `yaml:"idp" env:"GROUPS_OWNCLOUDSQL_IDP" desc:"The identity provider value to set in the userids of the CS3 user objects for users returned by this user provider." introductionVersion:"pre5.0"`
Nobody int64 `yaml:"nobody" env:"GROUPS_OWNCLOUDSQL_NOBODY" desc:"Fallback number if no numeric UID and GID properties are provided." introductionVersion:"pre5.0"`
JoinUsername bool `yaml:"join_username" env:"GROUPS_OWNCLOUDSQL_JOIN_USERNAME" desc:"Join the user properties table to read usernames." introductionVersion:"pre5.0"`
JoinOwnCloudUUID bool `yaml:"join_owncloud_uuid" env:"GROUPS_OWNCLOUDSQL_JOIN_OWNCLOUD_UUID" desc:"Join the user properties table to read user IDs." introductionVersion:"pre5.0"`
EnableMedialSearch bool `yaml:"enable_medial_search" env:"GROUPS_OWNCLOUDSQL_ENABLE_MEDIAL_SEARCH" desc:"Allow 'medial search' when searching for users instead of just doing a prefix search. This allows finding 'Alice' when searching for 'lic'." introductionVersion:"pre5.0"`
}
type JSONDriver struct {
File string
}

View File

@@ -72,18 +72,6 @@ func DefaultConfig() *config.Config {
Member: "member",
},
},
OwnCloudSQL: config.OwnCloudSQLDriver{
DBUsername: "owncloud",
DBPassword: "",
DBHost: "mysql",
DBPort: 3306,
DBName: "owncloud",
IDP: "https://localhost:9200",
Nobody: 90,
JoinUsername: false,
JoinOwnCloudUUID: false,
EnableMedialSearch: false,
},
},
}
}

View File

@@ -21,7 +21,7 @@ type Config struct {
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:"pre5.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:"pre5.0"`
UserSharingDriver string `yaml:"user_sharing_driver" env:"SHARING_USER_DRIVER" desc:"Driver to be used to persist shares. Supported values are 'jsoncs3', 'json' and 'cs3' (deprecated)." introductionVersion:"pre5.0"`
UserSharingDrivers UserSharingDrivers `yaml:"user_sharing_drivers"`
PublicSharingDriver string `yaml:"public_sharing_driver" env:"SHARING_PUBLIC_DRIVER" desc:"Driver to be used to persist public shares. Supported values are 'jsoncs3', 'json' and 'cs3' (deprecated)." introductionVersion:"pre5.0"`
PublicSharingDrivers PublicSharingDrivers `yaml:"public_sharing_drivers"`
@@ -62,10 +62,9 @@ type GRPCConfig struct {
}
type UserSharingDrivers struct {
JSONCS3 UserSharingJSONCS3Driver `yaml:"jsoncs3"`
JSON UserSharingJSONDriver `yaml:"json"`
CS3 UserSharingCS3Driver `yaml:"cs3"`
OwnCloudSQL UserSharingOwnCloudSQLDriver `yaml:"owncloudsql"`
JSONCS3 UserSharingJSONCS3Driver `yaml:"jsoncs3"`
JSON UserSharingJSONDriver `yaml:"json"`
CS3 UserSharingCS3Driver `yaml:"cs3"`
SQL UserSharingSQLDriver `yaml:"sql,omitempty"` // not supported by the oCIS product, therefore not part of docs
}
@@ -86,15 +85,6 @@ type UserSharingSQLDriver struct {
UserStorageMountID string `yaml:"user_storage_mount_id"`
}
type UserSharingOwnCloudSQLDriver struct {
DBUsername string `yaml:"db_username" env:"SHARING_USER_OWNCLOUDSQL_DB_USERNAME" desc:"Username for the database." introductionVersion:"pre5.0"`
DBPassword string `yaml:"db_password" env:"SHARING_USER_OWNCLOUDSQL_DB_PASSWORD" desc:"Password for the database." introductionVersion:"pre5.0"`
DBHost string `yaml:"db_host" env:"SHARING_USER_OWNCLOUDSQL_DB_HOST" desc:"Hostname or IP of the database server." introductionVersion:"pre5.0"`
DBPort int `yaml:"db_port" env:"SHARING_USER_OWNCLOUDSQL_DB_PORT" desc:"Port that the database server is listening on." introductionVersion:"pre5.0"`
DBName string `yaml:"db_name" env:"SHARING_USER_OWNCLOUDSQL_DB_NAME" desc:"Name of the database to be used." introductionVersion:"pre5.0"`
UserStorageMountID string `yaml:"user_storage_mount_id" env:"SHARING_USER_OWNCLOUDSQL_USER_STORAGE_MOUNT_ID" desc:"Mount ID of the ownCloudSQL users storage for mapping ownCloud 10 shares." introductionVersion:"pre5.0"`
}
type UserSharingCS3Driver struct {
ProviderAddr string `yaml:"provider_addr" env:"SHARING_USER_CS3_PROVIDER_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"pre5.0"`
SystemUserID string `yaml:"system_user_id" env:"OCIS_SYSTEM_USER_ID;SHARING_USER_CS3_SYSTEM_USER_ID" desc:"ID of the oCIS STORAGE-SYSTEM system user. Admins need to set the ID for the STORAGE-SYSTEM system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"pre5.0"`

View File

@@ -49,12 +49,6 @@ func DefaultConfig() *config.Config {
SystemUserIDP: "internal",
MaxConcurrency: 1,
},
OwnCloudSQL: config.UserSharingOwnCloudSQLDriver{
DBUsername: "owncloud",
DBHost: "mysql",
DBPort: 3306,
DBName: "owncloud",
},
},
PublicSharingDriver: "jsoncs3",
PublicSharingDrivers: config.PublicSharingDrivers{
@@ -69,7 +63,6 @@ func DefaultConfig() *config.Config {
ProviderAddr: "com.owncloud.api.storage-system", // system storage
SystemUserIDP: "internal",
},
// TODO implement and add owncloudsql publicshare driver
},
Events: config.Events{
Addr: "127.0.0.1:9233",

View File

@@ -54,15 +54,6 @@ func SharingConfigFromStruct(cfg *config.Config, logger log.Logger) (map[string]
"enable_expired_shares_cleanup": cfg.UserSharingDrivers.SQL.EnableExpiredSharesCleanup,
"janitor_run_interval": cfg.UserSharingDrivers.SQL.JanitorRunInterval,
},
"owncloudsql": map[string]interface{}{
"gateway_addr": cfg.Reva.Address,
"storage_mount_id": cfg.UserSharingDrivers.OwnCloudSQL.UserStorageMountID,
"db_username": cfg.UserSharingDrivers.OwnCloudSQL.DBUsername,
"db_password": cfg.UserSharingDrivers.OwnCloudSQL.DBPassword,
"db_host": cfg.UserSharingDrivers.OwnCloudSQL.DBHost,
"db_port": cfg.UserSharingDrivers.OwnCloudSQL.DBPort,
"db_name": cfg.UserSharingDrivers.OwnCloudSQL.DBName,
},
"cs3": map[string]interface{}{
"gateway_addr": cfg.UserSharingDrivers.CS3.ProviderAddr,
"provider_addr": cfg.UserSharingDrivers.CS3.ProviderAddr,

View File

@@ -24,7 +24,7 @@ type Config struct {
SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"STORAGE_USERS_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the loading of user's group memberships from the reva access token." introductionVersion:"pre5.0"`
GracefulShutdownTimeout int `yaml:"graceful_shutdown_timeout" env:"STORAGE_USERS_GRACEFUL_SHUTDOWN_TIMEOUT" desc:"The number of seconds to wait for the 'storage-users' service to shutdown cleanly before exiting with an error that gets logged. Note: This setting is only applicable when running the 'storage-users' service as a standalone service. See the text description for more details." introductionVersion:"pre5.0"`
Driver string `yaml:"driver" env:"STORAGE_USERS_DRIVER" desc:"The storage driver which should be used by the service. Defaults to 'ocis', Supported values are: 'ocis', 's3ng' and 'owncloudsql' (deprecated). The 'ocis' driver stores all data (blob and meta data) in an POSIX compliant volume. The 's3ng' driver stores metadata in a POSIX compliant volume and uploads blobs to the s3 bucket." introductionVersion:"pre5.0"`
Driver string `yaml:"driver" env:"STORAGE_USERS_DRIVER" desc:"The storage driver which should be used by the service. Defaults to 'ocis', Supported values are: 'ocis' and 's3ng'. The 'ocis' driver stores all data (blob and meta data) in an POSIX compliant volume. The 's3ng' driver stores metadata in a POSIX compliant volume and uploads blobs to the s3 bucket." introductionVersion:"pre5.0"`
Drivers Drivers `yaml:"drivers"`
DataServerURL string `yaml:"data_server_url" env:"STORAGE_USERS_DATA_SERVER_URL" desc:"URL of the data server, needs to be reachable by the data gateway provided by the frontend service or the user if directly exposed." introductionVersion:"pre5.0"`
DataGatewayURL string `yaml:"data_gateway_url" env:"STORAGE_USERS_DATA_GATEWAY_URL" desc:"URL of the data gateway server" introductionVersion:"pre5.0"`
@@ -100,10 +100,9 @@ type CORS struct {
// Drivers combine all storage driver configurations
type Drivers struct {
OCIS OCISDriver `yaml:"ocis"`
S3NG S3NGDriver `yaml:"s3ng"`
OwnCloudSQL OwnCloudSQLDriver `yaml:"owncloudsql"`
Posix PosixDriver `yaml:"posix"`
OCIS OCISDriver `yaml:"ocis"`
S3NG S3NGDriver `yaml:"s3ng"`
Posix PosixDriver `yaml:"posix"`
S3 S3Driver `yaml:",omitempty"` // not supported by the oCIS product, therefore not part of docs
EOS EOSDriver `yaml:",omitempty"` // not supported by the oCIS product, therefore not part of docs
@@ -176,22 +175,6 @@ type S3NGDriver struct {
DisableVersioning bool `yaml:"disable_versioning" env:"OCIS_DISABLE_VERSIONING" desc:"Disables versioning of files. When set to true, new uploads with the same filename will overwrite existing files instead of creating a new version." introductionVersion:"7.0.0"`
}
// OwnCloudSQLDriver is the storage driver configuration when using 'owncloudsql' storage driver
type OwnCloudSQLDriver struct {
// Root is the absolute path to the location of the data
Root string `yaml:"root" env:"STORAGE_USERS_OWNCLOUDSQL_DATADIR" desc:"The directory where the filesystem storage will store SQL migration data. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH/storage/owncloud." introductionVersion:"pre5.0"`
// ShareFolder defines the name of the folder jailing all shares
ShareFolder string `yaml:"share_folder" env:"STORAGE_USERS_OWNCLOUDSQL_SHARE_FOLDER" desc:"Name of the folder jailing all shares." introductionVersion:"pre5.0"`
UserLayout string `yaml:"user_layout" env:"STORAGE_USERS_OWNCLOUDSQL_LAYOUT" desc:"Path layout to use to navigate into a users folder in an owncloud data directory" introductionVersion:"pre5.0"`
UploadInfoDir string `yaml:"upload_info_dir" env:"STORAGE_USERS_OWNCLOUDSQL_UPLOADINFO_DIR" desc:"The directory where the filesystem will store uploads temporarily. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH/storage/uploadinfo." introductionVersion:"pre5.0"`
DBUsername string `yaml:"db_username" env:"STORAGE_USERS_OWNCLOUDSQL_DB_USERNAME" desc:"Username for the database." introductionVersion:"pre5.0"`
DBPassword string `yaml:"db_password" env:"STORAGE_USERS_OWNCLOUDSQL_DB_PASSWORD" desc:"Password for the database." introductionVersion:"pre5.0"`
DBHost string `yaml:"db_host" env:"STORAGE_USERS_OWNCLOUDSQL_DB_HOST" desc:"Hostname or IP of the database server." introductionVersion:"pre5.0"`
DBPort int `yaml:"db_port" env:"STORAGE_USERS_OWNCLOUDSQL_DB_PORT" desc:"Port that the database server is listening on." introductionVersion:"pre5.0"`
DBName string `yaml:"db_name" env:"STORAGE_USERS_OWNCLOUDSQL_DB_NAME" desc:"Name of the database to be used." introductionVersion:"pre5.0"`
UsersProviderEndpoint string `yaml:"users_provider_endpoint" env:"STORAGE_USERS_OWNCLOUDSQL_USERS_PROVIDER_ENDPOINT" desc:"Endpoint of the users provider." introductionVersion:"pre5.0"`
}
// PosixDriver is the storage driver configuration when using 'posix' storage driver
type PosixDriver struct {
// Root is the absolute path to the location of the data

View File

@@ -94,18 +94,6 @@ func DefaultConfig() *config.Config {
GracefulShutdownTimeout: 30,
Driver: "ocis",
Drivers: config.Drivers{
OwnCloudSQL: config.OwnCloudSQLDriver{
Root: filepath.Join(defaults.BaseDataPath(), "storage", "owncloud"),
ShareFolder: "/Shares",
UserLayout: "{{.Username}}",
UploadInfoDir: filepath.Join(defaults.BaseDataPath(), "storage", "uploadinfo"),
DBUsername: "owncloud",
DBPassword: "owncloud",
DBHost: "",
DBPort: 3306,
DBName: "owncloud",
UsersProviderEndpoint: "com.owncloud.api.users",
},
S3NG: config.S3NGDriver{
Propagator: "sync",
Root: filepath.Join(defaults.BaseDataPath(), "storage", "users"),

View File

@@ -137,29 +137,6 @@ func LocalHome(cfg *config.Config) map[string]interface{} {
}
}
// OwnCloudSQL is the config mapping for the OwnCloudSQL storage driver
func OwnCloudSQL(cfg *config.Config) map[string]interface{} {
return map[string]interface{}{
"datadirectory": cfg.Drivers.OwnCloudSQL.Root,
"upload_info_dir": cfg.Drivers.OwnCloudSQL.UploadInfoDir,
"share_folder": cfg.Drivers.OwnCloudSQL.ShareFolder,
"user_layout": cfg.Drivers.OwnCloudSQL.UserLayout,
"enable_home": false,
"dbusername": cfg.Drivers.OwnCloudSQL.DBUsername,
"dbpassword": cfg.Drivers.OwnCloudSQL.DBPassword,
"dbhost": cfg.Drivers.OwnCloudSQL.DBHost,
"dbport": cfg.Drivers.OwnCloudSQL.DBPort,
"dbname": cfg.Drivers.OwnCloudSQL.DBName,
"userprovidersvc": cfg.Drivers.OwnCloudSQL.UsersProviderEndpoint,
"tokens": map[string]interface{}{
"download_endpoint": cfg.DataServerURL,
"datagateway_endpoint": cfg.DataGatewayURL,
"transfer_shared_secret": cfg.Commons.TransferSecret,
"transfer_expires": cfg.TransferExpires,
},
}
}
// Ocis is the config mapping for the Ocis storage driver
func Ocis(cfg *config.Config) map[string]interface{} {
return map[string]interface{}{

View File

@@ -5,31 +5,29 @@ import "github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
// StorageProviderDrivers are the drivers for the storage provider
func StorageProviderDrivers(cfg *config.Config) map[string]interface{} {
return map[string]interface{}{
"eos": EOS(cfg),
"eoshome": EOSHome(cfg),
"eosgrpc": EOSGRPC(cfg),
"local": Local(cfg),
"localhome": LocalHome(cfg),
"owncloudsql": OwnCloudSQL(cfg),
"ocis": OcisNoEvents(cfg),
"s3": S3(cfg),
"s3ng": S3NGNoEvents(cfg),
"posix": Posix(cfg, true),
"eos": EOS(cfg),
"eoshome": EOSHome(cfg),
"eosgrpc": EOSGRPC(cfg),
"local": Local(cfg),
"localhome": LocalHome(cfg),
"ocis": OcisNoEvents(cfg),
"s3": S3(cfg),
"s3ng": S3NGNoEvents(cfg),
"posix": Posix(cfg, true),
}
}
// DataProviderDrivers are the drivers for the storage provider
func DataProviderDrivers(cfg *config.Config) map[string]interface{} {
return map[string]interface{}{
"eos": EOS(cfg),
"eoshome": EOSHome(cfg),
"eosgrpc": EOSGRPC(cfg),
"local": Local(cfg),
"localhome": LocalHome(cfg),
"owncloudsql": OwnCloudSQL(cfg),
"ocis": Ocis(cfg),
"s3": S3(cfg),
"s3ng": S3NG(cfg),
"posix": Posix(cfg, false),
"eos": EOS(cfg),
"eoshome": EOSHome(cfg),
"eosgrpc": EOSGRPC(cfg),
"local": Local(cfg),
"localhome": LocalHome(cfg),
"ocis": Ocis(cfg),
"s3": S3(cfg),
"s3ng": S3NG(cfg),
"posix": Posix(cfg, false),
}
}

View File

@@ -21,7 +21,7 @@ type Config struct {
SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"USERS_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the loading of user's group memberships from the reva access token." introductionVersion:"pre5.0"`
Driver string `yaml:"driver" env:"USERS_DRIVER" desc:"The driver which should be used by the users service. Supported values are 'ldap' and 'owncloudsql'." introductionVersion:"pre5.0"`
Driver string `yaml:"driver" env:"USERS_DRIVER" desc:"The driver which should be used by the users service. Supported value is 'ldap'." introductionVersion:"pre5.0"`
Drivers Drivers `yaml:"drivers"`
Context context.Context `yaml:"-"`
@@ -52,8 +52,7 @@ type GRPCConfig struct {
}
type Drivers struct {
LDAP LDAPDriver `yaml:"ldap"`
OwnCloudSQL OwnCloudSQLDriver `yaml:"owncloudsql"`
LDAP LDAPDriver `yaml:"ldap"`
JSON JSONDriver `yaml:"json,omitempty"` // not supported by the oCIS product, therefore not part of docs
REST RESTProvider `yaml:"rest,omitempty"` // not supported by the oCIS product, therefore not part of docs
@@ -113,18 +112,6 @@ type LDAPGroupSchema struct {
Member string `yaml:"member" env:"OCIS_LDAP_GROUP_SCHEMA_MEMBER;USERS_LDAP_GROUP_SCHEMA_MEMBER" desc:"LDAP Attribute that is used for group members." introductionVersion:"pre5.0"`
}
type OwnCloudSQLDriver struct {
DBUsername string `yaml:"db_username" env:"USERS_OWNCLOUDSQL_DB_USERNAME" desc:"Database user to use for authenticating with the owncloud database." introductionVersion:"pre5.0"`
DBPassword string `yaml:"db_password" env:"USERS_OWNCLOUDSQL_DB_PASSWORD" desc:"Password for the database user." introductionVersion:"pre5.0"`
DBHost string `yaml:"db_host" env:"USERS_OWNCLOUDSQL_DB_HOST" desc:"Hostname of the database server." introductionVersion:"pre5.0"`
DBPort int `yaml:"db_port" env:"USERS_OWNCLOUDSQL_DB_PORT" desc:"Network port to use for the database connection." introductionVersion:"pre5.0"`
DBName string `yaml:"db_name" env:"USERS_OWNCLOUDSQL_DB_NAME" desc:"Name of the owncloud database." introductionVersion:"pre5.0"`
IDP string `yaml:"idp" env:"USERS_OWNCLOUDSQL_IDP" desc:"The identity provider value to set in the userids of the CS3 user objects for users returned by this user provider." introductionVersion:"pre5.0"`
Nobody int64 `yaml:"nobody" env:"USERS_OWNCLOUDSQL_NOBODY" desc:"Fallback number if no numeric UID and GID properties are provided." introductionVersion:"pre5.0"`
JoinUsername bool `yaml:"join_username" env:"USERS_OWNCLOUDSQL_JOIN_USERNAME" desc:"Join the user properties table to read usernames" introductionVersion:"pre5.0"`
JoinOwnCloudUUID bool `yaml:"join_owncloud_uuid" env:"USERS_OWNCLOUDSQL_JOIN_OWNCLOUD_UUID" desc:"Join the user properties table to read user IDs." introductionVersion:"pre5.0"`
EnableMedialSearch bool `yaml:"enable_medial_search" env:"USERS_OWNCLOUDSQL_ENABLE_MEDIAL_SEARCH" desc:"Allow 'medial search' when searching for users instead of just doing a prefix search. This allows finding 'Alice' when searching for 'lic'." introductionVersion:"pre5.0"`
}
type RESTProvider struct {
ClientID string
ClientSecret string

View File

@@ -77,18 +77,6 @@ func DefaultConfig() *config.Config {
},
},
JSON: config.JSONDriver{},
OwnCloudSQL: config.OwnCloudSQLDriver{
DBUsername: "owncloud",
DBPassword: "secret",
DBHost: "mysql",
DBPort: 3306,
DBName: "owncloud",
IDP: "https://localhost:9200",
Nobody: 90,
JoinUsername: false,
JoinOwnCloudUUID: false,
EnableMedialSearch: false,
},
},
}
}

View File

@@ -30,18 +30,6 @@ func UsersConfigFromStruct(cfg *config.Config) map[string]interface{} {
"users": cfg.Drivers.JSON.File,
},
"ldap": ldapConfigFromString(cfg.Drivers.LDAP),
"owncloudsql": map[string]interface{}{
"dbusername": cfg.Drivers.OwnCloudSQL.DBUsername,
"dbpassword": cfg.Drivers.OwnCloudSQL.DBPassword,
"dbhost": cfg.Drivers.OwnCloudSQL.DBHost,
"dbport": cfg.Drivers.OwnCloudSQL.DBPort,
"dbname": cfg.Drivers.OwnCloudSQL.DBName,
"idp": cfg.Drivers.OwnCloudSQL.IDP,
"nobody": cfg.Drivers.OwnCloudSQL.Nobody,
"join_username": cfg.Drivers.OwnCloudSQL.JoinUsername,
"join_ownclouduuid": cfg.Drivers.OwnCloudSQL.JoinOwnCloudUUID,
"enable_medial_search": cfg.Drivers.OwnCloudSQL.EnableMedialSearch,
},
},
},
},

View File

@@ -1,159 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package flate
// Sort sorts data.
// It makes one call to data.Len to determine n, and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func sortByFreq(data []literalNode) {
n := len(data)
quickSortByFreq(data, 0, n, maxDepth(n))
}
func quickSortByFreq(data []literalNode, a, b, maxDepth int) {
for b-a > 12 { // Use ShellSort for slices <= 12 elements
if maxDepth == 0 {
heapSort(data, a, b)
return
}
maxDepth--
mlo, mhi := doPivotByFreq(data, a, b)
// Avoiding recursion on the larger subproblem guarantees
// a stack depth of at most lg(b-a).
if mlo-a < b-mhi {
quickSortByFreq(data, a, mlo, maxDepth)
a = mhi // i.e., quickSortByFreq(data, mhi, b)
} else {
quickSortByFreq(data, mhi, b, maxDepth)
b = mlo // i.e., quickSortByFreq(data, a, mlo)
}
}
if b-a > 1 {
// Do ShellSort pass with gap 6
// It could be written in this simplified form cause b-a <= 12
for i := a + 6; i < b; i++ {
if data[i].freq == data[i-6].freq && data[i].literal < data[i-6].literal || data[i].freq < data[i-6].freq {
data[i], data[i-6] = data[i-6], data[i]
}
}
insertionSortByFreq(data, a, b)
}
}
func doPivotByFreq(data []literalNode, lo, hi int) (midlo, midhi int) {
m := int(uint(lo+hi) >> 1) // Written like this to avoid integer overflow.
if hi-lo > 40 {
// Tukey's ``Ninther,'' median of three medians of three.
s := (hi - lo) / 8
medianOfThreeSortByFreq(data, lo, lo+s, lo+2*s)
medianOfThreeSortByFreq(data, m, m-s, m+s)
medianOfThreeSortByFreq(data, hi-1, hi-1-s, hi-1-2*s)
}
medianOfThreeSortByFreq(data, lo, m, hi-1)
// Invariants are:
// data[lo] = pivot (set up by ChoosePivot)
// data[lo < i < a] < pivot
// data[a <= i < b] <= pivot
// data[b <= i < c] unexamined
// data[c <= i < hi-1] > pivot
// data[hi-1] >= pivot
pivot := lo
a, c := lo+1, hi-1
for ; a < c && (data[a].freq == data[pivot].freq && data[a].literal < data[pivot].literal || data[a].freq < data[pivot].freq); a++ {
}
b := a
for {
for ; b < c && (data[pivot].freq == data[b].freq && data[pivot].literal > data[b].literal || data[pivot].freq > data[b].freq); b++ { // data[b] <= pivot
}
for ; b < c && (data[pivot].freq == data[c-1].freq && data[pivot].literal < data[c-1].literal || data[pivot].freq < data[c-1].freq); c-- { // data[c-1] > pivot
}
if b >= c {
break
}
// data[b] > pivot; data[c-1] <= pivot
data[b], data[c-1] = data[c-1], data[b]
b++
c--
}
// If hi-c<3 then there are duplicates (by property of median of nine).
// Let's be a bit more conservative, and set border to 5.
protect := hi-c < 5
if !protect && hi-c < (hi-lo)/4 {
// Lets test some points for equality to pivot
dups := 0
if data[pivot].freq == data[hi-1].freq && data[pivot].literal > data[hi-1].literal || data[pivot].freq > data[hi-1].freq { // data[hi-1] = pivot
data[c], data[hi-1] = data[hi-1], data[c]
c++
dups++
}
if data[b-1].freq == data[pivot].freq && data[b-1].literal > data[pivot].literal || data[b-1].freq > data[pivot].freq { // data[b-1] = pivot
b--
dups++
}
// m-lo = (hi-lo)/2 > 6
// b-lo > (hi-lo)*3/4-1 > 8
// ==> m < b ==> data[m] <= pivot
if data[m].freq == data[pivot].freq && data[m].literal > data[pivot].literal || data[m].freq > data[pivot].freq { // data[m] = pivot
data[m], data[b-1] = data[b-1], data[m]
b--
dups++
}
// if at least 2 points are equal to pivot, assume skewed distribution
protect = dups > 1
}
if protect {
// Protect against a lot of duplicates
// Add invariant:
// data[a <= i < b] unexamined
// data[b <= i < c] = pivot
for {
for ; a < b && (data[b-1].freq == data[pivot].freq && data[b-1].literal > data[pivot].literal || data[b-1].freq > data[pivot].freq); b-- { // data[b] == pivot
}
for ; a < b && (data[a].freq == data[pivot].freq && data[a].literal < data[pivot].literal || data[a].freq < data[pivot].freq); a++ { // data[a] < pivot
}
if a >= b {
break
}
// data[a] == pivot; data[b-1] < pivot
data[a], data[b-1] = data[b-1], data[a]
a++
b--
}
}
// Swap pivot into middle
data[pivot], data[b-1] = data[b-1], data[pivot]
return b - 1, c
}
// Insertion sort
func insertionSortByFreq(data []literalNode, a, b int) {
for i := a + 1; i < b; i++ {
for j := i; j > a && (data[j].freq == data[j-1].freq && data[j].literal < data[j-1].literal || data[j].freq < data[j-1].freq); j-- {
data[j], data[j-1] = data[j-1], data[j]
}
}
}
// quickSortByFreq, loosely following Bentley and McIlroy,
// ``Engineering a Sort Function,'' SP&E November 1993.
// medianOfThreeSortByFreq moves the median of the three values data[m0], data[m1], data[m2] into data[m1].
func medianOfThreeSortByFreq(data []literalNode, m1, m0, m2 int) {
// sort 3 elements
if data[m1].freq == data[m0].freq && data[m1].literal < data[m0].literal || data[m1].freq < data[m0].freq {
data[m1], data[m0] = data[m0], data[m1]
}
// data[m0] <= data[m1]
if data[m2].freq == data[m1].freq && data[m2].literal < data[m1].literal || data[m2].freq < data[m1].freq {
data[m2], data[m1] = data[m1], data[m2]
// data[m0] <= data[m2] && data[m1] < data[m2]
if data[m1].freq == data[m0].freq && data[m1].literal < data[m0].literal || data[m1].freq < data[m0].freq {
data[m1], data[m0] = data[m0], data[m1]
}
}
// now data[m0] <= data[m1] <= data[m2]
}

View File

@@ -1,201 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package flate
// Sort sorts data.
// It makes one call to data.Len to determine n, and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func sortByLiteral(data []literalNode) {
n := len(data)
quickSort(data, 0, n, maxDepth(n))
}
func quickSort(data []literalNode, a, b, maxDepth int) {
for b-a > 12 { // Use ShellSort for slices <= 12 elements
if maxDepth == 0 {
heapSort(data, a, b)
return
}
maxDepth--
mlo, mhi := doPivot(data, a, b)
// Avoiding recursion on the larger subproblem guarantees
// a stack depth of at most lg(b-a).
if mlo-a < b-mhi {
quickSort(data, a, mlo, maxDepth)
a = mhi // i.e., quickSort(data, mhi, b)
} else {
quickSort(data, mhi, b, maxDepth)
b = mlo // i.e., quickSort(data, a, mlo)
}
}
if b-a > 1 {
// Do ShellSort pass with gap 6
// It could be written in this simplified form cause b-a <= 12
for i := a + 6; i < b; i++ {
if data[i].literal < data[i-6].literal {
data[i], data[i-6] = data[i-6], data[i]
}
}
insertionSort(data, a, b)
}
}
func heapSort(data []literalNode, a, b int) {
first := a
lo := 0
hi := b - a
// Build heap with greatest element at top.
for i := (hi - 1) / 2; i >= 0; i-- {
siftDown(data, i, hi, first)
}
// Pop elements, largest first, into end of data.
for i := hi - 1; i >= 0; i-- {
data[first], data[first+i] = data[first+i], data[first]
siftDown(data, lo, i, first)
}
}
// siftDown implements the heap property on data[lo, hi).
// first is an offset into the array where the root of the heap lies.
func siftDown(data []literalNode, lo, hi, first int) {
root := lo
for {
child := 2*root + 1
if child >= hi {
break
}
if child+1 < hi && data[first+child].literal < data[first+child+1].literal {
child++
}
if data[first+root].literal > data[first+child].literal {
return
}
data[first+root], data[first+child] = data[first+child], data[first+root]
root = child
}
}
func doPivot(data []literalNode, lo, hi int) (midlo, midhi int) {
m := int(uint(lo+hi) >> 1) // Written like this to avoid integer overflow.
if hi-lo > 40 {
// Tukey's ``Ninther,'' median of three medians of three.
s := (hi - lo) / 8
medianOfThree(data, lo, lo+s, lo+2*s)
medianOfThree(data, m, m-s, m+s)
medianOfThree(data, hi-1, hi-1-s, hi-1-2*s)
}
medianOfThree(data, lo, m, hi-1)
// Invariants are:
// data[lo] = pivot (set up by ChoosePivot)
// data[lo < i < a] < pivot
// data[a <= i < b] <= pivot
// data[b <= i < c] unexamined
// data[c <= i < hi-1] > pivot
// data[hi-1] >= pivot
pivot := lo
a, c := lo+1, hi-1
for ; a < c && data[a].literal < data[pivot].literal; a++ {
}
b := a
for {
for ; b < c && data[pivot].literal > data[b].literal; b++ { // data[b] <= pivot
}
for ; b < c && data[pivot].literal < data[c-1].literal; c-- { // data[c-1] > pivot
}
if b >= c {
break
}
// data[b] > pivot; data[c-1] <= pivot
data[b], data[c-1] = data[c-1], data[b]
b++
c--
}
// If hi-c<3 then there are duplicates (by property of median of nine).
// Let's be a bit more conservative, and set border to 5.
protect := hi-c < 5
if !protect && hi-c < (hi-lo)/4 {
// Lets test some points for equality to pivot
dups := 0
if data[pivot].literal > data[hi-1].literal { // data[hi-1] = pivot
data[c], data[hi-1] = data[hi-1], data[c]
c++
dups++
}
if data[b-1].literal > data[pivot].literal { // data[b-1] = pivot
b--
dups++
}
// m-lo = (hi-lo)/2 > 6
// b-lo > (hi-lo)*3/4-1 > 8
// ==> m < b ==> data[m] <= pivot
if data[m].literal > data[pivot].literal { // data[m] = pivot
data[m], data[b-1] = data[b-1], data[m]
b--
dups++
}
// if at least 2 points are equal to pivot, assume skewed distribution
protect = dups > 1
}
if protect {
// Protect against a lot of duplicates
// Add invariant:
// data[a <= i < b] unexamined
// data[b <= i < c] = pivot
for {
for ; a < b && data[b-1].literal > data[pivot].literal; b-- { // data[b] == pivot
}
for ; a < b && data[a].literal < data[pivot].literal; a++ { // data[a] < pivot
}
if a >= b {
break
}
// data[a] == pivot; data[b-1] < pivot
data[a], data[b-1] = data[b-1], data[a]
a++
b--
}
}
// Swap pivot into middle
data[pivot], data[b-1] = data[b-1], data[pivot]
return b - 1, c
}
// Insertion sort
func insertionSort(data []literalNode, a, b int) {
for i := a + 1; i < b; i++ {
for j := i; j > a && data[j].literal < data[j-1].literal; j-- {
data[j], data[j-1] = data[j-1], data[j]
}
}
}
// maxDepth returns a threshold at which quicksort should switch
// to heapsort. It returns 2*ceil(lg(n+1)).
func maxDepth(n int) int {
var depth int
for i := n; i > 0; i >>= 1 {
depth++
}
return depth * 2
}
// medianOfThree moves the median of the three values data[m0], data[m1], data[m2] into data[m1].
func medianOfThree(data []literalNode, m1, m0, m2 int) {
// sort 3 elements
if data[m1].literal < data[m0].literal {
data[m1], data[m0] = data[m0], data[m1]
}
// data[m0] <= data[m1]
if data[m2].literal < data[m1].literal {
data[m2], data[m1] = data[m1], data[m2]
// data[m0] <= data[m2] && data[m1] < data[m2]
if data[m1].literal < data[m0].literal {
data[m1], data[m0] = data[m0], data[m1]
}
}
// now data[m0] <= data[m1] <= data[m2]
}

View File

@@ -29,7 +29,6 @@ import (
_ "github.com/owncloud/reva/v2/pkg/auth/manager/nextcloud"
_ "github.com/owncloud/reva/v2/pkg/auth/manager/ocmshares"
_ "github.com/owncloud/reva/v2/pkg/auth/manager/oidc"
_ "github.com/owncloud/reva/v2/pkg/auth/manager/owncloudsql"
_ "github.com/owncloud/reva/v2/pkg/auth/manager/publicshares"
_ "github.com/owncloud/reva/v2/pkg/auth/manager/serviceaccounts"
// Add your own here

View File

@@ -1,165 +0,0 @@
// Copyright 2018-2021 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.
package accounts
import (
"context"
"database/sql"
"strings"
"time"
"github.com/owncloud/reva/v2/pkg/appctx"
"github.com/pkg/errors"
)
// Accounts represents oc10-style Accounts
type Accounts struct {
driver string
db *sql.DB
joinUsername, joinUUID, enableMedialSearch bool
selectSQL string
}
// NewMysql returns a new accounts instance connecting to a MySQL database
func NewMysql(dsn string, joinUsername, joinUUID, enableMedialSearch bool) (*Accounts, error) {
sqldb, err := sql.Open("mysql", dsn)
if err != nil {
return nil, errors.Wrap(err, "error connecting to the database")
}
// FIXME make configurable
sqldb.SetConnMaxLifetime(time.Minute * 3)
sqldb.SetConnMaxIdleTime(time.Second * 30)
sqldb.SetMaxOpenConns(100)
sqldb.SetMaxIdleConns(10)
err = sqldb.Ping()
if err != nil {
return nil, errors.Wrap(err, "error connecting to the database")
}
return New("mysql", sqldb, joinUsername, joinUUID, enableMedialSearch)
}
// New returns a new accounts instance connecting to the given sql.DB
func New(driver string, sqldb *sql.DB, joinUsername, joinUUID, enableMedialSearch bool) (*Accounts, error) {
sel := "SELECT id, email, user_id, display_name, quota, last_login, backend, home, state, password"
from := `
FROM oc_accounts a
LEFT JOIN oc_users u
ON a.user_id=u.uid
`
if joinUsername {
sel += ", p.configvalue AS username"
from += `LEFT JOIN oc_preferences p
ON a.user_id=p.userid
AND p.appid='core'
AND p.configkey='username'`
} else {
// fallback to user_id as username
sel += ", user_id AS username"
}
if joinUUID {
sel += ", p2.configvalue AS ownclouduuid"
from += `LEFT JOIN oc_preferences p2
ON a.user_id=p2.userid
AND p2.appid='core'
AND p2.configkey='ownclouduuid'`
} else {
// fallback to user_id as ownclouduuid
sel += ", user_id AS ownclouduuid"
}
return &Accounts{
driver: driver,
db: sqldb,
joinUsername: joinUsername,
joinUUID: joinUUID,
enableMedialSearch: enableMedialSearch,
selectSQL: sel + from,
}, nil
}
// Account stores information about accounts.
type Account struct {
ID uint64
Email sql.NullString
UserID string
DisplayName sql.NullString
Quota sql.NullString
LastLogin int
Backend string
Home string
State int8
PasswordHash string // from oc_users
Username sql.NullString // optional comes from the oc_preferences
OwnCloudUUID sql.NullString // optional comes from the oc_preferences
}
func (as *Accounts) rowToAccount(ctx context.Context, row Scannable) (*Account, error) {
a := Account{}
if err := row.Scan(&a.ID, &a.Email, &a.UserID, &a.DisplayName, &a.Quota, &a.LastLogin, &a.Backend, &a.Home, &a.State, &a.PasswordHash, &a.Username, &a.OwnCloudUUID); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
return nil, err
}
return &a, nil
}
// Scannable describes the interface providing a Scan method
type Scannable interface {
Scan(...interface{}) error
}
// GetAccountByLogin fetches an account by mail or username
func (as *Accounts) GetAccountByLogin(ctx context.Context, login string) (*Account, error) {
var row *sql.Row
username := strings.ToLower(login) // usernames are lowercased in owncloud classic
if as.joinUsername {
row = as.db.QueryRowContext(ctx, as.selectSQL+" WHERE a.email=? OR a.lower_user_id=? OR p.configvalue=?", login, username, login)
} else {
row = as.db.QueryRowContext(ctx, as.selectSQL+" WHERE a.email=? OR a.lower_user_id=?", login, username)
}
return as.rowToAccount(ctx, row)
}
// GetAccountGroups reads the groups for an account
func (as *Accounts) GetAccountGroups(ctx context.Context, uid string) ([]string, error) {
rows, err := as.db.QueryContext(ctx, "SELECT gid FROM oc_group_user WHERE uid=?", uid)
if err != nil {
return nil, err
}
defer rows.Close()
var group string
groups := []string{}
for rows.Next() {
if err := rows.Scan(&group); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
continue
}
groups = append(groups, group)
}
if err = rows.Err(); err != nil {
return nil, err
}
return groups, nil
}

View File

@@ -1,192 +0,0 @@
// Copyright 2018-2021 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.
package owncloudsql
import (
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"fmt"
"strings"
"golang.org/x/crypto/bcrypt"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/owncloud/reva/v2/pkg/appctx"
"github.com/owncloud/reva/v2/pkg/auth"
"github.com/owncloud/reva/v2/pkg/auth/manager/owncloudsql/accounts"
"github.com/owncloud/reva/v2/pkg/auth/manager/registry"
"github.com/owncloud/reva/v2/pkg/auth/scope"
"github.com/owncloud/reva/v2/pkg/errtypes"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
// Provides mysql drivers
_ "github.com/go-sql-driver/mysql"
)
func init() {
registry.Register("owncloudsql", NewMysql)
}
type manager struct {
c *config
db *accounts.Accounts
}
type config struct {
DbUsername string `mapstructure:"dbusername"`
DbPassword string `mapstructure:"dbpassword"`
DbHost string `mapstructure:"dbhost"`
DbPort int `mapstructure:"dbport"`
DbName string `mapstructure:"dbname"`
Idp string `mapstructure:"idp"`
Nobody int64 `mapstructure:"nobody"`
LegacySalt string `mapstructure:"legacy_salt"`
JoinUsername bool `mapstructure:"join_username"`
JoinOwnCloudUUID bool `mapstructure:"join_ownclouduuid"`
}
// NewMysql returns a new auth manager connection to an owncloud mysql database
func NewMysql(m map[string]interface{}) (auth.Manager, error) {
mgr := &manager{}
err := mgr.Configure(m)
if err != nil {
err = errors.Wrap(err, "error creating a new auth manager")
return nil, err
}
mgr.db, err = accounts.NewMysql(
fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", mgr.c.DbUsername, mgr.c.DbPassword, mgr.c.DbHost, mgr.c.DbPort, mgr.c.DbName),
mgr.c.JoinUsername,
mgr.c.JoinOwnCloudUUID,
false,
)
if err != nil {
return nil, err
}
return mgr, nil
}
func (m *manager) Configure(ml map[string]interface{}) error {
c, err := parseConfig(ml)
if err != nil {
return err
}
if c.Nobody == 0 {
c.Nobody = 99
}
m.c = c
return nil
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, &c); err != nil {
return nil, err
}
return c, nil
}
func (m *manager) Authenticate(ctx context.Context, login, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
log := appctx.GetLogger(ctx)
// 1. find user by login
account, err := m.db.GetAccountByLogin(ctx, login)
if err != nil {
return nil, nil, errtypes.NotFound(login)
}
// 2. verify the user password
if !m.verify(clientSecret, account.PasswordHash) {
return nil, nil, errtypes.InvalidCredentials(login)
}
userID := &user.UserId{
Idp: m.c.Idp,
OpaqueId: account.OwnCloudUUID.String,
Type: user.UserType_USER_TYPE_PRIMARY, // TODO: assign the appropriate user type for guest accounts
}
u := &user.User{
Id: userID,
// TODO add more claims from the StandardClaims, eg EmailVerified and lastlogin
Username: account.Username.String,
Mail: account.Email.String,
DisplayName: account.DisplayName.String,
//UidNumber: uidNumber,
//GidNumber: gidNumber,
}
if u.Groups, err = m.db.GetAccountGroups(ctx, account.UserID); err != nil {
return nil, nil, err
}
var scopes map[string]*authpb.Scope
if userID != nil && (userID.Type == user.UserType_USER_TYPE_LIGHTWEIGHT || userID.Type == user.UserType_USER_TYPE_FEDERATED) {
scopes, err = scope.AddLightweightAccountScope(authpb.Role_ROLE_OWNER, nil)
if err != nil {
return nil, nil, err
}
} else {
scopes, err = scope.AddOwnerScope(nil)
if err != nil {
return nil, nil, err
}
}
// do not log password hash
account.PasswordHash = "***redacted***"
log.Debug().Interface("account", account).Interface("user", u).Msg("authenticated user")
return u, scopes, nil
}
func (m *manager) verify(password, hash string) bool {
splitHash := strings.SplitN(hash, "|", 2)
switch len(splitHash) {
case 2:
if splitHash[0] == "1" {
return m.verifyHashV1(password, splitHash[1])
}
case 1:
return m.legacyHashVerify(password, hash)
}
return false
}
func (m *manager) legacyHashVerify(password, hash string) bool {
// TODO rehash $newHash = $this->hash($message);
switch len(hash) {
case 60: // legacy PHPass hash
return nil == bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+m.c.LegacySalt))
case 40: // legacy sha1 hash
h := sha1.Sum([]byte(password))
return hmac.Equal([]byte(hash), []byte(hex.EncodeToString(h[:])))
}
return false
}
func (m *manager) verifyHashV1(password, hash string) bool {
// TODO implement password_needs_rehash
return nil == bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}

View File

@@ -23,6 +23,5 @@ import (
_ "github.com/owncloud/reva/v2/pkg/publicshare/manager/cs3"
_ "github.com/owncloud/reva/v2/pkg/publicshare/manager/json"
_ "github.com/owncloud/reva/v2/pkg/publicshare/manager/memory"
_ "github.com/owncloud/reva/v2/pkg/publicshare/manager/owncloudsql"
// Add your own here
)

View File

@@ -1,226 +0,0 @@
// Copyright 2018-2021 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.
package owncloudsql
import (
"context"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/owncloud/reva/v2/pkg/conversions"
"github.com/owncloud/reva/v2/pkg/rgrpc/status"
"github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool"
"github.com/jellydator/ttlcache/v2"
)
// DBShare stores information about user and public shares.
type DBShare struct {
ID string
UIDOwner string
UIDInitiator string
ItemStorage string
FileSource string
ItemType string // 'file' or 'folder'
ShareWith string
Token string
Expiration string
Permissions int
ShareType int
ShareName string
STime int
FileTarget string
RejectedBy string
State int
Parent int
}
// UserConverter describes an interface for converting user ids to names and back
type UserConverter interface {
UserNameToUserID(ctx context.Context, username string) (*userpb.UserId, error)
UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error)
}
// GatewayUserConverter converts usernames and ids using the gateway
type GatewayUserConverter struct {
gwAddr string
IDCache *ttlcache.Cache
NameCache *ttlcache.Cache
}
// NewGatewayUserConverter returns a instance of GatewayUserConverter
func NewGatewayUserConverter(gwAddr string) *GatewayUserConverter {
IDCache := ttlcache.NewCache()
_ = IDCache.SetTTL(30 * time.Second)
IDCache.SkipTTLExtensionOnHit(true)
NameCache := ttlcache.NewCache()
_ = NameCache.SetTTL(30 * time.Second)
NameCache.SkipTTLExtensionOnHit(true)
return &GatewayUserConverter{
gwAddr: gwAddr,
IDCache: IDCache,
NameCache: NameCache,
}
}
// UserIDToUserName converts a user ID to an username
func (c *GatewayUserConverter) UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error) {
username, err := c.NameCache.Get(userid.String())
if err == nil {
return username.(string), nil
}
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
if err != nil {
return "", err
}
getUserResponse, err := gwConn.GetUser(ctx, &userpb.GetUserRequest{
UserId: userid,
SkipFetchingUserGroups: true,
})
if err != nil {
return "", err
}
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
return "", status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
}
_ = c.NameCache.Set(userid.String(), getUserResponse.User.Username)
return getUserResponse.User.Username, nil
}
// UserNameToUserID converts a username to an user ID
func (c *GatewayUserConverter) UserNameToUserID(ctx context.Context, username string) (*userpb.UserId, error) {
id, err := c.IDCache.Get(username)
if err == nil {
return id.(*userpb.UserId), nil
}
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
if err != nil {
return nil, err
}
getUserResponse, err := gwConn.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
Claim: "username",
Value: username,
SkipFetchingUserGroups: true,
})
if err != nil {
return nil, err
}
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
return nil, status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
}
_ = c.IDCache.Set(username, getUserResponse.User.Id)
return getUserResponse.User.Id, nil
}
func resourceTypeToItem(r provider.ResourceType) string {
switch r {
case provider.ResourceType_RESOURCE_TYPE_FILE:
return "file"
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
return "folder"
case provider.ResourceType_RESOURCE_TYPE_REFERENCE:
return "reference"
case provider.ResourceType_RESOURCE_TYPE_SYMLINK:
return "symlink"
default:
return ""
}
}
func sharePermToInt(p *provider.ResourcePermissions) int {
return int(conversions.RoleFromResourcePermissions(p, true).OCSPermissions())
}
func intTosharePerm(p int) (*provider.ResourcePermissions, error) {
perms, err := conversions.NewPermissions(p)
if err != nil {
return nil, err
}
return conversions.RoleFromOCSPermissions(perms, nil).CS3ResourcePermissions(), nil
}
func formatUserID(u *userpb.UserId) string {
return u.OpaqueId
}
// ConvertToCS3PublicShare converts a DBShare to a CS3API public share
func (m *mgr) ConvertToCS3PublicShare(ctx context.Context, s DBShare) (*link.PublicShare, error) {
ts := &typespb.Timestamp{
Seconds: uint64(s.STime),
}
permissions, err := intTosharePerm(s.Permissions)
if err != nil {
return nil, err
}
owner, err := m.userConverter.UserNameToUserID(ctx, s.UIDOwner)
if err != nil {
return nil, err
}
var creator *userpb.UserId
if s.UIDOwner == s.UIDInitiator {
creator = owner
} else {
creator, err = m.userConverter.UserNameToUserID(ctx, s.UIDOwner)
if err != nil {
return nil, err
}
}
pwd := false
if s.ShareWith != "" {
pwd = true
}
var expires *typespb.Timestamp
if s.Expiration != "" {
t, err := time.Parse("2006-01-02 15:04:05", s.Expiration)
if err != nil {
t, err = time.Parse("2006-01-02 15:04:05-07:00", s.Expiration)
}
if err == nil {
expires = &typespb.Timestamp{
Seconds: uint64(t.Unix()),
}
}
}
return &link.PublicShare{
Id: &link.PublicShareId{
OpaqueId: s.ID,
},
ResourceId: &provider.ResourceId{
SpaceId: s.ItemStorage,
OpaqueId: s.FileSource,
},
Permissions: &link.PublicSharePermissions{Permissions: permissions},
Owner: owner,
Creator: creator,
Token: s.Token,
DisplayName: s.ShareName,
PasswordProtected: pwd,
Expiration: expires,
Ctime: ts,
Mtime: ts,
}, nil
}

View File

@@ -1,533 +0,0 @@
// Copyright 2018-2021 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.
// Package owncloudsql implements a publiclink share manager backed by an existing ownCloud 10 database
//
// The SQL queries use `coalesce({column_identifier}, ”) as {column_identifier}` to read an emptystring
// instead of null values, which better fits the golang default values.
package owncloudsql
import (
"context"
"database/sql"
"fmt"
"strconv"
"strings"
"time"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/owncloud/reva/v2/pkg/errtypes"
"github.com/owncloud/reva/v2/pkg/publicshare"
"github.com/owncloud/reva/v2/pkg/publicshare/manager/registry"
"github.com/owncloud/reva/v2/pkg/sharedconf"
"github.com/owncloud/reva/v2/pkg/utils"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
"google.golang.org/protobuf/proto"
// Provides mysql drivers
_ "github.com/go-sql-driver/mysql"
)
const (
publicShareType = 3
)
func init() {
registry.Register("owncloudsql", NewMysql)
}
// Config configures an owncloudsql publicshare manager
type Config struct {
GatewayAddr string `mapstructure:"gateway_addr"`
DbUsername string `mapstructure:"db_username"`
DbPassword string `mapstructure:"db_password"`
DbHost string `mapstructure:"db_host"`
DbPort int `mapstructure:"db_port"`
DbName string `mapstructure:"db_name"`
EnableExpiredSharesCleanup bool `mapstructure:"enable_expired_shares_cleanup"`
SharePasswordHashCost int `mapstructure:"password_hash_cost"`
}
type mgr struct {
driver string
db *sql.DB
c Config
userConverter UserConverter
}
// NewMysql returns a new publicshare manager connection to a mysql database
func NewMysql(m map[string]interface{}) (publicshare.Manager, error) {
c, err := parseConfig(m)
if err != nil {
err = errors.Wrap(err, "error creating a new manager")
return nil, err
}
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.DbUsername, c.DbPassword, c.DbHost, c.DbPort, c.DbName))
if err != nil {
return nil, err
}
userConverter := NewGatewayUserConverter(sharedconf.GetGatewaySVC(c.GatewayAddr))
return New("mysql", db, *c, userConverter)
}
// New returns a new Cache instance connecting to the given sql.DB
func New(driver string, db *sql.DB, c Config, userConverter UserConverter) (publicshare.Manager, error) {
if c.SharePasswordHashCost == 0 {
c.SharePasswordHashCost = bcrypt.DefaultCost
}
return &mgr{
driver: driver,
db: db,
c: c,
userConverter: userConverter,
}, nil
}
func parseConfig(m map[string]interface{}) (*Config, error) {
c := &Config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
func (m *mgr) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error) {
tkn := utils.RandString(15)
now := time.Now().Unix()
displayName := tkn
if rInfo.ArbitraryMetadata != nil && rInfo.ArbitraryMetadata.Metadata["name"] != "" {
displayName = rInfo.ArbitraryMetadata.Metadata["name"]
}
createdAt := &typespb.Timestamp{
Seconds: uint64(now),
}
creator := u.Username
owner, err := m.userConverter.UserIDToUserName(ctx, rInfo.Owner)
if err != nil {
return nil, err
}
permissions := sharePermToInt(g.Permissions.Permissions)
itemType := resourceTypeToItem(rInfo.Type)
itemSource := rInfo.Id.OpaqueId
fileSource, err := strconv.ParseUint(itemSource, 10, 64)
if err != nil {
// it can be the case that the item source may be a character string
// we leave fileSource blank in that case
fileSource = 0
}
columns := "share_type,uid_owner,uid_initiator,item_type,item_source,file_source,permissions,stime,token,share_name"
placeholders := "?,?,?,?,?,?,?,?,?,?"
params := []interface{}{publicShareType, owner, creator, itemType, itemSource, fileSource, permissions, now, tkn, displayName}
var passwordProtected bool
password := g.Password
if password != "" {
password, err = hashPassword(password, m.c.SharePasswordHashCost)
if err != nil {
return nil, errors.Wrap(err, "could not hash share password")
}
passwordProtected = true
columns += ",share_with"
placeholders += ",?"
params = append(params, password)
}
if g.Expiration != nil && g.Expiration.Seconds != 0 {
t := time.Unix(int64(g.Expiration.Seconds), 0)
columns += ",expiration"
placeholders += ",?"
params = append(params, t)
}
query := "INSERT INTO oc_share (" + columns + ") VALUES (" + placeholders + ")"
stmt, err := m.db.Prepare(query)
if err != nil {
return nil, err
}
result, err := stmt.Exec(params...)
if err != nil {
return nil, err
}
lastID, err := result.LastInsertId()
if err != nil {
return nil, err
}
return &link.PublicShare{
Id: &link.PublicShareId{
OpaqueId: strconv.FormatInt(lastID, 10),
},
Owner: rInfo.GetOwner(),
Creator: u.Id,
ResourceId: rInfo.Id,
Token: tkn,
Permissions: g.Permissions,
Ctime: createdAt,
Mtime: createdAt,
PasswordProtected: passwordProtected,
Expiration: g.Expiration,
DisplayName: displayName,
}, nil
}
// owncloud 10 prefixes the hash with `1|`
func hashPassword(password string, cost int) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
return "1|" + string(bytes), err
}
// UpdatePublicShare updates the expiration date, permissions and Mtime
func (m *mgr) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest) (*link.PublicShare, error) {
query := "update oc_share set "
paramsMap := map[string]interface{}{}
params := []interface{}{}
now := time.Now().Unix()
uid := u.Username
switch req.GetUpdate().GetType() {
case link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME:
paramsMap["share_name"] = req.Update.GetDisplayName()
case link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
paramsMap["permissions"] = sharePermToInt(req.Update.GetGrant().GetPermissions().Permissions)
case link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION:
paramsMap["expiration"] = time.Unix(int64(req.Update.GetGrant().Expiration.Seconds), 0)
case link.UpdatePublicShareRequest_Update_TYPE_PASSWORD:
if req.Update.GetGrant().Password == "" {
paramsMap["share_with"] = ""
} else {
h, err := hashPassword(req.Update.GetGrant().Password, m.c.SharePasswordHashCost)
if err != nil {
return nil, errors.Wrap(err, "could not hash share password")
}
paramsMap["share_with"] = h
}
default:
return nil, fmt.Errorf("invalid update type: %v", req.GetUpdate().GetType())
}
for k, v := range paramsMap {
query += k + "=?"
params = append(params, v)
}
switch {
case req.Ref.GetId() != nil:
query += ",stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, now, req.Ref.GetId().OpaqueId, uid, uid)
case req.Ref.GetToken() != "":
query += ",stime=? where token=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, now, req.Ref.GetToken(), uid, uid)
default:
return nil, errtypes.NotFound(req.Ref.String())
}
stmt, err := m.db.Prepare(query)
if err != nil {
return nil, err
}
if _, err = stmt.Exec(params...); err != nil {
return nil, err
}
return m.GetPublicShare(ctx, u, req.Ref, false)
}
func (m *mgr) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (share *link.PublicShare, err error) {
ps, err := m.getWithPassword(ctx, ref)
if err != nil {
return nil, err
}
if publicshare.IsExpired(&ps.PublicShare) {
if err := m.cleanupExpiredShares(); err != nil {
return nil, err
}
return nil, errtypes.NotFound("public share has expired")
}
if ps.PublicShare.PasswordProtected && sign {
err = publicshare.AddSignature(&ps.PublicShare, ps.Password)
if err != nil {
return nil, err
}
}
return &ps.PublicShare, nil
}
func (m *mgr) getWithPassword(ctx context.Context, ref *link.PublicShareReference) (*publicshare.WithPassword, error) {
switch {
case ref.GetToken() != "":
return m.getByToken(ctx, ref.GetToken())
case ref.GetId().GetOpaqueId() != "":
return m.getByID(ctx, ref.GetId().GetOpaqueId())
default:
return nil, errtypes.BadRequest("neither id nor token given")
}
}
func (m *mgr) getByToken(ctx context.Context, token string) (*publicshare.WithPassword, error) {
s, err := getByToken(m.db, token)
if err != nil {
return nil, err
}
ps, err := m.ConvertToCS3PublicShare(ctx, s)
if err != nil {
return nil, err
}
ret := &publicshare.WithPassword{
Password: strings.TrimPrefix(s.ShareWith, "1|"),
}
proto.Merge(&ret.PublicShare, ps)
return ret, nil
}
func getByToken(db *sql.DB, token string) (DBShare, error) {
s := DBShare{Token: token}
query := `SELECT
coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
coalesce(item_type, '') as item_type,
coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
s.id, s.stime, s.permissions, fc.storage as storage
FROM oc_share s
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
WHERE share_type=? AND token=?`
if err := db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.ItemStorage); err != nil {
if err == sql.ErrNoRows {
return s, errtypes.NotFound(token)
}
return s, err
}
return s, nil
}
func (m *mgr) getByID(ctx context.Context, id string) (*publicshare.WithPassword, error) {
s := DBShare{ID: id}
query := `SELECT
coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
coalesce(item_type, '') as item_type, coalesce(token,'') as token,
coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
s.stime, s.permissions, fc.storage as storage
FROM oc_share s
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
WHERE share_type=? AND id=?`
if err := m.db.QueryRow(query, publicShareType, id).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.STime, &s.Permissions, &s.ItemStorage); err != nil {
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(id)
}
return nil, err
}
ps, err := m.ConvertToCS3PublicShare(ctx, s)
if err != nil {
return nil, err
}
ret := &publicshare.WithPassword{
Password: strings.TrimPrefix(s.ShareWith, "1|"),
}
proto.Merge(&ret.PublicShare, ps)
return ret, nil
}
func (m *mgr) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error) {
uid := u.Username
// FIXME instead of joining we may want to have to do a stat call ... if we want to store shares from other providers? or just Dump()? and be done with migration?
query := `SELECT
coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
coalesce(item_type, '') as item_type, coalesce(token,'') as token,
coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
s.id, s.stime, s.permissions, fc.storage as storage
FROM oc_share s
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
WHERE (uid_owner=? or uid_initiator=?)
AND (share_type=?)`
var resourceFilters, ownerFilters, creatorFilters, storageFilters string
var resourceParams, ownerParams, creatorParams, storageParams []interface{}
params := []interface{}{uid, uid, publicShareType}
for _, f := range filters {
switch f.Type {
case link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID:
if len(resourceFilters) != 0 {
resourceFilters += " OR "
}
resourceFilters += "item_source=?"
resourceParams = append(resourceParams, f.GetResourceId().GetOpaqueId())
case link.ListPublicSharesRequest_Filter_TYPE_OWNER:
if len(ownerFilters) != 0 {
ownerFilters += " OR "
}
ownerFilters += "(uid_owner=?)"
ownerParams = append(ownerParams, formatUserID(f.GetOwner()))
case link.ListPublicSharesRequest_Filter_TYPE_CREATOR:
if len(creatorFilters) != 0 {
creatorFilters += " OR "
}
creatorFilters += "(uid_initiator=?)"
creatorParams = append(creatorParams, formatUserID(f.GetCreator()))
case publicshare.StorageIDFilterType:
if len(storageFilters) != 0 {
storageFilters += " OR "
}
storageFilters += "(storage=?)"
storageParams = append(storageParams, f.GetResourceId().GetStorageId())
}
}
if resourceFilters != "" {
query = fmt.Sprintf("%s AND (%s)", query, resourceFilters)
params = append(params, resourceParams...)
}
if ownerFilters != "" {
query = fmt.Sprintf("%s AND (%s)", query, ownerFilters)
params = append(params, ownerParams...)
}
if creatorFilters != "" {
query = fmt.Sprintf("%s AND (%s)", query, creatorFilters)
params = append(params, creatorParams...)
}
if storageFilters != "" {
query = fmt.Sprintf("%s AND (%s)", query, storageFilters)
params = append(params, storageParams...)
}
rows, err := m.db.Query(query, params...)
if err != nil {
return nil, err
}
defer rows.Close()
var s DBShare
shares := []*link.PublicShare{}
for rows.Next() {
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.ItemStorage); err != nil {
continue
}
var cs3Share *link.PublicShare
if cs3Share, err = m.ConvertToCS3PublicShare(ctx, s); err != nil {
return nil, err
}
if publicshare.IsExpired(cs3Share) {
_ = m.cleanupExpiredShares()
} else {
if cs3Share.PasswordProtected && sign {
if err := publicshare.AddSignature(cs3Share, strings.TrimPrefix(s.ShareWith, "1|")); err != nil {
return nil, err
}
}
shares = append(shares, cs3Share)
}
}
if err = rows.Err(); err != nil {
return nil, err
}
return shares, nil
}
func (m *mgr) RevokePublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) error {
uid := u.Username
query := "delete from oc_share where "
params := []interface{}{}
switch {
case ref.GetId() != nil && ref.GetId().OpaqueId != "":
query += "id=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, ref.GetId().OpaqueId, uid, uid)
case ref.GetToken() != "":
query += "token=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, ref.GetToken(), uid, uid)
default:
return errtypes.NotFound(ref.String())
}
stmt, err := m.db.Prepare(query)
if err != nil {
return err
}
res, err := stmt.Exec(params...)
if err != nil {
return err
}
rowCnt, err := res.RowsAffected()
if err != nil {
return err
}
if rowCnt == 0 {
return errtypes.NotFound(ref.String())
}
return nil
}
func (m *mgr) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) {
ps, err := m.getByToken(ctx, token)
if err != nil {
return nil, err
}
if publicshare.IsExpired(&ps.PublicShare) {
if err := m.cleanupExpiredShares(); err != nil {
return nil, err
}
return nil, errtypes.NotFound("public share has expired")
}
if ps.PublicShare.PasswordProtected {
if !publicshare.Authenticate(&ps.PublicShare, ps.Password, auth) {
return nil, errtypes.InvalidCredentials("access denied")
}
}
return &ps.PublicShare, nil
}
func (m *mgr) cleanupExpiredShares() error {
if !m.c.EnableExpiredSharesCleanup {
return nil
}
query := "DELETE FROM oc_share WHERE expiration IS NOT NULL AND expiration < ?"
params := []interface{}{time.Now().Format("2006-01-02 03:04:05")}
stmt, err := m.db.Prepare(query)
if err != nil {
return err
}
if _, err = stmt.Exec(params...); err != nil {
return err
}
return nil
}

View File

@@ -20,6 +20,7 @@ package pool
import (
"fmt"
"math/rand"
"strings"
"sync"
@@ -45,7 +46,9 @@ import (
"github.com/owncloud/reva/v2/pkg/registry"
"github.com/pkg/errors"
"github.com/sercand/kuberesolver/v5"
mRegistry "go-micro.dev/v4/registry"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
)
func init() {
@@ -82,6 +85,27 @@ func GetSelector[T any](k string, id string, f func(cc grpc.ClientConnInterface)
return newSelector
}
// cachedClient pairs a grpc client with the connection backing it, so Next
// can check the connection's health before handing the client back out.
type cachedClient[T any] struct {
conn *grpc.ClientConn
client T
}
// healthy reports whether the cached connection is safe to hand out again.
// A connection only becomes TransientFailure or Shutdown after a real RPC on
// it has already failed - a connection that was never used for an RPC stays
// Idle, so it is treated as healthy here (there is no way to probe
// reachability without using it).
func (c cachedClient[T]) healthy() bool {
switch c.conn.GetState() {
case connectivity.TransientFailure, connectivity.Connecting, connectivity.Shutdown:
return false
default:
return true
}
}
type Selector[T any] struct {
id string
clientFactory func(cc grpc.ClientConnInterface) T
@@ -101,47 +125,99 @@ func (s *Selector[T]) Next(opts ...Option) (T, error) {
opt(&options)
}
target := s.id
// if the target is given as a recognized gRPC URI, skip registry lookup
// see https://github.com/grpc/grpc/blob/master/doc/naming.md#name-syntax
prefix := strings.SplitN(s.id, ":", 2)[0]
switch {
case prefix == "dns":
fallthrough
case prefix == "unix":
fallthrough
case prefix == "kubernetes":
// use target as is and skip registry lookup
case prefix == "dns", prefix == "unix", prefix == "kubernetes":
// use the id as is and skip registry lookup
return s.loadOrNewClient(s.id, allOpts)
case options.registry != nil:
// use service registry to look up address
// use the service registry to look up every known node, and pick
// the first one that is ready
services, err := options.registry.GetService(s.id)
if err != nil {
return *new(T), fmt.Errorf("%s: %w", s.id, err)
}
nodeAddress, err := registry.GetNodeAddress(services)
if err != nil {
return *new(T), fmt.Errorf("%s: %w", s.id, err)
}
target = nodeAddress
targets := shuffledTargets(services)
return s.nextHealthy(targets, allOpts)
default:
// if no registry is available, use the target as is
// if no registry is available, use the id as is
return s.loadOrNewClient(s.id, allOpts)
}
}
// nextHealthy walks targets in the given order. It returns the client for
// the first target that is ready. A target with no cache entry is also a
// candidate, since Next has no way to know its state without dialing it. If
// no target is ready, nextHealthy reuses the first one found, so the caller
// gets a real error from a real attempt instead of a fabricated one.
func (s *Selector[T]) nextHealthy(targets []string, opts []Option) (T, error) {
if len(targets) == 0 {
return *new(T), fmt.Errorf("%s: no nodes found in registry", s.id)
}
var fallback *cachedClient[T]
for _, target := range targets {
cached, ok := s.clientMap.Load(target)
if !ok {
return s.newClient(target, opts)
}
cc := cached.(cachedClient[T])
if cc.healthy() {
return cc.client, nil
}
if fallback == nil {
fallback = &cc
}
}
existingClient, ok := s.clientMap.Load(target)
if ok {
return existingClient.(T), nil
}
return fallback.client, nil
}
conn, err := NewConn(target, allOpts...)
// loadOrNewClient returns the cached client for target, creating a new one if
// none exists yet. It does not check health: target was not looked up
// through the registry, so there is no alternative node to fall back to.
func (s *Selector[T]) loadOrNewClient(target string, opts []Option) (T, error) {
if cached, ok := s.clientMap.Load(target); ok {
return cached.(cachedClient[T]).client, nil
}
return s.newClient(target, opts)
}
// newClient creates a new connection to target, wraps it in a client and caches
// both under target.
func (s *Selector[T]) newClient(target string, opts []Option) (T, error) {
conn, err := NewConn(target, opts...)
if err != nil {
return *new(T), errors.Wrap(err, fmt.Sprintf("could not create connection for %s to %s", s.id, target))
}
newClient := s.clientFactory(conn)
s.clientMap.Store(target, newClient)
newClient := cachedClient[T]{conn: conn, client: s.clientFactory(conn)}
// if another goroutine already created a client for this target, use it instead
if existing, loaded := s.clientMap.LoadOrStore(target, newClient); loaded {
_ = conn.Close()
return existing.(cachedClient[T]).client, nil
}
return newClient, nil
return newClient.client, nil
}
// shuffledTargets flattens every node address out of services and returns
// them in random order, so repeated calls spread load across all known
// nodes instead of always preferring the same one.
func shuffledTargets(services []*mRegistry.Service) []string {
var targets []string
for _, svc := range services {
for _, node := range svc.Nodes {
targets = append(targets, node.Address)
}
}
rand.Shuffle(len(targets), func(i, j int) {
targets[i], targets[j] = targets[j], targets[i]
})
return targets
}
// GatewaySelector returns a Selector[gateway.GatewayAPIClient].

View File

@@ -24,6 +24,5 @@ import (
_ "github.com/owncloud/reva/v2/pkg/share/manager/json"
_ "github.com/owncloud/reva/v2/pkg/share/manager/jsoncs3"
_ "github.com/owncloud/reva/v2/pkg/share/manager/memory"
_ "github.com/owncloud/reva/v2/pkg/share/manager/owncloudsql"
// Add your own here
)

View File

@@ -1,301 +0,0 @@
// Copyright 2018-2021 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.
package owncloudsql
import (
"context"
"strings"
"time"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
userprovider "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"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/owncloud/reva/v2/pkg/conversions"
"github.com/owncloud/reva/v2/pkg/rgrpc/status"
"github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool"
"github.com/owncloud/reva/v2/pkg/utils"
"github.com/jellydator/ttlcache/v2"
)
// DBShare stores information about user and public shares.
type DBShare struct {
ID string
UIDOwner string
UIDInitiator string
ItemStorage string
FileSource string
ShareWith string
Token string
Expiration string
Permissions int
ShareType int
ShareName string
STime int
FileTarget string
RejectedBy string
State int
Parent int
}
// UserConverter describes an interface for converting user ids to names and back
type UserConverter interface {
UserNameToUserID(ctx context.Context, username string) (*userpb.UserId, error)
UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error)
GetUser(userid *userpb.UserId) (*userpb.User, error)
}
// GatewayUserConverter converts usernames and ids using the gateway
type GatewayUserConverter struct {
gwAddr string
IDCache *ttlcache.Cache
NameCache *ttlcache.Cache
}
// NewGatewayUserConverter returns a instance of GatewayUserConverter
func NewGatewayUserConverter(gwAddr string) *GatewayUserConverter {
IDCache := ttlcache.NewCache()
_ = IDCache.SetTTL(30 * time.Second)
IDCache.SkipTTLExtensionOnHit(true)
NameCache := ttlcache.NewCache()
_ = NameCache.SetTTL(30 * time.Second)
NameCache.SkipTTLExtensionOnHit(true)
return &GatewayUserConverter{
gwAddr: gwAddr,
IDCache: IDCache,
NameCache: NameCache,
}
}
// UserIDToUserName converts a user ID to an username
func (c *GatewayUserConverter) UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error) {
username, err := c.NameCache.Get(userid.String())
if err == nil {
return username.(string), nil
}
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
if err != nil {
return "", err
}
getUserResponse, err := gwConn.GetUser(ctx, &userprovider.GetUserRequest{
UserId: userid,
SkipFetchingUserGroups: true,
})
if err != nil {
return "", err
}
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
return "", status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
}
_ = c.NameCache.Set(userid.String(), getUserResponse.User.Username)
return getUserResponse.User.Username, nil
}
// UserNameToUserID converts a username to an user ID
func (c *GatewayUserConverter) UserNameToUserID(ctx context.Context, username string) (*userpb.UserId, error) {
id, err := c.IDCache.Get(username)
if err == nil {
return id.(*userpb.UserId), nil
}
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
if err != nil {
return nil, err
}
getUserResponse, err := gwConn.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
Claim: "username",
Value: username,
SkipFetchingUserGroups: true,
})
if err != nil {
return nil, err
}
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
return nil, status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
}
_ = c.IDCache.Set(username, getUserResponse.User.Id)
return getUserResponse.User.Id, nil
}
// GetUser gets the user
func (c *GatewayUserConverter) GetUser(userid *userpb.UserId) (*userpb.User, error) {
gwc, err := pool.GetGatewayServiceClient(c.gwAddr)
if err != nil {
return nil, err
}
return utils.GetUser(userid, gwc)
}
func (m *mgr) formatGrantee(ctx context.Context, g *provider.Grantee) (int, string, error) {
var granteeType int
var formattedID string
switch g.Type {
case provider.GranteeType_GRANTEE_TYPE_USER:
granteeType = 0
var err error
formattedID, err = m.userConverter.UserIDToUserName(ctx, g.GetUserId())
if err != nil {
return 0, "", err
}
case provider.GranteeType_GRANTEE_TYPE_GROUP:
granteeType = 1
formattedID = formatGroupID(g.GetGroupId())
default:
granteeType = -1
}
return granteeType, formattedID, nil
}
func (m *mgr) extractGrantee(ctx context.Context, t int, g string) (*provider.Grantee, error) {
var grantee provider.Grantee
switch t {
case 0:
userid, err := m.userConverter.UserNameToUserID(ctx, g)
if err != nil {
return nil, err
}
grantee.Type = provider.GranteeType_GRANTEE_TYPE_USER
grantee.Id = &provider.Grantee_UserId{UserId: userid}
case 1, 2:
grantee.Type = provider.GranteeType_GRANTEE_TYPE_GROUP
grantee.Id = &provider.Grantee_GroupId{GroupId: extractGroupID(g)}
default:
grantee.Type = provider.GranteeType_GRANTEE_TYPE_INVALID
}
return &grantee, nil
}
func resourceTypeToItem(r provider.ResourceType) string {
switch r {
case provider.ResourceType_RESOURCE_TYPE_FILE:
return "file"
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
return "folder"
case provider.ResourceType_RESOURCE_TYPE_REFERENCE:
return "reference"
case provider.ResourceType_RESOURCE_TYPE_SYMLINK:
return "symlink"
default:
return ""
}
}
func sharePermToInt(p *provider.ResourcePermissions) int {
return int(conversions.RoleFromResourcePermissions(p, false).OCSPermissions())
}
func intTosharePerm(p int) (*provider.ResourcePermissions, error) {
perms, err := conversions.NewPermissions(p)
if err != nil {
return nil, err
}
return conversions.RoleFromOCSPermissions(perms, nil).CS3ResourcePermissions(), nil
}
func intToShareState(g int) collaboration.ShareState {
switch g {
case 0:
return collaboration.ShareState_SHARE_STATE_ACCEPTED
case 1:
return collaboration.ShareState_SHARE_STATE_PENDING
case 2:
return collaboration.ShareState_SHARE_STATE_REJECTED
default:
return collaboration.ShareState_SHARE_STATE_INVALID
}
}
func formatUserID(u *userpb.UserId) string {
return u.OpaqueId
}
func formatGroupID(u *grouppb.GroupId) string {
return u.OpaqueId
}
func extractGroupID(u string) *grouppb.GroupId {
return &grouppb.GroupId{OpaqueId: u}
}
func (m *mgr) convertToCS3Share(ctx context.Context, s DBShare, storageMountID string) (*collaboration.Share, error) {
ts := &typespb.Timestamp{
Seconds: uint64(s.STime),
}
permissions, err := intTosharePerm(s.Permissions)
if err != nil {
return nil, err
}
grantee, err := m.extractGrantee(ctx, s.ShareType, s.ShareWith)
if err != nil {
return nil, err
}
owner, err := m.userConverter.UserNameToUserID(ctx, s.UIDOwner)
if err != nil {
return nil, err
}
var creator *userpb.UserId
if s.UIDOwner == s.UIDInitiator {
creator = owner
} else {
creator, err = m.userConverter.UserNameToUserID(ctx, s.UIDOwner)
if err != nil {
return nil, err
}
}
return &collaboration.Share{
Id: &collaboration.ShareId{
OpaqueId: s.ID,
},
ResourceId: &provider.ResourceId{
SpaceId: s.ItemStorage,
OpaqueId: s.FileSource,
},
Permissions: &collaboration.SharePermissions{Permissions: permissions},
Grantee: grantee,
Owner: owner,
Creator: creator,
Ctime: ts,
Mtime: ts,
}, nil
}
func (m *mgr) convertToCS3ReceivedShare(ctx context.Context, s DBShare, storageMountID string) (*collaboration.ReceivedShare, error) {
share, err := m.convertToCS3Share(ctx, s, storageMountID)
if err != nil {
return nil, err
}
var state collaboration.ShareState
if s.RejectedBy != "" {
state = collaboration.ShareState_SHARE_STATE_REJECTED
} else {
state = intToShareState(s.State)
}
return &collaboration.ReceivedShare{
Share: share,
State: state,
MountPoint: &provider.Reference{Path: strings.TrimLeft(s.FileTarget, "/")},
}, nil
}

View File

@@ -1,674 +0,0 @@
// Copyright 2018-2021 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.
package owncloudsql
import (
"context"
"database/sql"
"fmt"
"path"
"strconv"
"strings"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
ctxpkg "github.com/owncloud/reva/v2/pkg/ctx"
"github.com/owncloud/reva/v2/pkg/errtypes"
"github.com/owncloud/reva/v2/pkg/share"
"github.com/owncloud/reva/v2/pkg/share/manager/registry"
"github.com/owncloud/reva/v2/pkg/utils"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"google.golang.org/genproto/protobuf/field_mask"
// Provides mysql drivers
_ "github.com/go-sql-driver/mysql"
)
const (
shareTypeUser = 0
shareTypeGroup = 1
)
func init() {
registry.Register("owncloudsql", NewMysql)
}
type config struct {
GatewayAddr string `mapstructure:"gateway_addr"`
StorageMountID string `mapstructure:"storage_mount_id"`
DbUsername string `mapstructure:"db_username"`
DbPassword string `mapstructure:"db_password"`
DbHost string `mapstructure:"db_host"`
DbPort int `mapstructure:"db_port"`
DbName string `mapstructure:"db_name"`
}
type mgr struct {
driver string
db *sql.DB
storageMountID string
userConverter UserConverter
}
// NewMysql returns a new share manager connection to a mysql database
func NewMysql(m map[string]interface{}) (share.Manager, error) {
c, err := parseConfig(m)
if err != nil {
err = errors.Wrap(err, "error creating a new manager")
return nil, err
}
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.DbUsername, c.DbPassword, c.DbHost, c.DbPort, c.DbName))
if err != nil {
return nil, err
}
userConverter := NewGatewayUserConverter(c.GatewayAddr)
return New("mysql", db, c.StorageMountID, userConverter)
}
// New returns a new Cache instance connecting to the given sql.DB
func New(driver string, db *sql.DB, storageMountID string, userConverter UserConverter) (share.Manager, error) {
return &mgr{
driver: driver,
db: db,
storageMountID: storageMountID,
userConverter: userConverter,
}, nil
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
func (m *mgr) Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) {
user := ctxpkg.ContextMustGetUser(ctx)
// do not allow share to myself or the owner if share is for a user
// TODO(labkode): should not this be caught already 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)) {
return nil, errtypes.BadRequest("owncloudsql: owner/creator and grantee are the same")
}
// check if share already exists.
key := &collaboration.ShareKey{
Owner: md.Owner,
ResourceId: md.Id,
Grantee: g.Grantee,
}
_, err := m.getByKey(ctx, key)
// share already exists
if err == nil {
return nil, errtypes.AlreadyExists(key.String())
}
now := time.Now().Unix()
ts := &typespb.Timestamp{
Seconds: uint64(now),
}
owner, err := m.userConverter.UserIDToUserName(ctx, md.Owner)
if err != nil {
return nil, err
}
shareType, shareWith, err := m.formatGrantee(ctx, g.Grantee)
if err != nil {
return nil, err
}
itemType := resourceTypeToItem(md.Type)
targetPath := path.Join("/", path.Base(md.Path))
permissions := sharePermToInt(g.Permissions.Permissions)
itemSource := md.Id.OpaqueId
fileSource, err := strconv.ParseUint(itemSource, 10, 64)
if err != nil {
// it can be the case that the item source may be a character string
// we leave fileSource blank in that case
fileSource = 0
}
stmtString := "INSERT INTO oc_share (share_type,uid_owner,uid_initiator,item_type,item_source,file_source,permissions,stime,share_with,file_target) VALUES (?,?,?,?,?,?,?,?,?,?)"
stmtValues := []interface{}{shareType, owner, user.Username, itemType, itemSource, fileSource, permissions, now, shareWith, targetPath}
stmt, err := m.db.Prepare(stmtString)
if err != nil {
return nil, err
}
result, err := stmt.ExecContext(ctx, stmtValues...)
if err != nil {
return nil, err
}
lastID, err := result.LastInsertId()
if err != nil {
return nil, err
}
return &collaboration.Share{
Id: &collaboration.ShareId{
OpaqueId: strconv.FormatInt(lastID, 10),
},
ResourceId: md.Id,
Permissions: g.Permissions,
Grantee: g.Grantee,
Owner: md.Owner,
Creator: user.Id,
Ctime: ts,
Mtime: ts,
}, nil
}
func (m *mgr) GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) {
var s *collaboration.Share
var err error
switch {
case ref.GetId() != nil:
s, err = m.getByID(ctx, ref.GetId())
case ref.GetKey() != nil:
s, err = m.getByKey(ctx, ref.GetKey())
default:
err = errtypes.NotFound(ref.String())
}
if err != nil {
return nil, err
}
return s, nil
}
func (m *mgr) Unshare(ctx context.Context, ref *collaboration.ShareReference) error {
uid := ctxpkg.ContextMustGetUser(ctx).Username
var query string
params := []interface{}{}
switch {
case ref.GetId() != nil:
query = "DELETE FROM oc_share where id=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, ref.GetId().OpaqueId, uid, uid)
case ref.GetKey() != nil:
key := ref.GetKey()
shareType, shareWith, err := m.formatGrantee(ctx, key.Grantee)
if err != nil {
return err
}
owner := formatUserID(key.Owner)
query = "DELETE FROM oc_share WHERE uid_owner=? AND file_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, owner, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid)
default:
return errtypes.NotFound(ref.String())
}
stmt, err := m.db.Prepare(query)
if err != nil {
return err
}
res, err := stmt.ExecContext(ctx, params...)
if err != nil {
return err
}
rowCnt, err := res.RowsAffected()
if err != nil {
return err
}
if rowCnt == 0 {
return errtypes.NotFound(ref.String())
}
return nil
}
func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference, p *collaboration.SharePermissions, updated *collaboration.Share, fieldMask *field_mask.FieldMask) (*collaboration.Share, error) {
permissions := sharePermToInt(p.Permissions)
uid := ctxpkg.ContextMustGetUser(ctx).Username
var query string
params := []interface{}{}
switch {
case ref.GetId() != nil:
query = "update oc_share set permissions=?,stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, permissions, time.Now().Unix(), ref.GetId().OpaqueId, uid, uid)
case ref.GetKey() != nil:
key := ref.GetKey()
shareType, shareWith, err := m.formatGrantee(ctx, key.Grantee)
if err != nil {
return nil, err
}
owner := formatUserID(key.Owner)
query = "update oc_share set permissions=?,stime=? where (uid_owner=? or uid_initiator=?) AND file_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, permissions, time.Now().Unix(), owner, owner, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid)
default:
return nil, errtypes.NotFound(ref.String())
}
stmt, err := m.db.Prepare(query)
if err != nil {
return nil, err
}
if _, err = stmt.ExecContext(ctx, params...); err != nil {
return nil, err
}
return m.GetShare(ctx, ref)
}
func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) {
uid := ctxpkg.ContextMustGetUser(ctx).Username
query := `
SELECT
coalesce(s.uid_owner, '') as uid_owner, coalesce(s.uid_initiator, '') as uid_initiator,
coalesce(s.share_with, '') as share_with, coalesce(s.file_source, '') as file_source,
s.file_target, s.id, s.stime, s.permissions, s.share_type, fc.storage as storage
FROM oc_share s
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
WHERE (uid_owner=? or uid_initiator=?)
`
params := []interface{}{uid, uid}
var (
filterQuery string
filterParams []interface{}
err error
)
if len(filters) == 0 {
filterQuery += "(share_type=? OR share_type=?)"
params = append(params, shareTypeUser)
params = append(params, shareTypeGroup)
} else {
filterQuery, filterParams, err = translateFilters(filters)
if err != nil {
return nil, err
}
params = append(params, filterParams...)
}
if filterQuery != "" {
query = fmt.Sprintf("%s AND (%s)", query, filterQuery)
}
rows, err := m.db.QueryContext(ctx, query, params...)
if err != nil {
return nil, err
}
defer rows.Close()
var s DBShare
shares := []*collaboration.Share{}
for rows.Next() {
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.ItemStorage); err != nil {
continue
}
share, err := m.convertToCS3Share(ctx, s, m.storageMountID)
if err != nil {
return nil, err
}
shares = append(shares, share)
}
if err = rows.Err(); err != nil {
return nil, err
}
return shares, nil
}
// we list the shares that are targeted to the user in context or to the user groups.
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, forUser *userpb.UserId) ([]*collaboration.ReceivedShare, error) {
user := ctxpkg.ContextMustGetUser(ctx)
if user.GetId().GetType() == userpb.UserType_USER_TYPE_SERVICE {
u, err := m.userConverter.GetUser(forUser)
if err != nil {
return nil, errtypes.BadRequest("user not found")
}
user = u
}
uid := user.Username
params := []interface{}{uid, uid, uid}
for _, v := range user.Groups {
params = append(params, v)
}
homeConcat := ""
if m.driver == "mysql" { // mysql concat
homeConcat = "storages.id = CONCAT('home::', s.uid_owner)"
} else { // sqlite3 concat
homeConcat = "storages.id = 'home::' || s.uid_owner"
}
userSelect := ""
if len(user.Groups) > 0 {
userSelect = "AND ((share_type != 1 AND share_with=?) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
} else {
userSelect = "AND (share_type != 1 AND share_with=?)"
}
query := `
WITH results AS
(
SELECT s.*, storages.numeric_id FROM oc_share s
LEFT JOIN oc_storages storages ON ` + homeConcat + `
WHERE (uid_owner != ? AND uid_initiator != ?) ` + userSelect + `
)
SELECT COALESCE(r.uid_owner, '') AS uid_owner, COALESCE(r.uid_initiator, '') AS uid_initiator, COALESCE(r.share_with, '')
AS share_with, COALESCE(r.file_source, '') AS file_source, COALESCE(r2.file_target, r.file_target), r.id, r.stime, r.permissions, r.share_type, COALESCE(r2.accepted, r.accepted),
r.numeric_id, COALESCE(r.parent, -1) AS parent FROM results r LEFT JOIN results r2 ON r.id = r2.parent WHERE r.parent IS NULL`
filterQuery, filterParams, err := translateFilters(filters)
if err != nil {
return nil, err
}
params = append(params, filterParams...)
if filterQuery != "" {
query = fmt.Sprintf("%s AND (%s)", query, filterQuery)
}
query += ";"
rows, err := m.db.QueryContext(ctx, query, params...)
if err != nil {
return nil, err
}
defer rows.Close()
var s DBShare
shares := []*collaboration.ReceivedShare{}
for rows.Next() {
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State, &s.ItemStorage, &s.Parent); err != nil {
continue
}
share, err := m.convertToCS3ReceivedShare(ctx, s, m.storageMountID)
if err != nil {
return nil, err
}
shares = append(shares, share)
}
if err = rows.Err(); err != nil {
return nil, err
}
return shares, nil
}
func (m *mgr) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
var s *collaboration.ReceivedShare
var err error
switch {
case ref.GetId() != nil:
s, err = m.getReceivedByID(ctx, ref.GetId())
case ref.GetKey() != nil:
s, err = m.getReceivedByKey(ctx, ref.GetKey())
default:
err = errtypes.NotFound(ref.String())
}
if err != nil {
return nil, err
}
return s, nil
}
func (m *mgr) UpdateReceivedShare(ctx context.Context, receivedShare *collaboration.ReceivedShare, fieldMask *field_mask.FieldMask, _ *userpb.UserId) (*collaboration.ReceivedShare, error) {
// TODO: How to inject the uid when a UserId is set? override it in the ctx? Add parameter to GetReceivedShare?
rs, err := m.GetReceivedShare(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: receivedShare.Share.Id}})
if err != nil {
return nil, err
}
fields := []string{}
params := []interface{}{}
for i := range fieldMask.Paths {
switch fieldMask.Paths[i] {
case "state":
rs.State = receivedShare.State
fields = append(fields, "accepted=?")
switch rs.State {
case collaboration.ShareState_SHARE_STATE_REJECTED:
params = append(params, 2)
case collaboration.ShareState_SHARE_STATE_ACCEPTED:
params = append(params, 0)
}
case "mount_point":
fields = append(fields, "file_target=?")
rs.MountPoint = receivedShare.MountPoint
params = append(params, rs.MountPoint.Path)
case "hidden":
continue
default:
return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported")
}
}
if len(fields) == 0 {
return nil, fmt.Errorf("no valid field provided in the fieldmask")
}
updateReceivedShare := func(column string) error {
query := "update oc_share set "
query += strings.Join(fields, ",")
query += fmt.Sprintf(" where %s=?", column)
params := append(params, rs.Share.Id.OpaqueId)
stmt, err := m.db.Prepare(query)
if err != nil {
return err
}
res, err := stmt.ExecContext(ctx, params...)
if err != nil {
return err
}
affected, err := res.RowsAffected()
if err != nil {
return err
}
if affected < 1 {
return fmt.Errorf("No rows updated")
}
return nil
}
err = updateReceivedShare("parent") // Try to update the child state in case of group shares first
if err != nil {
err = updateReceivedShare("id")
}
if err != nil {
return nil, err
}
return rs, nil
}
func (m *mgr) getByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.Share, error) {
uid := ctxpkg.ContextMustGetUser(ctx).Username
s := DBShare{ID: id.OpaqueId}
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source, file_target, stime, permissions, share_type FROM oc_share WHERE id=? AND (uid_owner=? or uid_initiator=?)"
if err := m.db.QueryRowContext(ctx, query, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.STime, &s.Permissions, &s.ShareType); err != nil {
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(id.OpaqueId)
}
return nil, err
}
return m.convertToCS3Share(ctx, s, m.storageMountID)
}
func (m *mgr) getByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.Share, error) {
owner, err := m.userConverter.UserIDToUserName(ctx, key.Owner)
if err != nil {
return nil, err
}
uid := ctxpkg.ContextMustGetUser(ctx).Username
s := DBShare{}
shareType, shareWith, err := m.formatGrantee(ctx, key.Grantee)
if err != nil {
return nil, err
}
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source, file_target, id, stime, permissions, share_type FROM oc_share WHERE uid_owner=? AND file_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
if err = m.db.QueryRowContext(ctx, query, owner, key.ResourceId.StorageId, shareType, shareWith, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil {
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(key.String())
}
return nil, err
}
return m.convertToCS3Share(ctx, s, m.storageMountID)
}
func (m *mgr) getReceivedByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.ReceivedShare, error) {
user := ctxpkg.ContextMustGetUser(ctx)
uid := user.Username
params := []interface{}{id.OpaqueId, id.OpaqueId, uid}
for _, v := range user.Groups {
params = append(params, v)
}
homeConcat := ""
if m.driver == "mysql" { // mysql concat
homeConcat = "storages.id = CONCAT('home::', s.uid_owner)"
} else { // sqlite3 concat
homeConcat = "storages.id = 'home::' || s.uid_owner"
}
userSelect := ""
if len(user.Groups) > 0 {
userSelect = "AND ((share_type != 1 AND share_with=?) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
} else {
userSelect = "AND (share_type != 1 AND share_with=?)"
}
query := `
WITH results AS
(
SELECT s.*, storages.numeric_id
FROM oc_share s
LEFT JOIN oc_storages storages ON ` + homeConcat + `
WHERE s.id=? OR s.parent=? ` + userSelect + `
)
SELECT COALESCE(r.uid_owner, '') AS uid_owner, COALESCE(r.uid_initiator, '') AS uid_initiator, COALESCE(r.share_with, '')
AS share_with, COALESCE(r.file_source, '') AS file_source, COALESCE(r2.file_target, r.file_target), r.id, r.stime, r.permissions, r.share_type, COALESCE(r2.accepted, r.accepted),
r.numeric_id, COALESCE(r.parent, -1) AS parent
FROM results r
LEFT JOIN results r2 ON r.id = r2.parent
WHERE r.parent IS NULL;
`
s := DBShare{}
if err := m.db.QueryRowContext(ctx, query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State, &s.ItemStorage, &s.Parent); err != nil {
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(id.OpaqueId)
}
return nil, err
}
return m.convertToCS3ReceivedShare(ctx, s, m.storageMountID)
}
func (m *mgr) getReceivedByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.ReceivedShare, error) {
user := ctxpkg.ContextMustGetUser(ctx)
uid := user.Username
shareType, shareWith, err := m.formatGrantee(ctx, key.Grantee)
if err != nil {
return nil, err
}
params := []interface{}{uid, formatUserID(key.Owner), key.ResourceId.StorageId, key.ResourceId.OpaqueId, shareType, shareWith, shareWith}
for _, v := range user.Groups {
params = append(params, v)
}
s := DBShare{}
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source, file_target, ts.id, stime, permissions, share_type, accepted FROM oc_share ts WHERE uid_owner=? AND file_source=? AND share_type=? AND share_with=? "
if len(user.Groups) > 0 {
query += "AND (share_with=? OR share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + "))"
} else {
query += "AND (share_with=?)"
}
if err := m.db.QueryRowContext(ctx, query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil {
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(key.String())
}
return nil, err
}
return m.convertToCS3ReceivedShare(ctx, s, m.storageMountID)
}
func granteeTypeToShareType(granteeType provider.GranteeType) int {
switch granteeType {
case provider.GranteeType_GRANTEE_TYPE_USER:
return shareTypeUser
case provider.GranteeType_GRANTEE_TYPE_GROUP:
return shareTypeGroup
}
return -1
}
// translateFilters translates the filters to sql queries
func translateFilters(filters []*collaboration.Filter) (string, []interface{}, error) {
var (
filterQuery string
params []interface{}
)
groupedFilters := share.GroupFiltersByType(filters)
// If multiple filters of the same type are passed to this function, they need to be combined with the `OR` operator.
// That is why the filters got grouped by type.
// For every given filter type, iterate over the filters and if there are more than one combine them.
// Combine the different filter types using `AND`
var filterCounter = 0
for filterType, filters := range groupedFilters {
switch filterType {
case collaboration.Filter_TYPE_RESOURCE_ID:
filterQuery += "("
for i, f := range filters {
filterQuery += "file_source=?"
params = append(params, f.GetResourceId().OpaqueId)
if i != len(filters)-1 {
filterQuery += " OR "
}
}
filterQuery += ")"
case collaboration.Filter_TYPE_GRANTEE_TYPE:
filterQuery += "("
for i, f := range filters {
filterQuery += "r.share_type=?"
params = append(params, granteeTypeToShareType(f.GetGranteeType()))
if i != len(filters)-1 {
filterQuery += " OR "
}
}
filterQuery += ")"
case collaboration.Filter_TYPE_EXCLUDE_DENIALS:
// TODO this may change once the mapping of permission to share types is completed (cf. pkg/cbox/utils/conversions.go)
filterQuery += "r.permissions > 0"
default:
return "", nil, fmt.Errorf("filter type is not supported")
}
if filterCounter != len(groupedFilters)-1 {
filterQuery += " AND "
}
filterCounter++
}
return filterQuery, params, nil
}

View File

@@ -32,7 +32,6 @@ import (
_ "github.com/owncloud/reva/v2/pkg/storage/fs/localhome"
_ "github.com/owncloud/reva/v2/pkg/storage/fs/nextcloud"
_ "github.com/owncloud/reva/v2/pkg/storage/fs/ocis"
_ "github.com/owncloud/reva/v2/pkg/storage/fs/owncloudsql"
_ "github.com/owncloud/reva/v2/pkg/storage/fs/s3"
_ "github.com/owncloud/reva/v2/pkg/storage/fs/s3ng"
// Add your own here

View File

@@ -1,761 +0,0 @@
// Copyright 2018-2021 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.
package filecache
import (
"context"
"crypto/md5"
"database/sql"
"encoding/hex"
"fmt"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/owncloud/reva/v2/pkg/conversions"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
// Provides mysql drivers
_ "github.com/go-sql-driver/mysql"
)
// Cache represents a oc10-style file cache
type Cache struct {
driver string
db *sql.DB
}
// Storage represents a storage entry in the database
type Storage struct {
ID string
NumericID int
}
// File represents an entry of the file cache
type File struct {
ID int
Storage int
Parent int
MimePart int
MimeType int
MimeTypeString string
Size int
MTime int
StorageMTime int
UnencryptedSize int
Permissions int
Encrypted bool
Path string
Name string
Etag string
Checksum string
}
// TrashItem represents a trash item of the file cache
type TrashItem struct {
ID int
Name string
User string
Path string
Timestamp int
}
// Scannable describes the interface providing a Scan method
type Scannable interface {
Scan(...interface{}) error
}
// NewMysql returns a new Cache instance connecting to a MySQL database
func NewMysql(dsn string) (*Cache, error) {
sqldb, err := sql.Open("mysql", dsn)
if err != nil {
return nil, errors.Wrap(err, "error connecting to the database")
}
// FIXME make configurable
sqldb.SetConnMaxLifetime(time.Minute * 3)
sqldb.SetConnMaxIdleTime(time.Second * 30)
sqldb.SetMaxOpenConns(100)
sqldb.SetMaxIdleConns(10)
err = sqldb.Ping()
if err != nil {
return nil, errors.Wrap(err, "error connecting to the database")
}
return New("mysql", sqldb)
}
// New returns a new Cache instance connecting to the given sql.DB
func New(driver string, sqldb *sql.DB) (*Cache, error) {
return &Cache{
driver: driver,
db: sqldb,
}, nil
}
// ListStorages returns the list of numeric ids of all storages
// Optionally only home storages are considered
func (c *Cache) ListStorages(ctx context.Context, onlyHome bool) ([]*Storage, error) {
query := ""
if onlyHome {
mountPointConcat := ""
if c.driver == "mysql" {
mountPointConcat = "m.mount_point = CONCAT('/', m.user_id, '/')"
} else { // sqlite3
mountPointConcat = "m.mount_point = '/' || m.user_id || '/'"
}
query = "SELECT s.id, s.numeric_id FROM oc_storages s JOIN oc_mounts m ON s.numeric_id = m.storage_id WHERE " + mountPointConcat
} else {
query = "SELECT id, numeric_id FROM oc_storages"
}
rows, err := c.db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
storages := []*Storage{}
for rows.Next() {
storage := &Storage{}
err := rows.Scan(&storage.ID, &storage.NumericID)
if err != nil {
return nil, err
}
storages = append(storages, storage)
}
return storages, nil
}
// GetStorage returns the storage with the given numeric id
func (c *Cache) GetStorage(ctx context.Context, numeridID interface{}) (*Storage, error) {
numericID, err := toIntID(numeridID)
if err != nil {
return nil, err
}
row := c.db.QueryRowContext(ctx, "SELECT id, numeric_id FROM oc_storages WHERE numeric_id = ?", numericID)
s := &Storage{}
switch err := row.Scan(&s.ID, &s.NumericID); err {
case nil:
return s, nil
default:
return nil, err
}
}
// GetNumericStorageID returns the database id for the given storage
func (c *Cache) GetNumericStorageID(ctx context.Context, id string) (int, error) {
row := c.db.QueryRowContext(ctx, "SELECT numeric_id FROM oc_storages WHERE id = ?", id)
var nid int
switch err := row.Scan(&nid); err {
case nil:
return nid, nil
default:
return -1, err
}
}
// CreateStorage creates a new storage and returns its numeric id
func (c *Cache) CreateStorage(ctx context.Context, id string) (int, error) {
tx, err := c.db.Begin()
if err != nil {
return -1, err
}
defer func() { _ = tx.Rollback() }()
stmt, err := tx.PrepareContext(ctx, "INSERT INTO oc_storages(id) VALUES(?)")
if err != nil {
return -1, err
}
defer stmt.Close()
res, err := stmt.ExecContext(ctx, id)
if err != nil {
return -1, err
}
insertedID, err := res.LastInsertId()
if err != nil {
return -1, err
}
data := map[string]interface{}{
"path": "",
"etag": "",
"mimetype": "httpd/unix-directory",
}
_, err = c.doInsertOrUpdate(ctx, tx, int(insertedID), data, true)
if err != nil {
return -1, err
}
err = tx.Commit()
if err != nil {
return -1, err
}
return int(insertedID), err
}
// GetStorageOwner returns the username of the owner of the given storage
func (c *Cache) GetStorageOwner(ctx context.Context, numericID interface{}) (string, error) {
numericID, err := toIntID(numericID)
if err != nil {
return "", err
}
row := c.db.QueryRowContext(ctx, "SELECT id FROM oc_storages WHERE numeric_id = ?", numericID)
var id string
switch err := row.Scan(&id); err {
case nil:
return strings.TrimPrefix(id, "home::"), nil
default:
return "", err
}
}
// GetStorageOwnerByFileID returns the username of the owner of the given entry
func (c *Cache) GetStorageOwnerByFileID(ctx context.Context, numericID interface{}) (string, error) {
numericID, err := toIntID(numericID)
if err != nil {
return "", err
}
row := c.db.QueryRowContext(ctx, "SELECT id FROM oc_storages storages, oc_filecache cache WHERE storages.numeric_id = cache.storage AND cache.fileid = ?", numericID)
var id string
switch err := row.Scan(&id); err {
case nil:
return strings.TrimPrefix(id, "home::"), nil
default:
return "", err
}
}
func (c *Cache) rowToFile(row Scannable) (*File, error) {
var fileid, storage, parent, mimetype, mimepart, size, mtime, storageMtime, encrypted, unencryptedSize int
var permissions sql.NullInt32
var path, name, etag, checksum, mimetypestring sql.NullString
err := row.Scan(&fileid, &storage, &path, &parent, &permissions, &mimetype, &mimepart, &mimetypestring, &size, &mtime, &storageMtime, &encrypted, &unencryptedSize, &name, &etag, &checksum)
if err != nil {
return nil, err
}
return &File{
ID: fileid,
Storage: storage,
Path: path.String,
Parent: parent,
Permissions: int(permissions.Int32),
MimeType: mimetype,
MimeTypeString: mimetypestring.String,
MimePart: mimepart,
Size: size,
MTime: mtime,
StorageMTime: storageMtime,
Encrypted: encrypted == 1,
UnencryptedSize: unencryptedSize,
Name: name.String,
Etag: etag.String,
Checksum: checksum.String,
}, nil
}
// Get returns the cache entry for the specified storage/path
func (c *Cache) Get(ctx context.Context, s interface{}, p string) (*File, error) {
storageID, err := toIntID(s)
if err != nil {
return nil, err
}
phashBytes := md5.Sum([]byte(p))
phash := hex.EncodeToString(phashBytes[:])
row := c.db.QueryRowContext(ctx, `
SELECT
fc.fileid, fc.storage, fc.path, fc.parent, fc.permissions, fc.mimetype, fc.mimepart,
mt.mimetype, fc.size, fc.mtime, fc.storage_mtime, fc.encrypted, fc.unencrypted_size,
fc.name, fc.etag, fc.checksum
FROM oc_filecache fc
LEFT JOIN oc_mimetypes mt ON fc.mimetype = mt.id
WHERE path_hash = ? AND storage = ?`, phash, storageID)
return c.rowToFile(row)
}
// Path returns the path for the specified entry
func (c *Cache) Path(ctx context.Context, id interface{}) (string, error) {
id, err := toIntID(id)
if err != nil {
return "", err
}
row := c.db.QueryRowContext(ctx, "SELECT path FROM oc_filecache WHERE fileid = ?", id)
var path string
err = row.Scan(&path)
if err != nil {
return "", err
}
return path, nil
}
// List returns the list of entries below the given path
func (c *Cache) List(ctx context.Context, storage interface{}, p string) ([]*File, error) {
storageID, err := toIntID(storage)
if err != nil {
return nil, err
}
var rows *sql.Rows
phash := fmt.Sprintf("%x", md5.Sum([]byte(strings.Trim(p, "/"))))
rows, err = c.db.QueryContext(ctx, `
SELECT
fc.fileid, fc.storage, fc.path, fc.parent, fc.permissions, fc.mimetype, fc.mimepart,
mt.mimetype, fc.size, fc.mtime, fc.storage_mtime, fc.encrypted, fc.unencrypted_size,
fc.name, fc.etag, fc.checksum
FROM oc_filecache fc
LEFT JOIN oc_mimetypes mt ON fc.mimetype = mt.id
WHERE storage = ? AND parent = (SELECT fileid FROM oc_filecache WHERE storage = ? AND path_hash=?) AND name IS NOT NULL
`, storageID, storageID, phash)
if err != nil {
return nil, err
}
defer rows.Close()
entries := []*File{}
for rows.Next() {
entry, err := c.rowToFile(rows)
if err != nil {
return nil, err
}
entries = append(entries, entry)
}
return entries, nil
}
// Permissions returns the permissions for the specified storage/path
func (c *Cache) Permissions(ctx context.Context, storage interface{}, p string) (*provider.ResourcePermissions, error) {
entry, err := c.Get(ctx, storage, p)
if err != nil {
return nil, err
}
perms, err := conversions.NewPermissions(entry.Permissions)
if err != nil {
return nil, err
}
return conversions.RoleFromOCSPermissions(perms, nil).CS3ResourcePermissions(), nil
}
// InsertOrUpdate creates or updates a cache entry
func (c *Cache) InsertOrUpdate(ctx context.Context, storage interface{}, data map[string]interface{}, allowEmptyParent bool) (int, error) {
tx, err := c.db.Begin()
if err != nil {
return -1, err
}
defer func() { _ = tx.Rollback() }()
id, err := c.doInsertOrUpdate(ctx, tx, storage, data, allowEmptyParent)
if err != nil {
return -1, err
}
err = tx.Commit()
if err != nil {
return -1, err
}
return id, err
}
func (c *Cache) doInsertOrUpdate(ctx context.Context, tx *sql.Tx, storage interface{}, data map[string]interface{}, allowEmptyParent bool) (int, error) {
storageID, err := toIntID(storage)
if err != nil {
return -1, err
}
columns := []string{"storage"}
placeholders := []string{"?"}
values := []interface{}{storage}
for _, key := range []string{"path", "mimetype", "etag"} {
if _, exists := data[key]; !exists {
return -1, fmt.Errorf("missing required data")
}
}
path := data["path"].(string)
data["name"] = filepath.Base(path)
if data["name"] == "." {
data["name"] = ""
}
parentPath := strings.TrimRight(filepath.Dir(path), "/")
if parentPath == "." {
parentPath = ""
}
if path == "" {
data["parent"] = -1
} else {
parent, err := c.Get(ctx, storageID, parentPath)
if err == nil {
data["parent"] = parent.ID
} else {
if allowEmptyParent {
data["parent"] = -1
} else {
return -1, fmt.Errorf("could not find parent %s, %s, %v, %w", parentPath, path, parent, err)
}
}
}
if _, exists := data["checksum"]; !exists {
data["checksum"] = ""
}
for k, v := range data {
switch k {
case "path":
phashBytes := md5.Sum([]byte(v.(string)))
phash := hex.EncodeToString(phashBytes[:])
columns = append(columns, "path_hash")
values = append(values, phash)
placeholders = append(placeholders, "?")
case "storage_mtime":
if _, exists := data["mtime"]; !exists {
columns = append(columns, "mtime")
values = append(values, v)
placeholders = append(placeholders, "?")
}
case "mimetype":
parts := strings.Split(v.(string), "/")
columns = append(columns, "mimetype")
values = append(values, v)
placeholders = append(placeholders, "(SELECT id FROM oc_mimetypes WHERE mimetype=?)")
columns = append(columns, "mimepart")
values = append(values, parts[0])
placeholders = append(placeholders, "(SELECT id FROM oc_mimetypes WHERE mimetype=?)")
continue
}
columns = append(columns, k)
values = append(values, v)
placeholders = append(placeholders, "?")
}
err = c.insertMimetype(ctx, tx, data["mimetype"].(string))
if err != nil {
return -1, err
}
query := "INSERT INTO oc_filecache( " + strings.Join(columns, ", ") + ") VALUES(" + strings.Join(placeholders, ",") + ")"
updates := []string{}
for i, column := range columns {
if column != "path" && column != "path_hash" && column != "storage" {
updates = append(updates, column+"="+placeholders[i])
values = append(values, values[i])
}
}
if c.driver == "mysql" { // mysql upsert
query += " ON DUPLICATE KEY UPDATE "
} else { // sqlite3 upsert
query += " ON CONFLICT(storage,path_hash) DO UPDATE SET "
}
query += strings.Join(updates, ",")
stmt, err := tx.PrepareContext(ctx, query)
if err != nil {
return -1, err
}
res, err := stmt.ExecContext(ctx, values...)
if err != nil {
log.Err(err).Msg("could not store filecache item")
return -1, err
}
id, err := res.LastInsertId()
if err != nil {
return -1, err
}
return int(id), nil
}
// Copy creates a copy of the specified entry at the target path
func (c *Cache) Copy(ctx context.Context, storage interface{}, sourcePath, targetPath string) (int, error) {
storageID, err := toIntID(storage)
if err != nil {
return -1, err
}
source, err := c.Get(ctx, storageID, sourcePath)
if err != nil {
return -1, errors.Wrap(err, "could not find source")
}
row := c.db.QueryRowContext(ctx, "SELECT mimetype FROM oc_mimetypes WHERE id=?", source.MimeType)
var mimetype string
err = row.Scan(&mimetype)
if err != nil {
return -1, errors.Wrap(err, "could not find source mimetype")
}
data := map[string]interface{}{
"path": targetPath,
"checksum": source.Checksum,
"mimetype": mimetype,
"permissions": source.Permissions,
"etag": source.Etag,
"size": source.Size,
"mtime": source.MTime,
"storage_mtime": source.StorageMTime,
"encrypted": source.Encrypted,
"unencrypted_size": source.UnencryptedSize,
}
return c.InsertOrUpdate(ctx, storage, data, false)
}
// Move moves the specified entry to the target path
func (c *Cache) Move(ctx context.Context, storage interface{}, sourcePath, targetPath string) error {
storageID, err := toIntID(storage)
if err != nil {
return err
}
source, err := c.Get(ctx, storageID, sourcePath)
if err != nil {
return errors.Wrap(err, "could not find source")
}
newParentPath := strings.TrimRight(filepath.Dir(targetPath), "/")
newParent, err := c.Get(ctx, storageID, newParentPath)
if err != nil {
return errors.Wrap(err, "could not find new parent")
}
tx, err := c.db.Begin()
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
stmt, err := tx.Prepare("UPDATE oc_filecache SET parent=?, path=?, name=?, path_hash=? WHERE storage = ? AND fileid=?")
if err != nil {
return err
}
defer stmt.Close()
phashBytes := md5.Sum([]byte(targetPath))
_, err = stmt.ExecContext(ctx, newParent.ID, targetPath, filepath.Base(targetPath), hex.EncodeToString(phashBytes[:]), storageID, source.ID)
if err != nil {
return err
}
childRows, err := tx.QueryContext(ctx, "SELECT fileid, path FROM oc_filecache WHERE parent = ?", source.ID)
if err != nil {
return err
}
defer childRows.Close()
children := map[int]string{}
for childRows.Next() {
var (
id int
path string
)
err = childRows.Scan(&id, &path)
if err != nil {
return err
}
children[id] = path
}
for id, path := range children {
path = strings.ReplaceAll(path, sourcePath, targetPath)
phashBytes = md5.Sum([]byte(path))
_, err = stmt.ExecContext(ctx, source.ID, path, filepath.Base(path), hex.EncodeToString(phashBytes[:]), storageID, id)
if err != nil {
return err
}
}
return tx.Commit()
}
// Purge removes the specified storage/path from the cache without putting it into the trash
func (c *Cache) Purge(ctx context.Context, storage interface{}, path string) error {
storageID, err := toIntID(storage)
if err != nil {
return err
}
phashBytes := md5.Sum([]byte(path))
phash := hex.EncodeToString(phashBytes[:])
_, err = c.db.ExecContext(ctx, "DELETE FROM oc_filecache WHERE storage = ? and path_hash = ?", storageID, phash)
return err
}
// Delete removes the specified storage/path from the cache
func (c *Cache) Delete(ctx context.Context, storage interface{}, user, path, trashPath string) error {
err := c.Move(ctx, storage, path, trashPath)
if err != nil {
return err
}
re := regexp.MustCompile(`(.*)\.d(\d+)$`)
parts := re.FindStringSubmatch(filepath.Base(trashPath))
query := "INSERT INTO oc_files_trash(user,id,timestamp,location) VALUES(?,?,?,?)"
stmt, err := c.db.PrepareContext(ctx, query)
if err != nil {
return err
}
relativeLocation, err := filepath.Rel("files/", filepath.Dir(path))
if err != nil {
return err
}
_, err = stmt.ExecContext(ctx, user, filepath.Base(parts[1]), parts[2], relativeLocation)
if err != nil {
log.Err(err).Msg("could not store filecache item")
return err
}
return nil
}
// GetRecycleItem returns the specified recycle item
func (c *Cache) GetRecycleItem(ctx context.Context, user, path string, timestamp int) (*TrashItem, error) {
row := c.db.QueryRowContext(ctx, "SELECT auto_id, id, location FROM oc_files_trash WHERE id = ? AND user = ? AND timestamp = ?", path, user, timestamp)
var autoID int
var id, location string
err := row.Scan(&autoID, &id, &location)
if err != nil {
return nil, err
}
return &TrashItem{
ID: autoID,
Name: id,
User: user,
Path: location,
Timestamp: timestamp,
}, nil
}
// EmptyRecycle clears the recycle bin for the given user
func (c *Cache) EmptyRecycle(ctx context.Context, user string) error {
_, err := c.db.ExecContext(ctx, "DELETE FROM oc_files_trash WHERE user = ?", user)
if err != nil {
return err
}
storage, err := c.GetNumericStorageID(ctx, "home::"+user)
if err != nil {
return err
}
_, err = c.db.ExecContext(ctx, "DELETE FROM oc_filecache WHERE storage = ? AND PATH LIKE ?", storage, "files_trashbin/%")
return err
}
// DeleteRecycleItem deletes the specified item from the trash
func (c *Cache) DeleteRecycleItem(ctx context.Context, user, path string, timestamp int) error {
_, err := c.db.ExecContext(ctx, "DELETE FROM oc_files_trash WHERE id = ? AND user = ? AND timestamp = ?", path, user, timestamp)
return err
}
// PurgeRecycleItem deletes the specified item from the filecache and the trash
func (c *Cache) PurgeRecycleItem(ctx context.Context, user, path string, timestamp int, isVersionFile bool) error {
row := c.db.QueryRowContext(ctx, "SELECT auto_id, location FROM oc_files_trash WHERE id = ? AND user = ? AND timestamp = ?", path, user, timestamp)
var autoID int
var location string
err := row.Scan(&autoID, &location)
if err != nil {
return err
}
_, err = c.db.ExecContext(ctx, "DELETE FROM oc_files_trash WHERE auto_id=?", autoID)
if err != nil {
return err
}
storage, err := c.GetNumericStorageID(ctx, "home::"+user)
if err != nil {
return err
}
trashType := "files"
if isVersionFile {
trashType = "versions"
}
item, err := c.Get(ctx, storage, filepath.Join("files_trashbin", trashType, path+".d"+strconv.Itoa(timestamp)))
if err != nil {
return err
}
_, err = c.db.ExecContext(ctx, "DELETE FROM oc_filecache WHERE fileid=? OR parent=?", item.ID, item.ID)
return err
}
// SetEtag set a new etag for the specified item
func (c *Cache) SetEtag(ctx context.Context, storage interface{}, path, etag string) error {
storageID, err := toIntID(storage)
if err != nil {
return err
}
source, err := c.Get(ctx, storageID, path)
if err != nil {
return errors.Wrap(err, "could not find source")
}
stmt, err := c.db.PrepareContext(ctx, "UPDATE oc_filecache SET etag=? WHERE storage = ? AND fileid=?")
if err != nil {
return err
}
_, err = stmt.ExecContext(ctx, etag, storageID, source.ID)
return err
}
func (c *Cache) insertMimetype(ctx context.Context, tx *sql.Tx, mimetype string) error {
insertPart := func(v string) error {
stmt, err := tx.PrepareContext(ctx, "INSERT INTO oc_mimetypes(mimetype) VALUES(?)")
if err != nil {
return err
}
_, err = stmt.ExecContext(ctx, v)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE") || strings.Contains(err.Error(), "Error 1062") {
return nil // Already exists
}
return err
}
return nil
}
parts := strings.Split(mimetype, "/")
err := insertPart(parts[0])
if err != nil {
return err
}
return insertPart(mimetype)
}
func toIntID(rid interface{}) (int, error) {
switch t := rid.(type) {
case int:
return t, nil
case string:
return strconv.Atoi(t)
default:
return -1, fmt.Errorf("invalid type")
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,87 +0,0 @@
// Copyright 2018-2021 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.
//go:build !windows
// +build !windows
package owncloudsql
import (
"context"
"crypto/md5"
"encoding/binary"
"fmt"
"os"
"strings"
"syscall"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/owncloud/reva/v2/pkg/appctx"
)
// TODO(jfd) get rid of the differences between unix and windows. the inode and dev should never be used for the etag because it interferes with backups
// calcEtag will create an etag based on the md5 of
// - mtime,
// - inode (if available),
// - device (if available) and
// - size.
// errors are logged, but an etag will still be returned
func calcEtag(ctx context.Context, fi os.FileInfo) string {
log := appctx.GetLogger(ctx)
h := md5.New()
err := binary.Write(h, binary.BigEndian, fi.ModTime().UnixNano())
if err != nil {
log.Error().Err(err).Msg("error writing mtime")
}
stat, ok := fi.Sys().(*syscall.Stat_t)
if ok {
// take device and inode into account
err = binary.Write(h, binary.BigEndian, stat.Ino)
if err != nil {
log.Error().Err(err).Msg("error writing inode")
}
err = binary.Write(h, binary.BigEndian, stat.Dev)
if err != nil {
log.Error().Err(err).Msg("error writing device")
}
}
err = binary.Write(h, binary.BigEndian, fi.Size())
if err != nil {
log.Error().Err(err).Msg("error writing size")
}
etag := fmt.Sprintf("%x", h.Sum(nil))
return strings.Trim(etag, "\"")
}
func (fs *owncloudsqlfs) GetQuota(ctx context.Context, ref *provider.Reference) (uint64, uint64, uint64, error) {
// TODO quota of which storage space?
// we could use the logged in user, but when a user has access to multiple storages this falls short
// for now return quota of root
stat := syscall.Statfs_t{}
err := syscall.Statfs(fs.toInternalPath(ctx, "/"), &stat)
if err != nil {
return 0, 0, 0, err
}
// Total data blocks in filesystem
total := stat.Blocks * uint64(stat.Bsize)
// Free blocks available to unprivileged user
used := (stat.Blocks - uint64(stat.Bavail)) * uint64(stat.Bsize) //nolint:unconvert
remaining := total - used
return total, used, remaining, nil
}

View File

@@ -1,76 +0,0 @@
// Copyright 2018-2021 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.
//go:build windows
// +build windows
package owncloudsql
import (
"context"
"crypto/md5"
"encoding/binary"
"fmt"
"os"
"strings"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/owncloud/reva/v2/pkg/appctx"
"golang.org/x/sys/windows"
)
// calcEtag will create an etag based on the md5 of
// - mtime,
// - inode (if available),
// - device (if available) and
// - size.
// errors are logged, but an etag will still be returned
func calcEtag(ctx context.Context, fi os.FileInfo) string {
log := appctx.GetLogger(ctx)
h := md5.New()
err := binary.Write(h, binary.BigEndian, fi.ModTime().UnixNano())
if err != nil {
log.Error().Err(err).Msg("error writing mtime")
}
// device and inode have no meaning on windows
err = binary.Write(h, binary.BigEndian, fi.Size())
if err != nil {
log.Error().Err(err).Msg("error writing size")
}
etag := fmt.Sprintf(`"%x"`, h.Sum(nil))
return fmt.Sprintf("\"%s\"", strings.Trim(etag, "\""))
}
func (fs *owncloudsqlfs) GetQuota(ctx context.Context, ref *provider.Reference) (uint64, uint64, uint64, error) {
// TODO quota of which storage space?
// we could use the logged in user, but when a user has access to multiple storages this falls short
// for now return quota of root
var free, total, avail uint64
pathPtr, err := windows.UTF16PtrFromString(fs.toInternalPath(ctx, "/"))
if err != nil {
return 0, 0, 0, err
}
err = windows.GetDiskFreeSpaceEx(pathPtr, &avail, &total, &free)
if err != nil {
return 0, 0, 0, err
}
used := total - free
return total, used, free, nil
}

View File

@@ -1,193 +0,0 @@
// Copyright 2018-2021 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.
package owncloudsql
import (
"context"
"fmt"
"strconv"
"strings"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
ctxpkg "github.com/owncloud/reva/v2/pkg/ctx"
"github.com/owncloud/reva/v2/pkg/errtypes"
"github.com/owncloud/reva/v2/pkg/storage"
"github.com/owncloud/reva/v2/pkg/storage/fs/owncloudsql/filecache"
"github.com/owncloud/reva/v2/pkg/storagespace"
"github.com/owncloud/reva/v2/pkg/utils"
)
// ListStorageSpaces lists storage spaces according to the provided filters
func (fs *owncloudsqlfs) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter, unrestricted bool) ([]*provider.StorageSpace, error) {
var (
spaceID = "*"
// uid *userpb.UserId
)
filteringUnsupportedSpaceTypes := false
for i := range filter {
switch filter[i].Type {
case provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE:
t := filter[i].GetSpaceType()
filteringUnsupportedSpaceTypes = (t != "personal" && !strings.HasPrefix(t, "+"))
case provider.ListStorageSpacesRequest_Filter_TYPE_ID:
_, spaceID, _, _ = storagespace.SplitID(filter[i].GetId().OpaqueId)
case provider.ListStorageSpacesRequest_Filter_TYPE_USER:
// uid = filter[i].GetUser()
}
}
if filteringUnsupportedSpaceTypes {
// owncloudsql only supports personal spaces, no need to search for something else
return []*provider.StorageSpace{}, nil
}
spaces := []*provider.StorageSpace{}
if spaceID == "*" {
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errtypes.UserRequired("error getting user from context")
}
// if uid != nil && utils.UserIDEqual(uid, u.Id) {
// return nil, errtypes.PermissionDenied("cannot access space of other user?")
// }
space, err := fs.getPersonalSpace(ctx, u)
if err != nil {
return nil, err
}
spaces = append(spaces, space)
} else {
id, err := strconv.Atoi(spaceID)
if err != nil {
// non-numeric space id -> this request is not for us
return []*provider.StorageSpace{}, nil
}
space, err := fs.getSpaceByNumericID(ctx, id)
if err != nil {
return nil, err
}
spaces = append(spaces, space)
}
return spaces, nil
}
// CreateStorageSpace creates a storage space
func (fs *owncloudsqlfs) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) {
return nil, errtypes.NotSupported("unimplemented: CreateStorageSpace")
}
// UpdateStorageSpace updates a storage space
func (fs *owncloudsqlfs) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) {
return nil, errtypes.NotSupported("update storage space")
}
// DeleteStorageSpace deletes a storage space
func (fs *owncloudsqlfs) DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) (*storage.DeleteStorageSpaceResult, error) {
return nil, errtypes.NotSupported("delete storage space")
}
// Note: currently unused but will be used later
// func (fs *owncloudsqlfs) listAllPersonalSpaces(ctx context.Context) ([]*provider.StorageSpace, error) {
// storages, err := fs.filecache.ListStorages(true)
// if err != nil {
// return nil, err
// }
// spaces := []*provider.StorageSpace{}
// for _, storage := range storages {
// space, err := fs.storageToSpace(ctx, storage)
// if err != nil {
// return nil, err
// }
// spaces = append(spaces, space)
// }
// return spaces, nil
// }
func (fs *owncloudsqlfs) getPersonalSpace(ctx context.Context, owner *userpb.User) (*provider.StorageSpace, error) {
storageID, err := fs.filecache.GetNumericStorageID(ctx, "home::"+owner.Username)
if err != nil {
return nil, err
}
storage, err := fs.filecache.GetStorage(ctx, storageID)
if err != nil {
return nil, err
}
root, err := fs.filecache.Get(ctx, storage.NumericID, "")
if err != nil {
return nil, err
}
space := &provider.StorageSpace{
Id: &provider.StorageSpaceId{OpaqueId: strconv.Itoa(storage.NumericID)},
Root: &provider.ResourceId{
// return ownclouds numeric storage id as the space id!
SpaceId: strconv.Itoa(storage.NumericID),
OpaqueId: strconv.Itoa(root.ID),
},
Name: owner.Username,
SpaceType: "personal",
Mtime: &types.Timestamp{Seconds: uint64(root.MTime)},
Owner: owner,
}
space.Opaque = utils.AppendPlainToOpaque(space.Opaque, "spaceAlias", "personal/"+owner.Username)
space.Opaque = utils.AppendPlainToOpaque(space.Opaque, "etag", fmt.Sprintf(`"%s"`, root.Etag))
return space, nil
}
func (fs *owncloudsqlfs) getSpaceByNumericID(ctx context.Context, spaceID int) (*provider.StorageSpace, error) {
storage, err := fs.filecache.GetStorage(ctx, spaceID)
if err != nil {
return nil, err
}
if !strings.HasPrefix(storage.ID, "home::") {
return nil, fmt.Errorf("only personal spaces are supported")
}
return fs.storageToSpace(ctx, storage)
}
func (fs *owncloudsqlfs) storageToSpace(ctx context.Context, storage *filecache.Storage) (*provider.StorageSpace, error) {
root, err := fs.filecache.Get(ctx, storage.NumericID, "")
if err != nil {
return nil, err
}
ownerName := strings.TrimPrefix(storage.ID, "home::")
owner, err := fs.getUser(ctx, ownerName)
if err != nil {
return nil, err
}
space := &provider.StorageSpace{
Id: &provider.StorageSpaceId{OpaqueId: strconv.Itoa(storage.NumericID)},
Root: &provider.ResourceId{
// return ownclouds numeric storage id as the space id!
SpaceId: strconv.Itoa(storage.NumericID),
OpaqueId: strconv.Itoa(root.ID),
},
Name: owner.Username,
SpaceType: "personal",
Mtime: &types.Timestamp{Seconds: uint64(root.MTime)},
Owner: owner,
}
space.Opaque = utils.AppendPlainToOpaque(space.Opaque, "spaceAlias", "personal/"+owner.Username)
space.Opaque = utils.AppendPlainToOpaque(space.Opaque, "etag", fmt.Sprintf(`"%s"`, root.Etag))
return space, nil
}

View File

@@ -1,43 +0,0 @@
// Copyright 2018-2021 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.
package owncloudsql
import (
"context"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/owncloud/reva/v2/pkg/errtypes"
"github.com/owncloud/reva/v2/pkg/storage"
)
func (fs *owncloudsqlfs) MarkProcessing(ctx context.Context, ref *provider.Reference, processing bool, sessionID string) error {
return errtypes.NotSupported("op not supported")
}
func (fs *owncloudsqlfs) CommitUpload(_ context.Context, _ *provider.Reference, _ string, _ storage.UploadSource) error {
return errtypes.NotSupported("op not supported")
}
func (fs *owncloudsqlfs) PrepareUpload(_ context.Context, _ *provider.Reference, _ string, info storage.UploadInfo) (*storage.PrepareUploadResult, error) {
return &storage.PrepareUploadResult{VersionCreated: info.NodeExisted}, nil
}
func (fs *owncloudsqlfs) RollbackUpload(_ context.Context, _ *provider.Reference, _ string, _ storage.RollbackInfo) error {
return nil
}

View File

@@ -25,6 +25,5 @@ import (
_ "github.com/owncloud/reva/v2/pkg/user/manager/ldap"
_ "github.com/owncloud/reva/v2/pkg/user/manager/memory"
_ "github.com/owncloud/reva/v2/pkg/user/manager/nextcloud"
_ "github.com/owncloud/reva/v2/pkg/user/manager/owncloudsql"
// Add your own here
)

View File

@@ -1,228 +0,0 @@
// Copyright 2018-2021 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.
package accounts
import (
"context"
"database/sql"
"strings"
"time"
"github.com/owncloud/reva/v2/pkg/appctx"
"github.com/pkg/errors"
)
// Accounts represents oc10-style Accounts
type Accounts struct {
driver string
db *sql.DB
joinUsername, joinUUID, enableMedialSearch bool
selectSQL string
}
// NewMysql returns a new Cache instance connecting to a MySQL database
func NewMysql(dsn string, joinUsername, joinUUID, enableMedialSearch bool) (*Accounts, error) {
sqldb, err := sql.Open("mysql", dsn)
if err != nil {
return nil, errors.Wrap(err, "error connecting to the database")
}
// FIXME make configurable
sqldb.SetConnMaxLifetime(time.Minute * 3)
sqldb.SetConnMaxIdleTime(time.Second * 30)
sqldb.SetMaxOpenConns(100)
sqldb.SetMaxIdleConns(10)
err = sqldb.Ping()
if err != nil {
return nil, errors.Wrap(err, "error connecting to the database")
}
return New("mysql", sqldb, joinUsername, joinUUID, enableMedialSearch)
}
// New returns a new Cache instance connecting to the given sql.DB
func New(driver string, sqldb *sql.DB, joinUsername, joinUUID, enableMedialSearch bool) (*Accounts, error) {
sel := "SELECT id, email, user_id, display_name, quota, last_login, backend, home, state"
from := `
FROM oc_accounts a
`
if joinUsername {
sel += ", p.configvalue AS username"
from += `LEFT JOIN oc_preferences p
ON a.user_id=p.userid
AND p.appid='core'
AND p.configkey='username'`
} else {
// fallback to user_id as username
sel += ", user_id AS username"
}
if joinUUID {
sel += ", p2.configvalue AS ownclouduuid"
from += `LEFT JOIN oc_preferences p2
ON a.user_id=p2.userid
AND p2.appid='core'
AND p2.configkey='ownclouduuid'`
} else {
// fallback to user_id as ownclouduuid
sel += ", user_id AS ownclouduuid"
}
return &Accounts{
driver: driver,
db: sqldb,
joinUsername: joinUsername,
joinUUID: joinUUID,
enableMedialSearch: enableMedialSearch,
selectSQL: sel + from,
}, nil
}
// Account stores information about accounts.
type Account struct {
ID uint64
Email sql.NullString
UserID string
DisplayName sql.NullString
Quota sql.NullString
LastLogin int
Backend string
Home string
State int8
Username sql.NullString // optional comes from the oc_preferences
OwnCloudUUID sql.NullString // optional comes from the oc_preferences
}
func (as *Accounts) rowToAccount(ctx context.Context, row Scannable) (*Account, error) {
a := Account{}
if err := row.Scan(&a.ID, &a.Email, &a.UserID, &a.DisplayName, &a.Quota, &a.LastLogin, &a.Backend, &a.Home, &a.State, &a.Username, &a.OwnCloudUUID); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
return nil, err
}
return &a, nil
}
// Scannable describes the interface providing a Scan method
type Scannable interface {
Scan(...interface{}) error
}
// GetAccountByClaim fetches an account by mail, username or userid
func (as *Accounts) GetAccountByClaim(ctx context.Context, claim, value string) (*Account, error) {
// TODO align supported claims with rest driver and the others, maybe refactor into common mapping
var row *sql.Row
var where string
switch claim {
case "mail":
where = "WHERE a.email=?"
// case "uid":
// claim = m.c.Schema.UIDNumber
// case "gid":
// claim = m.c.Schema.GIDNumber
case "username":
if as.joinUsername {
where = "WHERE p.configvalue=?"
} else {
// use user_id as username
where = "WHERE a.user_id=?"
}
case "userid":
if as.joinUUID {
where = "WHERE p2.configvalue=?"
} else {
// use user_id as uuid
where = "WHERE a.user_id=?"
}
default:
return nil, errors.New("owncloudsql: invalid field " + claim)
}
row = as.db.QueryRowContext(ctx, as.selectSQL+where, value)
return as.rowToAccount(ctx, row)
}
func sanitizeWildcards(q string) string {
return strings.ReplaceAll(strings.ReplaceAll(q, "%", `\%`), "_", `\_`)
}
// FindAccounts searches userid, displayname and email using the given query. The Wildcard caracters % and _ are escaped.
func (as *Accounts) FindAccounts(ctx context.Context, query string) ([]Account, error) {
if as.enableMedialSearch {
query = "%" + sanitizeWildcards(query) + "%"
}
// TODO join oc_account_terms
where := "WHERE a.user_id LIKE ? OR a.display_name LIKE ? OR a.email LIKE ?"
args := []interface{}{query, query, query}
if as.joinUsername {
where += " OR p.configvalue LIKE ?"
args = append(args, query)
}
if as.joinUUID {
where += " OR p2.configvalue LIKE ?"
args = append(args, query)
}
rows, err := as.db.QueryContext(ctx, as.selectSQL+where, args...)
if err != nil {
return nil, err
}
defer rows.Close()
accounts := []Account{}
for rows.Next() {
a := Account{}
if err := rows.Scan(&a.ID, &a.Email, &a.UserID, &a.DisplayName, &a.Quota, &a.LastLogin, &a.Backend, &a.Home, &a.State, &a.Username, &a.OwnCloudUUID); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
continue
}
accounts = append(accounts, a)
}
if err = rows.Err(); err != nil {
return nil, err
}
return accounts, nil
}
// GetAccountGroups lasts the groups for an account
func (as *Accounts) GetAccountGroups(ctx context.Context, uid string) ([]string, error) {
rows, err := as.db.QueryContext(ctx, "SELECT gid FROM oc_group_user WHERE uid=?", uid)
if err != nil {
return nil, err
}
defer rows.Close()
groups := []string{}
for rows.Next() {
var group string
if err := rows.Scan(&group); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
continue
}
groups = append(groups, group)
}
if err = rows.Err(); err != nil {
return nil, err
}
return groups, nil
}

View File

@@ -1,189 +0,0 @@
// Copyright 2018-2021 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.
package owncloudsql
import (
"context"
"database/sql"
"fmt"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/owncloud/reva/v2/pkg/appctx"
"github.com/owncloud/reva/v2/pkg/errtypes"
"github.com/owncloud/reva/v2/pkg/user"
"github.com/owncloud/reva/v2/pkg/user/manager/owncloudsql/accounts"
"github.com/owncloud/reva/v2/pkg/user/manager/registry"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
// Provides mysql drivers
_ "github.com/go-sql-driver/mysql"
)
func init() {
registry.Register("owncloudsql", NewMysql)
}
type manager struct {
c *config
db *accounts.Accounts
}
type config struct {
DbUsername string `mapstructure:"dbusername"`
DbPassword string `mapstructure:"dbpassword"`
DbHost string `mapstructure:"dbhost"`
DbPort int `mapstructure:"dbport"`
DbName string `mapstructure:"dbname"`
Idp string `mapstructure:"idp"`
Nobody int64 `mapstructure:"nobody"`
JoinUsername bool `mapstructure:"join_username"`
JoinOwnCloudUUID bool `mapstructure:"join_ownclouduuid"`
EnableMedialSearch bool `mapstructure:"enable_medial_search"`
}
// NewMysql returns a new user manager connection to an owncloud mysql database
func NewMysql(m map[string]interface{}) (user.Manager, error) {
mgr := &manager{}
err := mgr.Configure(m)
if err != nil {
err = errors.Wrap(err, "error creating a new manager")
return nil, err
}
mgr.db, err = accounts.NewMysql(
fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", mgr.c.DbUsername, mgr.c.DbPassword, mgr.c.DbHost, mgr.c.DbPort, mgr.c.DbName),
mgr.c.JoinUsername,
mgr.c.JoinOwnCloudUUID,
mgr.c.EnableMedialSearch,
)
if err != nil {
return nil, err
}
return mgr, nil
}
func (m *manager) Configure(ml map[string]interface{}) error {
c, err := parseConfig(ml)
if err != nil {
return err
}
if c.Nobody == 0 {
c.Nobody = 99
}
m.c = c
return nil
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, &c); err != nil {
return nil, err
}
return c, nil
}
func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId, skipFetchingGroups bool) (*userpb.User, error) {
// search via the user_id
a, err := m.db.GetAccountByClaim(ctx, "userid", uid.OpaqueId)
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(uid.OpaqueId)
}
return m.convertToCS3User(ctx, a, skipFetchingGroups)
}
func (m *manager) GetUserByClaim(ctx context.Context, claim, value string, skipFetchingGroups bool) (*userpb.User, error) {
a, err := m.db.GetAccountByClaim(ctx, claim, value)
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(claim + "=" + value)
} else if err != nil {
return nil, err
}
return m.convertToCS3User(ctx, a, skipFetchingGroups)
}
func (m *manager) FindUsers(ctx context.Context, query string, skipFetchingGroups bool) ([]*userpb.User, error) {
accounts, err := m.db.FindAccounts(ctx, query)
if err == sql.ErrNoRows {
return nil, errtypes.NotFound("no users found for " + query)
} else if err != nil {
return nil, err
}
users := make([]*userpb.User, 0, len(accounts))
for i := range accounts {
u, err := m.convertToCS3User(ctx, &accounts[i], skipFetchingGroups)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Interface("account", accounts[i]).Msg("could not convert account, skipping")
continue
}
users = append(users, u)
}
return users, nil
}
func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) {
groups, err := m.db.GetAccountGroups(ctx, uid.OpaqueId)
if err == sql.ErrNoRows {
return nil, errtypes.NotFound("no groups found for uid " + uid.OpaqueId)
} else if err != nil {
return nil, err
}
return groups, nil
}
func (m *manager) convertToCS3User(ctx context.Context, a *accounts.Account, skipFetchingGroups bool) (*userpb.User, error) {
u := &userpb.User{
Id: &userpb.UserId{
Idp: m.c.Idp,
OpaqueId: a.OwnCloudUUID.String,
Type: userpb.UserType_USER_TYPE_PRIMARY,
},
Username: a.Username.String,
Mail: a.Email.String,
DisplayName: a.DisplayName.String,
//Groups: groups,
GidNumber: m.c.Nobody,
UidNumber: m.c.Nobody,
}
// https://github.com/owncloud/reva/pull/4135
// fall back to userid
if u.Id.OpaqueId == "" {
u.Id.OpaqueId = a.UserID
}
if u.Username == "" {
u.Username = u.Id.OpaqueId
}
if u.DisplayName == "" {
u.DisplayName = u.Id.OpaqueId
}
if !skipFetchingGroups {
var err error
if u.Groups, err = m.GetUserGroups(ctx, u.Id); err != nil {
return nil, err
}
}
return u, nil
}

View File

@@ -1,126 +0,0 @@
// Copyright The Prometheus Authors
// 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.
package util
import (
"errors"
"os"
"strconv"
"strings"
)
// ParseUint32s parses a slice of strings into a slice of uint32s.
func ParseUint32s(ss []string) ([]uint32, error) {
us := make([]uint32, 0, len(ss))
for _, s := range ss {
u, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return nil, err
}
us = append(us, uint32(u))
}
return us, nil
}
// ParseUint64s parses a slice of strings into a slice of uint64s.
func ParseUint64s(ss []string) ([]uint64, error) {
us := make([]uint64, 0, len(ss))
for _, s := range ss {
u, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return nil, err
}
us = append(us, u)
}
return us, nil
}
// ParsePInt64s parses a slice of strings into a slice of int64 pointers.
func ParsePInt64s(ss []string) ([]*int64, error) {
us := make([]*int64, 0, len(ss))
for _, s := range ss {
u, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return nil, err
}
us = append(us, &u)
}
return us, nil
}
// Parses a uint64 from given hex in string.
func ParseHexUint64s(ss []string) ([]*uint64, error) {
us := make([]*uint64, 0, len(ss))
for _, s := range ss {
u, err := strconv.ParseUint(s, 16, 64)
if err != nil {
return nil, err
}
us = append(us, &u)
}
return us, nil
}
// ReadUintFromFile reads a file and attempts to parse a uint64 from it.
func ReadUintFromFile(path string) (uint64, error) {
data, err := os.ReadFile(path)
if err != nil {
return 0, err
}
return strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
}
// ReadIntFromFile reads a file and attempts to parse a int64 from it.
func ReadIntFromFile(path string) (int64, error) {
data, err := os.ReadFile(path)
if err != nil {
return 0, err
}
return strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
}
// ParseBool parses a string into a boolean pointer.
func ParseBool(b string) *bool {
var truth bool
switch b {
case "enabled":
truth = true
case "disabled":
truth = false
default:
return nil
}
return &truth
}
// ReadHexFromFile reads a file and attempts to parse a uint64 from a hexadecimal format 0xXX.
func ReadHexFromFile(path string) (uint64, error) {
data, err := os.ReadFile(path)
if err != nil {
return 0, err
}
hexString := strings.TrimSpace(string(data))
if !strings.HasPrefix(hexString, "0x") {
return 0, errors.New("invalid format: hex string does not start with '0x'")
}
return strconv.ParseUint(hexString[2:], 16, 64)
}

View File

@@ -1,37 +0,0 @@
// Copyright The Prometheus Authors
// 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.
package util
import (
"io"
"os"
)
// ReadFileNoStat uses io.ReadAll to read contents of entire file.
// This is similar to os.ReadFile but without the call to os.Stat, because
// many files in /proc and /sys report incorrect file sizes (either 0 or 4096).
// Reads a max file size of 1024kB. For files larger than this, a scanner
// should be used.
func ReadFileNoStat(filename string) ([]byte, error) {
const maxBufferSize = 1024 * 1024
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
reader := io.LimitReader(f, maxBufferSize)
return io.ReadAll(reader)
}

View File

@@ -1,68 +0,0 @@
// Copyright The Prometheus Authors
// 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.
//go:build (linux || darwin) && !appengine
package util
import (
"bytes"
"os"
"strconv"
"strings"
"syscall"
)
// SysReadFile is a simplified os.ReadFile that invokes syscall.Read directly.
// https://github.com/prometheus/node_exporter/pull/728/files
//
// Note that this function will not read files larger than 128 bytes.
func SysReadFile(file string) (string, error) {
f, err := os.Open(file)
if err != nil {
return "", err
}
defer f.Close()
// On some machines, hwmon drivers are broken and return EAGAIN. This causes
// Go's os.ReadFile implementation to poll forever.
//
// Since we either want to read data or bail immediately, do the simplest
// possible read using syscall directly.
const sysFileBufferSize = 128
b := make([]byte, sysFileBufferSize)
n, err := syscall.Read(int(f.Fd()), b)
if err != nil {
return "", err
}
return string(bytes.TrimSpace(b[:n])), nil
}
// SysReadUintFromFile reads a file using SysReadFile and attempts to parse a uint64 from it.
func SysReadUintFromFile(path string) (uint64, error) {
data, err := SysReadFile(path)
if err != nil {
return 0, err
}
return strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
}
// SysReadIntFromFile reads a file using SysReadFile and attempts to parse a int64 from it.
func SysReadIntFromFile(path string) (int64, error) {
data, err := SysReadFile(path)
if err != nil {
return 0, err
}
return strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
}

View File

@@ -1,26 +0,0 @@
// Copyright The Prometheus Authors
// 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.
//go:build (linux && appengine) || (!linux && !darwin)
package util
import (
"fmt"
)
// SysReadFile is here implemented as a noop for builds that do not support
// the read syscall. For example Windows, or Linux on Google App Engine.
func SysReadFile(file string) (string, error) {
return "", fmt.Errorf("not supported on this platform")
}

View File

@@ -1,91 +0,0 @@
// Copyright The Prometheus Authors
// 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.
package util
import (
"strconv"
)
// TODO(mdlayher): util packages are an anti-pattern and this should be moved
// somewhere else that is more focused in the future.
// A ValueParser enables parsing a single string into a variety of data types
// in a concise and safe way. The Err method must be invoked after invoking
// any other methods to ensure a value was successfully parsed.
type ValueParser struct {
v string
err error
}
// NewValueParser creates a ValueParser using the input string.
func NewValueParser(v string) *ValueParser {
return &ValueParser{v: v}
}
// Int interprets the underlying value as an int and returns that value.
func (vp *ValueParser) Int() int { return int(vp.int64()) }
// PInt64 interprets the underlying value as an int64 and returns a pointer to
// that value.
func (vp *ValueParser) PInt64() *int64 {
if vp.err != nil {
return nil
}
v := vp.int64()
return &v
}
// int64 interprets the underlying value as an int64 and returns that value.
// TODO: export if/when necessary.
func (vp *ValueParser) int64() int64 {
if vp.err != nil {
return 0
}
// A base value of zero makes ParseInt infer the correct base using the
// string's prefix, if any.
const base = 0
v, err := strconv.ParseInt(vp.v, base, 64)
if err != nil {
vp.err = err
return 0
}
return v
}
// PUInt64 interprets the underlying value as an uint64 and returns a pointer to
// that value.
func (vp *ValueParser) PUInt64() *uint64 {
if vp.err != nil {
return nil
}
// A base value of zero makes ParseInt infer the correct base using the
// string's prefix, if any.
const base = 0
v, err := strconv.ParseUint(vp.v, base, 64)
if err != nil {
vp.err = err
return nil
}
return &v
}
// Err returns the last error, if any, encountered by the ValueParser.
func (vp *ValueParser) Err() error {
return vp.err
}

10
vendor/modules.txt vendored
View File

@@ -1321,7 +1321,7 @@ github.com/orcaman/concurrent-map
# github.com/owncloud/libre-graph-api-go v1.0.5-0.20260216101009-eeac018af245
## explicit; go 1.18
github.com/owncloud/libre-graph-api-go
# github.com/owncloud/reva/v2 v2.0.0-20261012083840-8b649a4c3899
# github.com/owncloud/reva/v2 v2.0.0-20261019155447-90c24c1b3575
## explicit; go 1.26.0
github.com/owncloud/reva/v2/cmd/revad/internal/grace
github.com/owncloud/reva/v2/cmd/revad/runtime
@@ -1439,8 +1439,6 @@ github.com/owncloud/reva/v2/pkg/auth/manager/machine
github.com/owncloud/reva/v2/pkg/auth/manager/nextcloud
github.com/owncloud/reva/v2/pkg/auth/manager/ocmshares
github.com/owncloud/reva/v2/pkg/auth/manager/oidc
github.com/owncloud/reva/v2/pkg/auth/manager/owncloudsql
github.com/owncloud/reva/v2/pkg/auth/manager/owncloudsql/accounts
github.com/owncloud/reva/v2/pkg/auth/manager/publicshares
github.com/owncloud/reva/v2/pkg/auth/manager/registry
github.com/owncloud/reva/v2/pkg/auth/manager/serviceaccounts
@@ -1546,7 +1544,6 @@ github.com/owncloud/reva/v2/pkg/publicshare/manager/json/persistence/file
github.com/owncloud/reva/v2/pkg/publicshare/manager/json/persistence/memory
github.com/owncloud/reva/v2/pkg/publicshare/manager/loader
github.com/owncloud/reva/v2/pkg/publicshare/manager/memory
github.com/owncloud/reva/v2/pkg/publicshare/manager/owncloudsql
github.com/owncloud/reva/v2/pkg/publicshare/manager/registry
github.com/owncloud/reva/v2/pkg/registry
github.com/owncloud/reva/v2/pkg/rgrpc
@@ -1578,7 +1575,6 @@ github.com/owncloud/reva/v2/pkg/share/manager/jsoncs3/sharecache
github.com/owncloud/reva/v2/pkg/share/manager/jsoncs3/shareid
github.com/owncloud/reva/v2/pkg/share/manager/loader
github.com/owncloud/reva/v2/pkg/share/manager/memory
github.com/owncloud/reva/v2/pkg/share/manager/owncloudsql
github.com/owncloud/reva/v2/pkg/share/manager/registry
github.com/owncloud/reva/v2/pkg/sharedconf
github.com/owncloud/reva/v2/pkg/siteacc
@@ -1621,8 +1617,6 @@ github.com/owncloud/reva/v2/pkg/storage/fs/localhome
github.com/owncloud/reva/v2/pkg/storage/fs/nextcloud
github.com/owncloud/reva/v2/pkg/storage/fs/ocis
github.com/owncloud/reva/v2/pkg/storage/fs/ocis/blobstore
github.com/owncloud/reva/v2/pkg/storage/fs/owncloudsql
github.com/owncloud/reva/v2/pkg/storage/fs/owncloudsql/filecache
github.com/owncloud/reva/v2/pkg/storage/fs/posix
github.com/owncloud/reva/v2/pkg/storage/fs/posix/blobstore
github.com/owncloud/reva/v2/pkg/storage/fs/posix/lookup
@@ -1691,8 +1685,6 @@ github.com/owncloud/reva/v2/pkg/user/manager/ldap
github.com/owncloud/reva/v2/pkg/user/manager/loader
github.com/owncloud/reva/v2/pkg/user/manager/memory
github.com/owncloud/reva/v2/pkg/user/manager/nextcloud
github.com/owncloud/reva/v2/pkg/user/manager/owncloudsql
github.com/owncloud/reva/v2/pkg/user/manager/owncloudsql/accounts
github.com/owncloud/reva/v2/pkg/user/manager/registry
github.com/owncloud/reva/v2/pkg/utils
github.com/owncloud/reva/v2/pkg/utils/cfg