chore: bump reva to latest main

This commit is contained in:
Ralf Haferkamp
2026-04-08 11:45:37 +02:00
committed by Ralf Haferkamp
parent 4c86d2a289
commit b8c4f581fb
139 changed files with 6620 additions and 3045 deletions
@@ -26,10 +26,8 @@ import (
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/json"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/ldap"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/machine"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/nextcloud"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/ocmshares"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/oidc"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/owncloudsql"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/publicshares"
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/serviceaccounts"
// Add your own here
@@ -1,197 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
// Package nextcloud verifies a clientID and clientSecret against a Nextcloud backend.
package nextcloud
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/pkg/errors"
)
func init() {
registry.Register("nextcloud", New)
}
// Manager is the Nextcloud-based implementation of the auth.Manager interface
// see https://github.com/cs3org/reva/blob/v1.13.0/pkg/auth/auth.go#L32-L35
type Manager struct {
client *http.Client
sharedSecret string
endPoint string
}
// AuthManagerConfig contains config for a Nextcloud-based AuthManager
type AuthManagerConfig struct {
EndPoint string `mapstructure:"endpoint" docs:";The Nextcloud backend endpoint for user check"`
SharedSecret string `mapstructure:"shared_secret"`
MockHTTP bool `mapstructure:"mock_http"`
}
// Action describes a REST request to forward to the Nextcloud backend
type Action struct {
verb string
username string
argS string
}
func (c *AuthManagerConfig) init() {
}
func parseConfig(m map[string]interface{}) (*AuthManagerConfig, error) {
c := &AuthManagerConfig{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New returns an auth manager implementation that verifies against a Nextcloud backend.
func New(m map[string]interface{}) (auth.Manager, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
return NewAuthManager(c)
}
// NewAuthManager returns a new Nextcloud-based AuthManager
func NewAuthManager(c *AuthManagerConfig) (*Manager, error) {
var client *http.Client
if c.MockHTTP {
// called := make([]string, 0)
// nextcloudServerMock := GetNextcloudServerMock(&called)
// client, _ = TestingHTTPClient(nextcloudServerMock)
// Wait for SetHTTPClient to be called later
client = nil
} else {
if len(c.EndPoint) == 0 {
return nil, errors.New("Please specify 'endpoint' in '[grpc.services.authprovider.auth_managers.nextcloud]'")
}
client = &http.Client{}
}
return &Manager{
endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/"
sharedSecret: c.SharedSecret,
client: client,
}, nil
}
// Configure method as defined in https://github.com/cs3org/reva/blob/v1.13.0/pkg/auth/auth.go#L32-L35
func (am *Manager) Configure(ml map[string]interface{}) error {
return nil
}
// SetHTTPClient sets the HTTP client
func (am *Manager) SetHTTPClient(c *http.Client) {
am.client = c
}
func (am *Manager) do(ctx context.Context, a Action) (int, []byte, error) {
log := appctx.GetLogger(ctx)
url := am.endPoint + "~" + a.username + "/api/auth/" + a.verb
log.Info().Msgf("am.do %s %s %s", url, a.argS, am.sharedSecret)
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS))
if err != nil {
return 0, nil, err
}
req.Header.Set("X-Reva-Secret", am.sharedSecret)
req.Header.Set("Content-Type", "application/json")
resp, err := am.client.Do(req)
if err != nil {
return 0, nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, nil, err
}
log.Info().Msgf("am.do response %d %s", resp.StatusCode, body)
return resp.StatusCode, body, nil
}
// Authenticate method as defined in https://github.com/cs3org/reva/blob/28500a8/pkg/auth/auth.go#L31-L33
func (am *Manager) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
type paramsObj struct {
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
// Scope authpb.Scope
}
bodyObj := &paramsObj{
ClientID: clientID,
ClientSecret: clientSecret,
// Scope: authpb.Scope{
// Resource: &types.OpaqueEntry{
// Decoder: "json",
// Value: []byte(`{"resource_id":{"storage_id":"storage-id","opaque_id":"opaque-id"},"path":"some/file/path.txt"}`),
// },
// Role: authpb.Role_ROLE_OWNER,
// },
}
bodyStr, err := json.Marshal(bodyObj)
if err != nil {
return nil, nil, err
}
log := appctx.GetLogger(ctx)
log.Info().Msgf("Authenticate %s %s", clientID, bodyStr)
statusCode, body, err := am.do(ctx, Action{"Authenticate", clientID, string(bodyStr)})
if err != nil {
return nil, nil, err
}
if statusCode != 200 {
return nil, nil, errors.New("Username/password not recognized by Nextcloud backend")
}
type resultsObj struct {
User user.User `json:"user"`
Scopes map[string]*authpb.Scope `json:"scopes"`
}
result := &resultsObj{}
err = json.Unmarshal(body, &result)
if err != nil {
return nil, nil, err
}
var pointersMap = make(map[string]*authpb.Scope)
for k := range result.Scopes {
scope := result.Scopes[k]
pointersMap[k] = scope
}
return &result.User, pointersMap, nil
}
@@ -1,99 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package nextcloud
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"strings"
)
// Response contains data for the Nextcloud mock server to respond
// and to switch to a new server state
type Response struct {
code int
body string
newServerState string
}
const serverStateError = "ERROR"
const serverStateEmpty = "EMPTY"
const serverStateHome = "HOME"
var serverState = serverStateEmpty
var responses = map[string]Response{
`POST /apps/sciencemesh/~einstein/api/auth/Authenticate {"clientID":"einstein","clientSecret":"relativity"}`: {200, `{"user":{"id":{"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}},"scopes":{"user":{"resource":{"decoder":"json","value":"eyJyZXNvdXJjZV9pZCI6eyJzdG9yYWdlX2lkIjoic3RvcmFnZS1pZCIsIm9wYXF1ZV9pZCI6Im9wYXF1ZS1pZCJ9LCJwYXRoIjoic29tZS9maWxlL3BhdGgudHh0In0="},"role":1}}}`, serverStateHome},
}
// GetNextcloudServerMock returns a handler that pretends to be a remote Nextcloud server
func GetNextcloudServerMock(called *[]string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buf := new(strings.Builder)
_, err := io.Copy(buf, r.Body)
if err != nil {
panic("Error reading response into buffer")
}
var key = fmt.Sprintf("%s %s %s", r.Method, r.URL, buf.String())
*called = append(*called, key)
response := responses[key]
if (response == Response{}) {
key = fmt.Sprintf("%s %s %s %s", r.Method, r.URL, buf.String(), serverState)
response = responses[key]
}
if (response == Response{}) {
fmt.Printf("%s %s %s %s", r.Method, r.URL, buf.String(), serverState)
response = Response{500, fmt.Sprintf("response not defined! %s", key), serverStateEmpty}
}
serverState = responses[key].newServerState
if serverState == `` {
serverState = serverStateError
}
w.WriteHeader(response.code)
// w.Header().Set("Etag", "mocker-etag")
_, err = w.Write([]byte(responses[key].body))
if err != nil {
panic(err)
}
})
}
// TestingHTTPClient thanks to https://itnext.io/how-to-stub-requests-to-remote-hosts-with-go-6c2c1db32bf2
// Ideally, this function would live in tests/helpers, but
// if we put it there, it gets excluded by .dockerignore, and the
// Docker build fails (see https://github.com/cs3org/reva/issues/1999)
// So putting it here for now - open to suggestions if someone knows
// a better way to inject this.
func TestingHTTPClient(handler http.Handler) (*http.Client, func()) {
s := httptest.NewServer(handler)
cli := &http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, network, _ string) (net.Conn, error) {
return net.Dial(network, s.Listener.Addr().String())
},
},
}
return cli, s.Close
}
@@ -1,165 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package accounts
import (
"context"
"database/sql"
"strings"
"time"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/pkg/errors"
)
// Accounts represents oc10-style Accounts
type Accounts struct {
driver string
db *sql.DB
joinUsername, joinUUID, enableMedialSearch bool
selectSQL string
}
// NewMysql returns a new accounts instance connecting to a MySQL database
func NewMysql(dsn string, joinUsername, joinUUID, enableMedialSearch bool) (*Accounts, error) {
sqldb, err := sql.Open("mysql", dsn)
if err != nil {
return nil, errors.Wrap(err, "error connecting to the database")
}
// FIXME make configurable
sqldb.SetConnMaxLifetime(time.Minute * 3)
sqldb.SetConnMaxIdleTime(time.Second * 30)
sqldb.SetMaxOpenConns(100)
sqldb.SetMaxIdleConns(10)
err = sqldb.Ping()
if err != nil {
return nil, errors.Wrap(err, "error connecting to the database")
}
return New("mysql", sqldb, joinUsername, joinUUID, enableMedialSearch)
}
// New returns a new accounts instance connecting to the given sql.DB
func New(driver string, sqldb *sql.DB, joinUsername, joinUUID, enableMedialSearch bool) (*Accounts, error) {
sel := "SELECT id, email, user_id, display_name, quota, last_login, backend, home, state, password"
from := `
FROM oc_accounts a
LEFT JOIN oc_users u
ON a.user_id=u.uid
`
if joinUsername {
sel += ", p.configvalue AS username"
from += `LEFT JOIN oc_preferences p
ON a.user_id=p.userid
AND p.appid='core'
AND p.configkey='username'`
} else {
// fallback to user_id as username
sel += ", user_id AS username"
}
if joinUUID {
sel += ", p2.configvalue AS ownclouduuid"
from += `LEFT JOIN oc_preferences p2
ON a.user_id=p2.userid
AND p2.appid='core'
AND p2.configkey='ownclouduuid'`
} else {
// fallback to user_id as ownclouduuid
sel += ", user_id AS ownclouduuid"
}
return &Accounts{
driver: driver,
db: sqldb,
joinUsername: joinUsername,
joinUUID: joinUUID,
enableMedialSearch: enableMedialSearch,
selectSQL: sel + from,
}, nil
}
// Account stores information about accounts.
type Account struct {
ID uint64
Email sql.NullString
UserID string
DisplayName sql.NullString
Quota sql.NullString
LastLogin int
Backend string
Home string
State int8
PasswordHash string // from oc_users
Username sql.NullString // optional comes from the oc_preferences
OwnCloudUUID sql.NullString // optional comes from the oc_preferences
}
func (as *Accounts) rowToAccount(ctx context.Context, row Scannable) (*Account, error) {
a := Account{}
if err := row.Scan(&a.ID, &a.Email, &a.UserID, &a.DisplayName, &a.Quota, &a.LastLogin, &a.Backend, &a.Home, &a.State, &a.PasswordHash, &a.Username, &a.OwnCloudUUID); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
return nil, err
}
return &a, nil
}
// Scannable describes the interface providing a Scan method
type Scannable interface {
Scan(...interface{}) error
}
// GetAccountByLogin fetches an account by mail or username
func (as *Accounts) GetAccountByLogin(ctx context.Context, login string) (*Account, error) {
var row *sql.Row
username := strings.ToLower(login) // usernames are lowercased in owncloud classic
if as.joinUsername {
row = as.db.QueryRowContext(ctx, as.selectSQL+" WHERE a.email=? OR a.lower_user_id=? OR p.configvalue=?", login, username, login)
} else {
row = as.db.QueryRowContext(ctx, as.selectSQL+" WHERE a.email=? OR a.lower_user_id=?", login, username)
}
return as.rowToAccount(ctx, row)
}
// GetAccountGroups reads the groups for an account
func (as *Accounts) GetAccountGroups(ctx context.Context, uid string) ([]string, error) {
rows, err := as.db.QueryContext(ctx, "SELECT gid FROM oc_group_user WHERE uid=?", uid)
if err != nil {
return nil, err
}
defer rows.Close()
var group string
groups := []string{}
for rows.Next() {
if err := rows.Scan(&group); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
continue
}
groups = append(groups, group)
}
if err = rows.Err(); err != nil {
return nil, err
}
return groups, nil
}
@@ -1,192 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package owncloudsql
import (
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"fmt"
"strings"
"golang.org/x/crypto/bcrypt"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/owncloudsql/accounts"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/pkg/errors"
// Provides mysql drivers
_ "github.com/go-sql-driver/mysql"
)
func init() {
registry.Register("owncloudsql", NewMysql)
}
type manager struct {
c *config
db *accounts.Accounts
}
type config struct {
DbUsername string `mapstructure:"dbusername"`
DbPassword string `mapstructure:"dbpassword"`
DbHost string `mapstructure:"dbhost"`
DbPort int `mapstructure:"dbport"`
DbName string `mapstructure:"dbname"`
Idp string `mapstructure:"idp"`
Nobody int64 `mapstructure:"nobody"`
LegacySalt string `mapstructure:"legacy_salt"`
JoinUsername bool `mapstructure:"join_username"`
JoinOwnCloudUUID bool `mapstructure:"join_ownclouduuid"`
}
// NewMysql returns a new auth manager connection to an owncloud mysql database
func NewMysql(m map[string]interface{}) (auth.Manager, error) {
mgr := &manager{}
err := mgr.Configure(m)
if err != nil {
err = errors.Wrap(err, "error creating a new auth manager")
return nil, err
}
mgr.db, err = accounts.NewMysql(
fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", mgr.c.DbUsername, mgr.c.DbPassword, mgr.c.DbHost, mgr.c.DbPort, mgr.c.DbName),
mgr.c.JoinUsername,
mgr.c.JoinOwnCloudUUID,
false,
)
if err != nil {
return nil, err
}
return mgr, nil
}
func (m *manager) Configure(ml map[string]interface{}) error {
c, err := parseConfig(ml)
if err != nil {
return err
}
if c.Nobody == 0 {
c.Nobody = 99
}
m.c = c
return nil
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, &c); err != nil {
return nil, err
}
return c, nil
}
func (m *manager) Authenticate(ctx context.Context, login, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
log := appctx.GetLogger(ctx)
// 1. find user by login
account, err := m.db.GetAccountByLogin(ctx, login)
if err != nil {
return nil, nil, errtypes.NotFound(login)
}
// 2. verify the user password
if !m.verify(clientSecret, account.PasswordHash) {
return nil, nil, errtypes.InvalidCredentials(login)
}
userID := &user.UserId{
Idp: m.c.Idp,
OpaqueId: account.OwnCloudUUID.String,
Type: user.UserType_USER_TYPE_PRIMARY, // TODO: assign the appropriate user type for guest accounts
}
u := &user.User{
Id: userID,
// TODO add more claims from the StandardClaims, eg EmailVerified and lastlogin
Username: account.Username.String,
Mail: account.Email.String,
DisplayName: account.DisplayName.String,
//UidNumber: uidNumber,
//GidNumber: gidNumber,
}
if u.Groups, err = m.db.GetAccountGroups(ctx, account.UserID); err != nil {
return nil, nil, err
}
var scopes map[string]*authpb.Scope
if userID != nil && (userID.Type == user.UserType_USER_TYPE_LIGHTWEIGHT || userID.Type == user.UserType_USER_TYPE_FEDERATED) {
scopes, err = scope.AddLightweightAccountScope(authpb.Role_ROLE_OWNER, nil)
if err != nil {
return nil, nil, err
}
} else {
scopes, err = scope.AddOwnerScope(nil)
if err != nil {
return nil, nil, err
}
}
// do not log password hash
account.PasswordHash = "***redacted***"
log.Debug().Interface("account", account).Interface("user", u).Msg("authenticated user")
return u, scopes, nil
}
func (m *manager) verify(password, hash string) bool {
splitHash := strings.SplitN(hash, "|", 2)
switch len(splitHash) {
case 2:
if splitHash[0] == "1" {
return m.verifyHashV1(password, splitHash[1])
}
case 1:
return m.legacyHashVerify(password, hash)
}
return false
}
func (m *manager) legacyHashVerify(password, hash string) bool {
// TODO rehash $newHash = $this->hash($message);
switch len(hash) {
case 60: // legacy PHPass hash
return nil == bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+m.c.LegacySalt))
case 40: // legacy sha1 hash
h := sha1.Sum([]byte(password))
return hmac.Equal([]byte(hash), []byte(hex.EncodeToString(h[:])))
}
return false
}
func (m *manager) verifyHashV1(password, hash string) bool {
// TODO implement password_needs_rehash
return nil == bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}
@@ -435,6 +435,7 @@ func (session *DecomposedFsSession) Cleanup(revertNodeMetadata, cleanBin, cleanI
revisionNode, err := node.ReadNode(ctx, session.store.lu, session.SpaceID(), versionID, "", false, n.SpaceRoot, false)
if err != nil {
sublog.Error().Err(err).Str("versionID", versionID).Msg("reading revision node failed")
return
}
if !revisionNode.Exists {
@@ -24,7 +24,5 @@ import (
_ "github.com/opencloud-eu/reva/v2/pkg/user/manager/json"
_ "github.com/opencloud-eu/reva/v2/pkg/user/manager/ldap"
_ "github.com/opencloud-eu/reva/v2/pkg/user/manager/memory"
_ "github.com/opencloud-eu/reva/v2/pkg/user/manager/nextcloud"
_ "github.com/opencloud-eu/reva/v2/pkg/user/manager/owncloudsql"
// Add your own here
)
@@ -1,249 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package nextcloud
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/user"
"github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
"github.com/pkg/errors"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
// "github.com/opencloud-eu/reva/v2/pkg/errtypes"
)
func init() {
registry.Register("nextcloud", New)
}
// Manager is the Nextcloud-based implementation of the share.Manager interface
// see https://github.com/cs3org/reva/blob/v1.13.0/pkg/user/user.go#L29-L35
type Manager struct {
client *http.Client
sharedSecret string
endPoint string
}
// UserManagerConfig contains config for a Nextcloud-based UserManager
type UserManagerConfig struct {
EndPoint string `mapstructure:"endpoint" docs:";The Nextcloud backend endpoint for user management"`
SharedSecret string `mapstructure:"shared_secret"`
MockHTTP bool `mapstructure:"mock_http"`
}
func (c *UserManagerConfig) init() {
if c.EndPoint == "" {
c.EndPoint = "http://localhost/end/point?"
}
}
func parseConfig(m map[string]interface{}) (*UserManagerConfig, error) {
c := &UserManagerConfig{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
c.init()
return c, nil
}
// Action describes a REST request to forward to the Nextcloud backend
type Action struct {
verb string
argS string
}
// New returns a user manager implementation that reads a json file to provide user metadata.
func New(m map[string]interface{}) (user.Manager, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
return NewUserManager(c)
}
// NewUserManager returns a new Nextcloud-based UserManager
func NewUserManager(c *UserManagerConfig) (*Manager, error) {
var client *http.Client
if c.MockHTTP {
// Wait for SetHTTPClient to be called later
client = nil
} else {
if len(c.EndPoint) == 0 {
return nil, errors.New("Please specify 'endpoint' in '[grpc.services.userprovider.drivers.nextcloud]'")
}
client = &http.Client{}
}
return &Manager{
endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/"
sharedSecret: c.SharedSecret,
client: client,
}, nil
}
// SetHTTPClient sets the HTTP client
func (um *Manager) SetHTTPClient(c *http.Client) {
um.client = c
}
func getUser(ctx context.Context) (*userpb.User, error) {
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
err := errors.Wrap(errtypes.UserRequired(""), "nextcloud storage driver: error getting user from ctx")
return nil, err
}
return u, nil
}
func (um *Manager) do(ctx context.Context, a Action, username string) (int, []byte, error) {
url := um.endPoint + "~" + username + "/api/user/" + a.verb
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS))
if err != nil {
panic(err)
}
req.Header.Set("X-Reva-Secret", um.sharedSecret)
req.Header.Set("Content-Type", "application/json")
fmt.Println(url)
resp, err := um.client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
return resp.StatusCode, body, err
}
// Configure method as defined in https://github.com/cs3org/reva/blob/v1.13.0/pkg/user/user.go#L29-L35
func (um *Manager) Configure(ml map[string]interface{}) error {
return nil
}
// GetUser method as defined in https://github.com/cs3org/reva/blob/v1.13.0/pkg/user/user.go#L29-L35
func (um *Manager) GetUser(ctx context.Context, uid *userpb.UserId, skipFetchingGroups bool) (*userpb.User, error) {
if uid.GetTenantId() != "" {
return nil, errtypes.NotSupported("tenant filter not supported in nextcloud user manager")
}
bodyStr, _ := json.Marshal(uid)
_, respBody, err := um.do(ctx, Action{"GetUser", string(bodyStr)}, "unauthenticated")
if err != nil {
return nil, err
}
result := &userpb.User{}
err = json.Unmarshal(respBody, &result)
if err != nil {
return nil, err
}
return result, err
}
// GetUserByClaim method as defined in https://github.com/cs3org/reva/blob/v1.13.0/pkg/user/user.go#L29-L35
func (um *Manager) GetUserByClaim(ctx context.Context, claim, value, tenantID string, skipFetchingGroups bool) (*userpb.User, error) {
if tenantID != "" {
return nil, errtypes.NotSupported("tenant filter not supported in nextcloud user manager")
}
type paramsObj struct {
Claim string `json:"claim"`
Value string `json:"value"`
}
bodyObj := &paramsObj{
Claim: claim,
Value: value,
}
user, err := getUser(ctx)
if err != nil {
return nil, err
}
bodyStr, _ := json.Marshal(bodyObj)
_, respBody, err := um.do(ctx, Action{"GetUserByClaim", string(bodyStr)}, user.Username)
if err != nil {
return nil, err
}
result := &userpb.User{}
err = json.Unmarshal(respBody, &result)
if err != nil {
return nil, err
}
return result, err
}
// GetUserGroups method as defined in https://github.com/cs3org/reva/blob/v1.13.0/pkg/user/user.go#L29-L35
func (um *Manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) {
bodyStr, err := json.Marshal(uid)
if err != nil {
return nil, err
}
user, err := getUser(ctx)
if err != nil {
return nil, err
}
_, respBody, err := um.do(ctx, Action{"GetUserGroups", string(bodyStr)}, user.Username)
if err != nil {
return nil, err
}
var gs []string
err = json.Unmarshal(respBody, &gs)
if err != nil {
return nil, err
}
return gs, err
}
// FindUsers method as defined in https://github.com/cs3org/reva/blob/v1.13.0/pkg/user/user.go#L29-L35
func (um *Manager) FindUsers(ctx context.Context, query, tenantID string, skipFetchingGroups bool) ([]*userpb.User, error) {
if tenantID != "" {
return nil, errtypes.NotSupported("tenant filter not supported in nextcloud user manager")
}
user, err := getUser(ctx)
if err != nil {
return nil, err
}
_, respBody, err := um.do(ctx, Action{"FindUsers", query}, user.Username)
if err != nil {
return nil, err
}
var respArr []userpb.User
err = json.Unmarshal(respBody, &respArr)
if err != nil {
return nil, err
}
var pointers = make([]*userpb.User, len(respArr))
for i := 0; i < len(respArr); i++ {
pointers[i] = &respArr[i]
}
return pointers, err
}
@@ -1,103 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package nextcloud
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"strings"
)
// Response contains data for the Nextcloud mock server to respond
// and to switch to a new server state
type Response struct {
code int
body string
newServerState string
}
const serverStateError = "ERROR"
const serverStateEmpty = "EMPTY"
const serverStateHome = "HOME"
var serverState = serverStateEmpty
var responses = map[string]Response{
`POST /apps/sciencemesh/~unauthenticated/api/user/GetUser {"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}`: {200, `{"id":{"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}}`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/user/GetUserByClaim {"claim":"claim-string","value":"value-string"}`: {200, `{"id":{"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}}`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/user/GetUserGroups {"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}`: {200, `["wine-lovers"]`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/user/FindUsers some-query`: {200, `[{"id":{"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}}]`, serverStateHome},
}
// GetNextcloudServerMock returns a handler that pretends to be a remote Nextcloud server
func GetNextcloudServerMock(called *[]string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buf := new(strings.Builder)
_, err := io.Copy(buf, r.Body)
if err != nil {
panic("Error reading response into buffer")
}
var key = fmt.Sprintf("%s %s %s", r.Method, r.URL, buf.String())
*called = append(*called, key)
response := responses[key]
if (response == Response{}) {
key = fmt.Sprintf("%s %s %s %s", r.Method, r.URL, buf.String(), serverState)
// *called = append(*called, key)
response = responses[key]
}
if (response == Response{}) {
fmt.Printf("%s %s %s %s", r.Method, r.URL, buf.String(), serverState)
response = Response{500, fmt.Sprintf("response not defined! %s", key), serverStateEmpty}
}
serverState = responses[key].newServerState
if serverState == `` {
serverState = serverStateError
}
w.WriteHeader(response.code)
// w.Header().Set("Etag", "mocker-etag")
_, err = w.Write([]byte(responses[key].body))
if err != nil {
panic(err)
}
})
}
// TestingHTTPClient thanks to https://itnext.io/how-to-stub-requests-to-remote-hosts-with-go-6c2c1db32bf2
// Ideally, this function would live in tests/helpers, but
// if we put it there, it gets excluded by .dockerignore, and the
// Docker build fails (see https://github.com/cs3org/reva/issues/1999)
// So putting it here for now - open to suggestions if someone knows
// a better way to inject this.
func TestingHTTPClient(handler http.Handler) (*http.Client, func()) {
s := httptest.NewServer(handler)
cli := &http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, network, _ string) (net.Conn, error) {
return net.Dial(network, s.Listener.Addr().String())
},
},
}
return cli, s.Close
}
@@ -1,228 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package accounts
import (
"context"
"database/sql"
"strings"
"time"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/pkg/errors"
)
// Accounts represents oc10-style Accounts
type Accounts struct {
driver string
db *sql.DB
joinUsername, joinUUID, enableMedialSearch bool
selectSQL string
}
// NewMysql returns a new Cache instance connecting to a MySQL database
func NewMysql(dsn string, joinUsername, joinUUID, enableMedialSearch bool) (*Accounts, error) {
sqldb, err := sql.Open("mysql", dsn)
if err != nil {
return nil, errors.Wrap(err, "error connecting to the database")
}
// FIXME make configurable
sqldb.SetConnMaxLifetime(time.Minute * 3)
sqldb.SetConnMaxIdleTime(time.Second * 30)
sqldb.SetMaxOpenConns(100)
sqldb.SetMaxIdleConns(10)
err = sqldb.Ping()
if err != nil {
return nil, errors.Wrap(err, "error connecting to the database")
}
return New("mysql", sqldb, joinUsername, joinUUID, enableMedialSearch)
}
// New returns a new Cache instance connecting to the given sql.DB
func New(driver string, sqldb *sql.DB, joinUsername, joinUUID, enableMedialSearch bool) (*Accounts, error) {
sel := "SELECT id, email, user_id, display_name, quota, last_login, backend, home, state"
from := `
FROM oc_accounts a
`
if joinUsername {
sel += ", p.configvalue AS username"
from += `LEFT JOIN oc_preferences p
ON a.user_id=p.userid
AND p.appid='core'
AND p.configkey='username'`
} else {
// fallback to user_id as username
sel += ", user_id AS username"
}
if joinUUID {
sel += ", p2.configvalue AS ownclouduuid"
from += `LEFT JOIN oc_preferences p2
ON a.user_id=p2.userid
AND p2.appid='core'
AND p2.configkey='ownclouduuid'`
} else {
// fallback to user_id as ownclouduuid
sel += ", user_id AS ownclouduuid"
}
return &Accounts{
driver: driver,
db: sqldb,
joinUsername: joinUsername,
joinUUID: joinUUID,
enableMedialSearch: enableMedialSearch,
selectSQL: sel + from,
}, nil
}
// Account stores information about accounts.
type Account struct {
ID uint64
Email sql.NullString
UserID string
DisplayName sql.NullString
Quota sql.NullString
LastLogin int
Backend string
Home string
State int8
Username sql.NullString // optional comes from the oc_preferences
OwnCloudUUID sql.NullString // optional comes from the oc_preferences
}
func (as *Accounts) rowToAccount(ctx context.Context, row Scannable) (*Account, error) {
a := Account{}
if err := row.Scan(&a.ID, &a.Email, &a.UserID, &a.DisplayName, &a.Quota, &a.LastLogin, &a.Backend, &a.Home, &a.State, &a.Username, &a.OwnCloudUUID); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
return nil, err
}
return &a, nil
}
// Scannable describes the interface providing a Scan method
type Scannable interface {
Scan(...interface{}) error
}
// GetAccountByClaim fetches an account by mail, username or userid
func (as *Accounts) GetAccountByClaim(ctx context.Context, claim, value string) (*Account, error) {
// TODO align supported claims with rest driver and the others, maybe refactor into common mapping
var row *sql.Row
var where string
switch claim {
case "mail":
where = "WHERE a.email=?"
// case "uid":
// claim = m.c.Schema.UIDNumber
// case "gid":
// claim = m.c.Schema.GIDNumber
case "username":
if as.joinUsername {
where = "WHERE p.configvalue=?"
} else {
// use user_id as username
where = "WHERE a.user_id=?"
}
case "userid":
if as.joinUUID {
where = "WHERE p2.configvalue=?"
} else {
// use user_id as uuid
where = "WHERE a.user_id=?"
}
default:
return nil, errors.New("owncloudsql: invalid field " + claim)
}
row = as.db.QueryRowContext(ctx, as.selectSQL+where, value)
return as.rowToAccount(ctx, row)
}
func sanitizeWildcards(q string) string {
return strings.ReplaceAll(strings.ReplaceAll(q, "%", `\%`), "_", `\_`)
}
// FindAccounts searches userid, displayname and email using the given query. The Wildcard caracters % and _ are escaped.
func (as *Accounts) FindAccounts(ctx context.Context, query string) ([]Account, error) {
if as.enableMedialSearch {
query = "%" + sanitizeWildcards(query) + "%"
}
// TODO join oc_account_terms
where := "WHERE a.user_id LIKE ? OR a.display_name LIKE ? OR a.email LIKE ?"
args := []interface{}{query, query, query}
if as.joinUsername {
where += " OR p.configvalue LIKE ?"
args = append(args, query)
}
if as.joinUUID {
where += " OR p2.configvalue LIKE ?"
args = append(args, query)
}
rows, err := as.db.QueryContext(ctx, as.selectSQL+where, args...)
if err != nil {
return nil, err
}
defer rows.Close()
accounts := []Account{}
for rows.Next() {
a := Account{}
if err := rows.Scan(&a.ID, &a.Email, &a.UserID, &a.DisplayName, &a.Quota, &a.LastLogin, &a.Backend, &a.Home, &a.State, &a.Username, &a.OwnCloudUUID); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
continue
}
accounts = append(accounts, a)
}
if err = rows.Err(); err != nil {
return nil, err
}
return accounts, nil
}
// GetAccountGroups lasts the groups for an account
func (as *Accounts) GetAccountGroups(ctx context.Context, uid string) ([]string, error) {
rows, err := as.db.QueryContext(ctx, "SELECT gid FROM oc_group_user WHERE uid=?", uid)
if err != nil {
return nil, err
}
defer rows.Close()
groups := []string{}
for rows.Next() {
var group string
if err := rows.Scan(&group); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("could not scan row, skipping")
continue
}
groups = append(groups, group)
}
if err = rows.Err(); err != nil {
return nil, err
}
return groups, nil
}
@@ -1,199 +0,0 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package owncloudsql
import (
"context"
"database/sql"
"fmt"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/user"
"github.com/opencloud-eu/reva/v2/pkg/user/manager/owncloudsql/accounts"
"github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
"github.com/pkg/errors"
// Provides mysql drivers
_ "github.com/go-sql-driver/mysql"
)
func init() {
registry.Register("owncloudsql", NewMysql)
}
type manager struct {
c *config
db *accounts.Accounts
}
type config struct {
DbUsername string `mapstructure:"dbusername"`
DbPassword string `mapstructure:"dbpassword"`
DbHost string `mapstructure:"dbhost"`
DbPort int `mapstructure:"dbport"`
DbName string `mapstructure:"dbname"`
Idp string `mapstructure:"idp"`
Nobody int64 `mapstructure:"nobody"`
JoinUsername bool `mapstructure:"join_username"`
JoinOwnCloudUUID bool `mapstructure:"join_ownclouduuid"`
EnableMedialSearch bool `mapstructure:"enable_medial_search"`
}
// NewMysql returns a new user manager connection to an owncloud mysql database
func NewMysql(m map[string]interface{}) (user.Manager, error) {
mgr := &manager{}
err := mgr.Configure(m)
if err != nil {
err = errors.Wrap(err, "error creating a new manager")
return nil, err
}
mgr.db, err = accounts.NewMysql(
fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", mgr.c.DbUsername, mgr.c.DbPassword, mgr.c.DbHost, mgr.c.DbPort, mgr.c.DbName),
mgr.c.JoinUsername,
mgr.c.JoinOwnCloudUUID,
mgr.c.EnableMedialSearch,
)
if err != nil {
return nil, err
}
return mgr, nil
}
func (m *manager) Configure(ml map[string]interface{}) error {
c, err := parseConfig(ml)
if err != nil {
return err
}
if c.Nobody == 0 {
c.Nobody = 99
}
m.c = c
return nil
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, &c); err != nil {
return nil, err
}
return c, nil
}
func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId, skipFetchingGroups bool) (*userpb.User, error) {
if uid.GetTenantId() != "" {
return nil, errtypes.NotSupported("tenant filter not supported in opencloudsql user manager")
}
// search via the user_id
a, err := m.db.GetAccountByClaim(ctx, "userid", uid.OpaqueId)
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(uid.OpaqueId)
}
return m.convertToCS3User(ctx, a, skipFetchingGroups)
}
func (m *manager) GetUserByClaim(ctx context.Context, claim, value, tenantID string, skipFetchingGroups bool) (*userpb.User, error) {
if tenantID != "" {
return nil, errtypes.NotSupported("tenant filter not supported in opencloudsql user manager")
}
a, err := m.db.GetAccountByClaim(ctx, claim, value)
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(claim + "=" + value)
} else if err != nil {
return nil, err
}
return m.convertToCS3User(ctx, a, skipFetchingGroups)
}
func (m *manager) FindUsers(ctx context.Context, query, tenantID string, skipFetchingGroups bool) ([]*userpb.User, error) {
if tenantID != "" {
return nil, errtypes.NotSupported("tenant filter not supported in opencloudsql user manager")
}
accounts, err := m.db.FindAccounts(ctx, query)
if err == sql.ErrNoRows {
return nil, errtypes.NotFound("no users found for " + query)
} else if err != nil {
return nil, err
}
users := make([]*userpb.User, 0, len(accounts))
for i := range accounts {
u, err := m.convertToCS3User(ctx, &accounts[i], skipFetchingGroups)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Interface("account", accounts[i]).Msg("could not convert account, skipping")
continue
}
users = append(users, u)
}
return users, nil
}
func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) {
groups, err := m.db.GetAccountGroups(ctx, uid.OpaqueId)
if err == sql.ErrNoRows {
return nil, errtypes.NotFound("no groups found for uid " + uid.OpaqueId)
} else if err != nil {
return nil, err
}
return groups, nil
}
func (m *manager) convertToCS3User(ctx context.Context, a *accounts.Account, skipFetchingGroups bool) (*userpb.User, error) {
u := &userpb.User{
Id: &userpb.UserId{
Idp: m.c.Idp,
OpaqueId: a.OwnCloudUUID.String,
Type: userpb.UserType_USER_TYPE_PRIMARY,
},
Username: a.Username.String,
Mail: a.Email.String,
DisplayName: a.DisplayName.String,
//Groups: groups,
GidNumber: m.c.Nobody,
UidNumber: m.c.Nobody,
}
// https://github.com/cs3org/reva/pull/4135
// fall back to userid
if u.Id.OpaqueId == "" {
u.Id.OpaqueId = a.UserID
}
if u.Username == "" {
u.Username = u.Id.OpaqueId
}
if u.DisplayName == "" {
u.DisplayName = u.Id.OpaqueId
}
if !skipFetchingGroups {
var err error
if u.Groups, err = m.GetUserGroups(ctx, u.Id); err != nil {
return nil, err
}
}
return u, nil
}