diff --git a/changelog/unreleased/owncloudsql-userprovider.md b/changelog/unreleased/owncloudsql-userprovider.md new file mode 100644 index 00000000000..ad24f5b9297 --- /dev/null +++ b/changelog/unreleased/owncloudsql-userprovider.md @@ -0,0 +1,5 @@ +Enhancement: Add owncloudsql driver for the userprovider + +We added a new backend for the userprovider that is backed by an owncloud 10 database. By default the `user_id` column is used as the reva user username and reva user opaque id. When setting `join_username=true` the reva user username is joined from the `oc_preferences` table (`appid='core' AND configkey='username'`) instead. When setting `join_ownclouduuid=true` the reva user opaqueid is joined from the `oc_preferences` table (`appid='core' AND configkey='ownclouduuid'`) instead. This allows more flexible migration strategies. It also supports a `enable_medial_search` config option when searching users that will enclose the query with `%`. + +https://github.com/cs3org/reva/pull/1994 diff --git a/pkg/user/manager/loader/loader.go b/pkg/user/manager/loader/loader.go index c935085bcbe..41e61a9a6f5 100644 --- a/pkg/user/manager/loader/loader.go +++ b/pkg/user/manager/loader/loader.go @@ -23,5 +23,6 @@ import ( _ "github.com/cs3org/reva/pkg/user/manager/demo" _ "github.com/cs3org/reva/pkg/user/manager/json" _ "github.com/cs3org/reva/pkg/user/manager/ldap" + _ "github.com/cs3org/reva/pkg/user/manager/owncloudsql" // Add your own here ) diff --git a/pkg/user/manager/owncloudsql/accounts/accounts.go b/pkg/user/manager/owncloudsql/accounts/accounts.go new file mode 100644 index 00000000000..e8353b1c348 --- /dev/null +++ b/pkg/user/manager/owncloudsql/accounts/accounts.go @@ -0,0 +1,228 @@ +// 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/cs3org/reva/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") + } + sqldb.SetConnMaxLifetime(time.Minute * 3) + sqldb.SetMaxOpenConns(10) + 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 ?" + if as.joinUsername { + where += " OR p.configvalue LIKE ?" + } + if as.joinUUID { + where += " OR p2.configvalue LIKE ?" + } + var rows *sql.Rows + var err error + if as.joinUsername && as.joinUUID { + rows, err = as.db.QueryContext(ctx, as.selectSQL+where, query, query, query, query, query) + } else if as.joinUsername || as.joinUUID { + rows, err = as.db.QueryContext(ctx, as.selectSQL+where, query, query, query, query) + } else { + rows, err = as.db.QueryContext(ctx, as.selectSQL+where, query, query, query) + } + 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() + + 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 +} diff --git a/pkg/user/manager/owncloudsql/accounts/accounts_suite_test.go b/pkg/user/manager/owncloudsql/accounts/accounts_suite_test.go new file mode 100644 index 00000000000..8564f5a515b --- /dev/null +++ b/pkg/user/manager/owncloudsql/accounts/accounts_suite_test.go @@ -0,0 +1,31 @@ +// 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_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestAccounts(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Accounts Suite") +} diff --git a/pkg/user/manager/owncloudsql/accounts/accounts_test.go b/pkg/user/manager/owncloudsql/accounts/accounts_test.go new file mode 100644 index 00000000000..61819b966fa --- /dev/null +++ b/pkg/user/manager/owncloudsql/accounts/accounts_test.go @@ -0,0 +1,459 @@ +// 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_test + +import ( + "context" + "database/sql" + "io/ioutil" + "os" + + _ "github.com/mattn/go-sqlite3" + + "github.com/cs3org/reva/pkg/user/manager/owncloudsql/accounts" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Accounts", func() { + var ( + conn *accounts.Accounts + testDbFile *os.File + sqldb *sql.DB + ) + + BeforeEach(func() { + var err error + testDbFile, err = ioutil.TempFile("", "example") + Expect(err).ToNot(HaveOccurred()) + + dbData, err := ioutil.ReadFile("test.sqlite") + Expect(err).ToNot(HaveOccurred()) + + _, err = testDbFile.Write(dbData) + Expect(err).ToNot(HaveOccurred()) + err = testDbFile.Close() + Expect(err).ToNot(HaveOccurred()) + + sqldb, err = sql.Open("sqlite3", testDbFile.Name()) + Expect(err).ToNot(HaveOccurred()) + + }) + + AfterEach(func() { + os.Remove(testDbFile.Name()) + }) + + Describe("GetAccountByClaim", func() { + + Context("without any joins", func() { + + BeforeEach(func() { + var err error + conn, err = accounts.New("sqlite3", sqldb, false, false, false) + Expect(err).ToNot(HaveOccurred()) + }) + + It("gets existing account by userid", func() { + userID := "admin" + account, err := conn.GetAccountByClaim(context.Background(), "userid", userID) + Expect(err).ToNot(HaveOccurred()) + Expect(account).ToNot(BeNil()) + Expect(account.ID).To(Equal(uint64(1))) + Expect(account.Email.String).To(Equal("admin@example.org")) + Expect(account.UserID).To(Equal("admin")) + Expect(account.DisplayName.String).To(Equal("admin")) + Expect(account.Quota.String).To(Equal("100 GB")) + Expect(account.LastLogin).To(Equal(1619082575)) + Expect(account.Backend).To(Equal(`OC\User\Database`)) + Expect(account.Home).To(Equal("/mnt/data/files/admin")) + Expect(account.State).To(Equal(int8(1))) + Expect(account.Username.String).To(Equal("admin")) + Expect(account.OwnCloudUUID.String).To(Equal("admin")) + }) + + It("gets existing account by mail", func() { + value := "admin@example.org" + account, err := conn.GetAccountByClaim(context.Background(), "mail", value) + Expect(err).ToNot(HaveOccurred()) + Expect(account).ToNot(BeNil()) + Expect(account.ID).To(Equal(uint64(1))) + Expect(account.Email.String).To(Equal("admin@example.org")) + Expect(account.UserID).To(Equal("admin")) + Expect(account.DisplayName.String).To(Equal("admin")) + Expect(account.Quota.String).To(Equal("100 GB")) + Expect(account.LastLogin).To(Equal(1619082575)) + Expect(account.Backend).To(Equal(`OC\User\Database`)) + Expect(account.Home).To(Equal("/mnt/data/files/admin")) + Expect(account.State).To(Equal(int8(1))) + Expect(account.Username.String).To(Equal("admin")) + Expect(account.OwnCloudUUID.String).To(Equal("admin")) + }) + + It("falls back to user_id colum when getting by username", func() { + value := "admin" + account, err := conn.GetAccountByClaim(context.Background(), "username", value) + Expect(err).ToNot(HaveOccurred()) + Expect(account).ToNot(BeNil()) + Expect(account.ID).To(Equal(uint64(1))) + Expect(account.Email.String).To(Equal("admin@example.org")) + Expect(account.UserID).To(Equal("admin")) + Expect(account.DisplayName.String).To(Equal("admin")) + Expect(account.Quota.String).To(Equal("100 GB")) + Expect(account.LastLogin).To(Equal(1619082575)) + Expect(account.Backend).To(Equal(`OC\User\Database`)) + Expect(account.Home).To(Equal("/mnt/data/files/admin")) + Expect(account.State).To(Equal(int8(1))) + Expect(account.Username.String).To(Equal("admin")) + Expect(account.OwnCloudUUID.String).To(Equal("admin")) + }) + + It("errors on unsupported claim", func() { + _, err := conn.GetAccountByClaim(context.Background(), "invalid", "invalid") + Expect(err).To(HaveOccurred()) + }) + }) + + Context("with username joins", func() { + + BeforeEach(func() { + var err error + conn, err = accounts.New("sqlite3", sqldb, true, false, false) + Expect(err).ToNot(HaveOccurred()) + }) + + It("gets existing account by userid", func() { + userID := "admin" + account, err := conn.GetAccountByClaim(context.Background(), "userid", userID) + Expect(err).ToNot(HaveOccurred()) + Expect(account).ToNot(BeNil()) + Expect(account.ID).To(Equal(uint64(1))) + Expect(account.Email.String).To(Equal("admin@example.org")) + Expect(account.UserID).To(Equal("admin")) + Expect(account.DisplayName.String).To(Equal("admin")) + Expect(account.Quota.String).To(Equal("100 GB")) + Expect(account.LastLogin).To(Equal(1619082575)) + Expect(account.Backend).To(Equal(`OC\User\Database`)) + Expect(account.Home).To(Equal("/mnt/data/files/admin")) + Expect(account.State).To(Equal(int8(1))) + Expect(account.Username.String).To(Equal("Administrator")) + Expect(account.OwnCloudUUID.String).To(Equal("admin")) + }) + + It("gets existing account by mail", func() { + value := "admin@example.org" + account, err := conn.GetAccountByClaim(context.Background(), "mail", value) + Expect(err).ToNot(HaveOccurred()) + Expect(account).ToNot(BeNil()) + Expect(account.ID).To(Equal(uint64(1))) + Expect(account.Email.String).To(Equal("admin@example.org")) + Expect(account.UserID).To(Equal("admin")) + Expect(account.DisplayName.String).To(Equal("admin")) + Expect(account.Quota.String).To(Equal("100 GB")) + Expect(account.LastLogin).To(Equal(1619082575)) + Expect(account.Backend).To(Equal(`OC\User\Database`)) + Expect(account.Home).To(Equal("/mnt/data/files/admin")) + Expect(account.State).To(Equal(int8(1))) + Expect(account.Username.String).To(Equal("Administrator")) + Expect(account.OwnCloudUUID.String).To(Equal("admin")) + }) + + It("gets existing account by username", func() { + value := "Administrator" + account, err := conn.GetAccountByClaim(context.Background(), "username", value) + Expect(err).ToNot(HaveOccurred()) + Expect(account).ToNot(BeNil()) + Expect(account.ID).To(Equal(uint64(1))) + Expect(account.Email.String).To(Equal("admin@example.org")) + Expect(account.UserID).To(Equal("admin")) + Expect(account.DisplayName.String).To(Equal("admin")) + Expect(account.Quota.String).To(Equal("100 GB")) + Expect(account.LastLogin).To(Equal(1619082575)) + Expect(account.Backend).To(Equal(`OC\User\Database`)) + Expect(account.Home).To(Equal("/mnt/data/files/admin")) + Expect(account.State).To(Equal(int8(1))) + Expect(account.Username.String).To(Equal("Administrator")) + Expect(account.OwnCloudUUID.String).To(Equal("admin")) + }) + + It("errors on unsupported claim", func() { + _, err := conn.GetAccountByClaim(context.Background(), "invalid", "invalid") + Expect(err).To(HaveOccurred()) + }) + }) + + Context("with uuid joins", func() { + + BeforeEach(func() { + var err error + conn, err = accounts.New("sqlite3", sqldb, false, true, false) + Expect(err).ToNot(HaveOccurred()) + }) + + It("gets existing account by uuid", func() { + userID := "7015b5ec-7723-4560-bb96-85e18a947314" + account, err := conn.GetAccountByClaim(context.Background(), "userid", userID) + Expect(err).ToNot(HaveOccurred()) + Expect(account).ToNot(BeNil()) + Expect(account.ID).To(Equal(uint64(1))) + Expect(account.Email.String).To(Equal("admin@example.org")) + Expect(account.UserID).To(Equal("admin")) + Expect(account.DisplayName.String).To(Equal("admin")) + Expect(account.Quota.String).To(Equal("100 GB")) + Expect(account.LastLogin).To(Equal(1619082575)) + Expect(account.Backend).To(Equal(`OC\User\Database`)) + Expect(account.Home).To(Equal("/mnt/data/files/admin")) + Expect(account.State).To(Equal(int8(1))) + Expect(account.Username.String).To(Equal("admin")) + Expect(account.OwnCloudUUID.String).To(Equal("7015b5ec-7723-4560-bb96-85e18a947314")) + }) + + It("gets existing account by mail", func() { + value := "admin@example.org" + account, err := conn.GetAccountByClaim(context.Background(), "mail", value) + Expect(err).ToNot(HaveOccurred()) + Expect(account).ToNot(BeNil()) + Expect(account.ID).To(Equal(uint64(1))) + Expect(account.Email.String).To(Equal("admin@example.org")) + Expect(account.UserID).To(Equal("admin")) + Expect(account.DisplayName.String).To(Equal("admin")) + Expect(account.Quota.String).To(Equal("100 GB")) + Expect(account.LastLogin).To(Equal(1619082575)) + Expect(account.Backend).To(Equal(`OC\User\Database`)) + Expect(account.Home).To(Equal("/mnt/data/files/admin")) + Expect(account.State).To(Equal(int8(1))) + Expect(account.Username.String).To(Equal("admin")) + Expect(account.OwnCloudUUID.String).To(Equal("7015b5ec-7723-4560-bb96-85e18a947314")) + }) + + It("gets existing account by username", func() { + value := "admin" + account, err := conn.GetAccountByClaim(context.Background(), "username", value) + Expect(err).ToNot(HaveOccurred()) + Expect(account).ToNot(BeNil()) + Expect(account.ID).To(Equal(uint64(1))) + Expect(account.Email.String).To(Equal("admin@example.org")) + Expect(account.UserID).To(Equal("admin")) + Expect(account.DisplayName.String).To(Equal("admin")) + Expect(account.Quota.String).To(Equal("100 GB")) + Expect(account.LastLogin).To(Equal(1619082575)) + Expect(account.Backend).To(Equal(`OC\User\Database`)) + Expect(account.Home).To(Equal("/mnt/data/files/admin")) + Expect(account.State).To(Equal(int8(1))) + Expect(account.Username.String).To(Equal("admin")) + Expect(account.OwnCloudUUID.String).To(Equal("7015b5ec-7723-4560-bb96-85e18a947314")) + }) + + It("errors on unsupported claim", func() { + _, err := conn.GetAccountByClaim(context.Background(), "invalid", "invalid") + Expect(err).To(HaveOccurred()) + }) + }) + + Context("with username and uuid joins", func() { + + BeforeEach(func() { + var err error + conn, err = accounts.New("sqlite3", sqldb, true, true, false) + Expect(err).ToNot(HaveOccurred()) + }) + + It("gets existing account by uuid", func() { + userID := "7015b5ec-7723-4560-bb96-85e18a947314" + account, err := conn.GetAccountByClaim(context.Background(), "userid", userID) + Expect(err).ToNot(HaveOccurred()) + Expect(account.ID).To(Equal(uint64(1))) + Expect(account.Email.String).To(Equal("admin@example.org")) + Expect(account.UserID).To(Equal("admin")) + Expect(account.DisplayName.String).To(Equal("admin")) + Expect(account.Quota.String).To(Equal("100 GB")) + Expect(account.LastLogin).To(Equal(1619082575)) + Expect(account.Backend).To(Equal(`OC\User\Database`)) + Expect(account.Home).To(Equal("/mnt/data/files/admin")) + Expect(account.State).To(Equal(int8(1))) + Expect(account.Username.String).To(Equal("Administrator")) + Expect(account.OwnCloudUUID.String).To(Equal("7015b5ec-7723-4560-bb96-85e18a947314")) + }) + + It("gets existing account by mail", func() { + value := "admin@example.org" + account, err := conn.GetAccountByClaim(context.Background(), "mail", value) + Expect(err).ToNot(HaveOccurred()) + Expect(account).ToNot(BeNil()) + Expect(account.ID).To(Equal(uint64(1))) + Expect(account.Email.String).To(Equal("admin@example.org")) + Expect(account.UserID).To(Equal("admin")) + Expect(account.DisplayName.String).To(Equal("admin")) + Expect(account.Quota.String).To(Equal("100 GB")) + Expect(account.LastLogin).To(Equal(1619082575)) + Expect(account.Backend).To(Equal(`OC\User\Database`)) + Expect(account.Home).To(Equal("/mnt/data/files/admin")) + Expect(account.State).To(Equal(int8(1))) + Expect(account.Username.String).To(Equal("Administrator")) + Expect(account.OwnCloudUUID.String).To(Equal("7015b5ec-7723-4560-bb96-85e18a947314")) + }) + + It("gets existing account by username", func() { + value := "Administrator" + account, err := conn.GetAccountByClaim(context.Background(), "username", value) + Expect(err).ToNot(HaveOccurred()) + Expect(account).ToNot(BeNil()) + Expect(account.ID).To(Equal(uint64(1))) + Expect(account.Email.String).To(Equal("admin@example.org")) + Expect(account.UserID).To(Equal("admin")) + Expect(account.DisplayName.String).To(Equal("admin")) + Expect(account.Quota.String).To(Equal("100 GB")) + Expect(account.LastLogin).To(Equal(1619082575)) + Expect(account.Backend).To(Equal(`OC\User\Database`)) + Expect(account.Home).To(Equal("/mnt/data/files/admin")) + Expect(account.State).To(Equal(int8(1))) + Expect(account.Username.String).To(Equal("Administrator")) + Expect(account.OwnCloudUUID.String).To(Equal("7015b5ec-7723-4560-bb96-85e18a947314")) + }) + + It("errors on unsupported claim", func() { + _, err := conn.GetAccountByClaim(context.Background(), "invalid", "invalid") + Expect(err).To(HaveOccurred()) + }) + }) + + }) + + Describe("FindAccounts", func() { + + Context("with username and uuid joins", func() { + + BeforeEach(func() { + var err error + conn, err = accounts.New("sqlite3", sqldb, true, true, false) + Expect(err).ToNot(HaveOccurred()) + }) + + It("finds the existing admin account", func() { + accounts, err := conn.FindAccounts(context.Background(), "admin") + Expect(err).ToNot(HaveOccurred()) + Expect(len(accounts)).To(Equal(1)) + Expect(accounts[0]).ToNot(BeNil()) + Expect(accounts[0].ID).To(Equal(uint64(1))) + Expect(accounts[0].Email.String).To(Equal("admin@example.org")) + Expect(accounts[0].UserID).To(Equal("admin")) + Expect(accounts[0].DisplayName.String).To(Equal("admin")) + Expect(accounts[0].Quota.String).To(Equal("100 GB")) + Expect(accounts[0].LastLogin).To(Equal(1619082575)) + Expect(accounts[0].Backend).To(Equal(`OC\User\Database`)) + Expect(accounts[0].Home).To(Equal("/mnt/data/files/admin")) + Expect(accounts[0].State).To(Equal(int8(1))) + Expect(accounts[0].Username.String).To(Equal("Administrator")) + Expect(accounts[0].OwnCloudUUID.String).To(Equal("7015b5ec-7723-4560-bb96-85e18a947314")) + }) + + It("handles query without results", func() { + accounts, err := conn.FindAccounts(context.Background(), "__notexisting__") + Expect(err).ToNot(HaveOccurred()) + Expect(len(accounts)).To(Equal(0)) + }) + }) + + Context("with username joins", func() { + + BeforeEach(func() { + var err error + conn, err = accounts.New("sqlite3", sqldb, true, false, false) + Expect(err).ToNot(HaveOccurred()) + }) + + It("finds the existing admin account", func() { + accounts, err := conn.FindAccounts(context.Background(), "admin") + Expect(err).ToNot(HaveOccurred()) + Expect(len(accounts)).To(Equal(1)) + Expect(accounts[0]).ToNot(BeNil()) + Expect(accounts[0].ID).To(Equal(uint64(1))) + Expect(accounts[0].Email.String).To(Equal("admin@example.org")) + Expect(accounts[0].UserID).To(Equal("admin")) + Expect(accounts[0].DisplayName.String).To(Equal("admin")) + Expect(accounts[0].Quota.String).To(Equal("100 GB")) + Expect(accounts[0].LastLogin).To(Equal(1619082575)) + Expect(accounts[0].Backend).To(Equal(`OC\User\Database`)) + Expect(accounts[0].Home).To(Equal("/mnt/data/files/admin")) + Expect(accounts[0].State).To(Equal(int8(1))) + Expect(accounts[0].Username.String).To(Equal("Administrator")) + Expect(accounts[0].OwnCloudUUID.String).To(Equal("admin")) + }) + + It("handles query without results", func() { + accounts, err := conn.FindAccounts(context.Background(), "__notexisting__") + Expect(err).ToNot(HaveOccurred()) + Expect(len(accounts)).To(Equal(0)) + }) + }) + + Context("without any joins", func() { + + BeforeEach(func() { + var err error + conn, err = accounts.New("sqlite3", sqldb, false, false, false) + Expect(err).ToNot(HaveOccurred()) + }) + + It("finds the existing admin account", func() { + accounts, err := conn.FindAccounts(context.Background(), "admin") + Expect(err).ToNot(HaveOccurred()) + Expect(len(accounts)).To(Equal(1)) + Expect(accounts[0]).ToNot(BeNil()) + Expect(accounts[0].ID).To(Equal(uint64(1))) + Expect(accounts[0].Email.String).To(Equal("admin@example.org")) + Expect(accounts[0].UserID).To(Equal("admin")) + Expect(accounts[0].DisplayName.String).To(Equal("admin")) + Expect(accounts[0].Quota.String).To(Equal("100 GB")) + Expect(accounts[0].LastLogin).To(Equal(1619082575)) + Expect(accounts[0].Backend).To(Equal(`OC\User\Database`)) + Expect(accounts[0].Home).To(Equal("/mnt/data/files/admin")) + Expect(accounts[0].State).To(Equal(int8(1))) + Expect(accounts[0].Username.String).To(Equal("admin")) + Expect(accounts[0].OwnCloudUUID.String).To(Equal("admin")) + }) + + It("handles query without results", func() { + accounts, err := conn.FindAccounts(context.Background(), "__notexisting__") + Expect(err).ToNot(HaveOccurred()) + Expect(len(accounts)).To(Equal(0)) + }) + }) + }) + + Describe("GetAccountGroups", func() { + BeforeEach(func() { + var err error + conn, err = accounts.New("sqlite3", sqldb, true, true, false) + Expect(err).ToNot(HaveOccurred()) + }) + It("get admin group for admin account", func() { + accounts, err := conn.GetAccountGroups(context.Background(), "admin") + Expect(err).ToNot(HaveOccurred()) + Expect(len(accounts)).To(Equal(1)) + Expect(accounts[0]).To(Equal("admin")) + }) + It("handles not existing account", func() { + accounts, err := conn.GetAccountGroups(context.Background(), "__notexisting__") + Expect(err).ToNot(HaveOccurred()) + Expect(len(accounts)).To(Equal(0)) + }) + }) +}) diff --git a/pkg/user/manager/owncloudsql/accounts/test.sqlite b/pkg/user/manager/owncloudsql/accounts/test.sqlite new file mode 100644 index 00000000000..c68bb753774 Binary files /dev/null and b/pkg/user/manager/owncloudsql/accounts/test.sqlite differ diff --git a/pkg/user/manager/owncloudsql/owncloudsql.go b/pkg/user/manager/owncloudsql/owncloudsql.go new file mode 100644 index 00000000000..87eb5802f40 --- /dev/null +++ b/pkg/user/manager/owncloudsql/owncloudsql.go @@ -0,0 +1,180 @@ +// 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/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/user" + "github.com/cs3org/reva/pkg/user/manager/owncloudsql/accounts" + "github.com/cs3org/reva/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) (*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) +} + +func (m *manager) GetUserByClaim(ctx context.Context, claim, value string) (*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) +} + +func (m *manager) FindUsers(ctx context.Context, query string) ([]*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 := []*userpb.User{} + for i := range accounts { + u, err := m.convertToCS3User(ctx, &accounts[i]) + 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) (*userpb.User, error) { + u := &userpb.User{ + Id: &userpb.UserId{ + Idp: m.c.Idp, + OpaqueId: a.UserID, // TODO rea + }, + Username: a.Username.String, + Mail: a.Email.String, + DisplayName: a.DisplayName.String, + //Groups: groups, + GidNumber: m.c.Nobody, + UidNumber: m.c.Nobody, + } + if u.Username == "" { + u.Username = u.Id.OpaqueId + } + if u.DisplayName == "" { + u.DisplayName = u.Id.OpaqueId + } + var err error + if u.Groups, err = m.GetUserGroups(ctx, u.Id); err != nil { + return nil, err + } + return u, nil +}