diff --git a/changelog/unreleased/authprovider-owncloudsql.md b/changelog/unreleased/authprovider-owncloudsql.md new file mode 100644 index 00000000000..acb274c74ef --- /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 f2a756cb0ca..69862bc1446 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 00000000000..1489669892a --- /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 00000000000..8564f5a515b --- /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 00000000000..bb00fcbf2f2 --- /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 00000000000..c68bb753774 Binary files /dev/null and b/pkg/auth/manager/owncloudsql/accounts/test.sqlite differ diff --git a/pkg/auth/manager/owncloudsql/owncloudsql.go b/pkg/auth/manager/owncloudsql/owncloudsql.go new file mode 100644 index 00000000000..2a534ace7b0 --- /dev/null +++ b/pkg/auth/manager/owncloudsql/owncloudsql.go @@ -0,0 +1,191 @@ +// 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/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/auth" + "github.com/cs3org/reva/pkg/auth/manager/owncloudsql/accounts" + "github.com/cs3org/reva/pkg/auth/manager/registry" + "github.com/cs3org/reva/pkg/auth/scope" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + + _ "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 { + 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)) +} diff --git a/pkg/auth/manager/owncloudsql/owncloudsql_test.go b/pkg/auth/manager/owncloudsql/owncloudsql_test.go new file mode 100644 index 00000000000..302e482b6ef --- /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) + } + }) + } +}