From aa0c0365bc6122a80e0d0dd044929e6dd0df8205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 13 Aug 2021 23:46:44 +0200 Subject: [PATCH] userprovider owncloudsql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- .../unreleased/owncloudsql-userprovider.md | 5 + pkg/user/manager/loader/loader.go | 1 + .../manager/owncloudsql/accounts/accounts.go | 228 +++++++++ .../accounts/accounts_suite_test.go | 31 ++ .../owncloudsql/accounts/accounts_test.go | 459 ++++++++++++++++++ .../manager/owncloudsql/accounts/test.sqlite | Bin 0 -> 90112 bytes pkg/user/manager/owncloudsql/owncloudsql.go | 180 +++++++ 7 files changed, 904 insertions(+) create mode 100644 changelog/unreleased/owncloudsql-userprovider.md create mode 100644 pkg/user/manager/owncloudsql/accounts/accounts.go create mode 100644 pkg/user/manager/owncloudsql/accounts/accounts_suite_test.go create mode 100644 pkg/user/manager/owncloudsql/accounts/accounts_test.go create mode 100644 pkg/user/manager/owncloudsql/accounts/test.sqlite create mode 100644 pkg/user/manager/owncloudsql/owncloudsql.go 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 0000000000000000000000000000000000000000..c68bb753774fc28eda95f71768f5d41d29830363 GIT binary patch literal 90112 zcmeI*T~8a?8Nl%wFc_S`u)86nYK1ZisfZN_HUtP+Z8t!i4O+rW78;0FGJ`!hPHc}e zV=pi54QZwQ2z$|s_F~n3fJ&8C>X&G*Dpl%brD|2FdzIUsIm6gvjDc!ZwOIcVG4pcH z%yWKo=FFTiGx>aNDPOUSUE3*|6=PBvP*hcUXc&s3OpAZl#lK+qjTngp6XLfT8gDf^ zt=vsK=-jK5vNqxD8 zBGxQIh`ldJ;_A^za|mX(m@lRMO#ZmYB4k5zJ~$(eQ?s9lMW(3-)B9dj|7`3;FvnFR zU9p^^D?e)>4U67CHI{}Xq1?4V8WMP+c-s*$9}YzI=OUI*!Y~bqgG!qY?V^SR?_D6G zAVaW@)^XKSis)ARqx#U0`qr(OI|WPZ6tBLx!Efz&!|QYNODSVJn7wV>jv0pB@y3^? zlX+=6x2NyLjpdbfV|im~$#|4{Jh!p5Zj6k?Mn%Ho+Ugw-+YK3lbY3QzvTVBUp6z7o z$tI?zTCRBY+2Yf=XIsW+sjZ+M#^U2zx4tl>Dw5lMQxMk^X|r0f<+RsC$P%WL!Pvmt zTUVocJg)w-GcYJS)~@APrHtiCgr@qMJVFZt6#*!Yh=y{uf-%eGaMH<%mPOsp*rnZk z?zMGru{Hb2zcdR~L0+&+xr()4sqL4*`vjmL`RKME)@W#(@mTz$zHWUbeuis^Lh6R5 z`wwGL{l*RTXdxh$$G)fQk6dYj_2(a(=E6#6)U?L(c)3L2$t&G@>c$y?`>Izh~GqWAQ;<+A@U_6r|Q&4nEG+u&Gywzpe_yuH&{WZAOT$}Fd%Eeb6c{trt1QS_XeDfenxh8HTfobLI>&pM;}@UZ&T$3eM| z+efc*2BVRt5(i6;i|f_Te5qpPEXSxfgW)|jx3RvmxGdK5G_|}g+dBVZqSj8xhOVhW zl&9}-?Qdug<$4PCo;bx{AYsEr!#Uf1Q?)B*y{Yk9rl!~mrdvrD>|DN7FMLx=myj{; zn3>mBsYRKWB6L2H3B(zyBHFS*`ScBKEHAEYq`Z@XciAJ4e{lk;&26L- zZNpSox4t-h#sL%T=k!FVI&@D(E}G3d%>?V{5*N@t-bM6kN4I|Gj=Cjl-0>;Owcb?a z8OJ|k1(Tf(HSa{)+uj*J zFd%>c0tg_000IagfB*srAb>!d2=uF+!@c!CKXCp2|2K;EO`8;uVjzG30tg_000Iag zfB*srAaF?qUZ_`;ab4Gc6d9Z8l@HN8wD!$nxnSM3ot&Ie|41eh#;5b!M<1`OEPSyc z9?JOQktrT~FkNfBSgMR?#l-k-zF@iIGEYpHsMi(Ss`v2{c|9_kAT#AJN0tg_000IagfB*srAb`NJfdBr#?-lKPNDx2(0R#|0 z009ILKmY**5I~@91p3rYU9Z0`(0~4){(sx-UkZW%0tg_000IagfB*srAb@};&>zu{ zU;QU<0{Gwm^ZtJ-2LcEnfB*srAb#A$@}K)y_w16J+Z-|@J1WPhdR3S_@E|NzMFT%Caa~r z{GsV&-Iw-WVpwequ#zuYhjz(IRUNx*jn7+7Az$*o|Bq;2E85rEKeRt-zi-$869^!H z00IagfB*srAbGt{eM+^C!YUrorWa{Ab