From 867d1e298708c3a63142d39733ef39d2121fb8fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Mon, 1 Nov 2021 10:49:21 +0100 Subject: [PATCH] add authprovider owncloudsql (#2119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- CODEOWNERS | 1 + .../unreleased/authprovider-owncloudsql.md | 5 + pkg/auth/manager/loader/loader.go | 1 + .../manager/owncloudsql/accounts/accounts.go | 162 +++++++++++ .../accounts/accounts_suite_test.go | 31 ++ .../owncloudsql/accounts/accounts_test.go | 269 ++++++++++++++++++ .../manager/owncloudsql/accounts/test.sqlite | Bin 0 -> 90112 bytes pkg/auth/manager/owncloudsql/owncloudsql.go | 192 +++++++++++++ .../manager/owncloudsql/owncloudsql_test.go | 104 +++++++ 9 files changed, 765 insertions(+) create mode 100644 changelog/unreleased/authprovider-owncloudsql.md create mode 100644 pkg/auth/manager/owncloudsql/accounts/accounts.go create mode 100644 pkg/auth/manager/owncloudsql/accounts/accounts_suite_test.go create mode 100644 pkg/auth/manager/owncloudsql/accounts/accounts_test.go create mode 100644 pkg/auth/manager/owncloudsql/accounts/test.sqlite create mode 100644 pkg/auth/manager/owncloudsql/owncloudsql.go create mode 100644 pkg/auth/manager/owncloudsql/owncloudsql_test.go diff --git a/CODEOWNERS b/CODEOWNERS index 82d307f49b..0617e98b32 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,4 +1,5 @@ * @labkode @ishank011 +pkg/auth/manager/owncloudsql @cs3org/owncloud-team pkg/storage/fs/owncloudsql @cs3org/owncloud-team pkg/storage/fs/owncloud @cs3org/owncloud-team pkg/storage/fs/ocis @cs3org/owncloud-team diff --git a/changelog/unreleased/authprovider-owncloudsql.md b/changelog/unreleased/authprovider-owncloudsql.md new file mode 100644 index 0000000000..acb274c74e --- /dev/null +++ b/changelog/unreleased/authprovider-owncloudsql.md @@ -0,0 +1,5 @@ +Enhancement: add authprovider owncloudsql + +We added an authprovider that can be configured to authenticate against an owncloud classic mysql database. It verifies the password from the oc_users table. + +https://github.com/cs3org/reva/pull/2119 diff --git a/pkg/auth/manager/loader/loader.go b/pkg/auth/manager/loader/loader.go index f2a756cb0c..69862bc144 100644 --- a/pkg/auth/manager/loader/loader.go +++ b/pkg/auth/manager/loader/loader.go @@ -28,6 +28,7 @@ import ( _ "github.com/cs3org/reva/pkg/auth/manager/machine" _ "github.com/cs3org/reva/pkg/auth/manager/nextcloud" _ "github.com/cs3org/reva/pkg/auth/manager/oidc" + _ "github.com/cs3org/reva/pkg/auth/manager/owncloudsql" _ "github.com/cs3org/reva/pkg/auth/manager/publicshares" // Add your own here ) diff --git a/pkg/auth/manager/owncloudsql/accounts/accounts.go b/pkg/auth/manager/owncloudsql/accounts/accounts.go new file mode 100644 index 0000000000..1489669892 --- /dev/null +++ b/pkg/auth/manager/owncloudsql/accounts/accounts.go @@ -0,0 +1,162 @@ +// 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 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") + } + 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 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 +} diff --git a/pkg/auth/manager/owncloudsql/accounts/accounts_suite_test.go b/pkg/auth/manager/owncloudsql/accounts/accounts_suite_test.go new file mode 100644 index 0000000000..8564f5a515 --- /dev/null +++ b/pkg/auth/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/auth/manager/owncloudsql/accounts/accounts_test.go b/pkg/auth/manager/owncloudsql/accounts/accounts_test.go new file mode 100644 index 0000000000..bb00fcbf2f --- /dev/null +++ b/pkg/auth/manager/owncloudsql/accounts/accounts_test.go @@ -0,0 +1,269 @@ +// 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/auth/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("GetAccountByLogin", 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 mail", func() { + value := "admin@example.org" + account, err := conn.GetAccountByLogin(context.Background(), 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("gets existing account by username", func() { + value := "admin" + account, err := conn.GetAccountByLogin(context.Background(), 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")) + }) + + }) + + 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 mail", func() { + value := "admin@example.org" + account, err := conn.GetAccountByLogin(context.Background(), 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 := "admin" + account, err := conn.GetAccountByLogin(context.Background(), 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")) + }) + }) + + 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 mail", func() { + value := "admin@example.org" + account, err := conn.GetAccountByLogin(context.Background(), 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.GetAccountByLogin(context.Background(), 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")) + }) + + }) + + 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 mail", func() { + value := "admin@example.org" + account, err := conn.GetAccountByLogin(context.Background(), 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.GetAccountByLogin(context.Background(), 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")) + }) + + }) + + }) + + 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/auth/manager/owncloudsql/accounts/test.sqlite b/pkg/auth/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{Abhash($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)) +} diff --git a/pkg/auth/manager/owncloudsql/owncloudsql_test.go b/pkg/auth/manager/owncloudsql/owncloudsql_test.go new file mode 100644 index 0000000000..302e482b6e --- /dev/null +++ b/pkg/auth/manager/owncloudsql/owncloudsql_test.go @@ -0,0 +1,104 @@ +// 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 ( + "testing" + + "github.com/cs3org/reva/pkg/auth/manager/owncloudsql/accounts" + "github.com/pkg/errors" +) + +// new returns a dummy auth manager for testing +func new(m map[string]interface{}) (*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.New("unused", nil, false, false, false) + if err != nil { + return nil, err + } + + return mgr, nil +} + +func TestVerify(t *testing.T) { + tests := map[string]struct { + password string + hash string + expected bool + }{ + // Bogus values + "bogus-1": {"", "asf32äà$$a.|3", false}, + "bogus-2": {"", "", false}, + + // Valid SHA1 strings + "valid-sha1-1": {"password", "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", true}, + "valid-sha1-2": {"owncloud.com", "27a4643e43046c3569e33b68c1a4b15d31306d29", true}, + + // Invalid SHA1 strings + "invalid-sha1-1": {"InvalidString", "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", false}, + "invalid-sha1-2": {"AnotherInvalidOne", "27a4643e43046c3569e33b68c1a4b15d31306d29", false}, + + // Valid legacy password string with password salt "6Wow67q1wZQZpUUeI6G2LsWUu4XKx" + "valid-legacy-1": {"password", "$2a$08$emCpDEl.V.QwPWt5gPrqrOhdpH6ailBmkj2Hd2vD5U8qIy20HBe7.", true}, + "valid-legacy-2": {"password", "$2a$08$yjaLO4ev70SaOsWZ9gRS3eRSEpHVsmSWTdTms1949mylxJ279hzo2", true}, + "valid-legacy-3": {"password", "$2a$08$.jNRG/oB4r7gHJhAyb.mDupNUAqTnBIW/tWBqFobaYflKXiFeG0A6", true}, + "valid-legacy-4": {"owncloud.com", "$2a$08$YbEsyASX/hXVNMv8hXQo7ezreN17T8Jl6PjecGZvpX.Ayz2aUyaZ2", true}, + "valid-legacy-5": {"owncloud.com", "$2a$11$cHdDA2IkUP28oNGBwlL7jO/U3dpr8/0LIjTZmE8dMPA7OCUQsSTqS", true}, + "valid-legacy-6": {"owncloud.com", "$2a$08$GH.UoIfJ1e.qeZ85KPqzQe6NR8XWRgJXWIUeE1o/j1xndvyTA1x96", true}, + + // Invalid legacy passwords + "invalid-legacy": {"password", "$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2", false}, + + // Valid passwords "6Wow67q1wZQZpUUeI6G2LsWUu4XKx" + "valid-1": {"password", "1|$2a$05$ezAE0dkwk57jlfo6z5Pql.gcIK3ReXT15W7ITNxVS0ksfhO/4E4Kq", true}, + "valid-2": {"password", "1|$2a$05$4OQmloFW4yTVez2MEWGIleDO9Z5G9tWBXxn1vddogmKBQq/Mq93pe", true}, + "valid-3": {"password", "1|$2a$11$yj0hlp6qR32G9exGEXktB.yW2rgt2maRBbPgi3EyxcDwKrD14x/WO", true}, + "valid-4": {"owncloud.com", "1|$2a$10$Yiss2WVOqGakxuuqySv5UeOKpF8d8KmNjuAPcBMiRJGizJXjA2bKm", true}, + "valid-5": {"owncloud.com", "1|$2a$10$v9mh8/.mF/Ut9jZ7pRnpkuac3bdFCnc4W/gSumheQUi02Sr.xMjPi", true}, + "valid-6": {"owncloud.com", "1|$2a$05$ST5E.rplNRfDCzRpzq69leRzsTGtY7k88h9Vy2eWj0Ug/iA9w5kGK", true}, + + // Invalid passwords + "invalid-1": {"password", "0|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2", false}, + "invalid-2": {"password", "1|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2", false}, + "invalid-3": {"password", "2|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2", false}, + } + + u, err := new(map[string]interface{}{ + "legacy_salt": "6Wow67q1wZQZpUUeI6G2LsWUu4XKx", + }) + if err != nil { + t.Fatalf("could not initialize owncloudsql auth manager: %v", err) + } + + for name := range tests { + var tc = tests[name] + t.Run(name, func(t *testing.T) { + actual := u.verify(tc.password, tc.hash) + if actual != tc.expected { + t.Fatalf("%v returned wrong verification:\n\tAct: %v\n\tExp: %v", t.Name(), actual, tc.expected) + } + }) + } +}