Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+280
View File
@@ -0,0 +1,280 @@
// Copyright 2018-2023 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 client
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"time"
"github.com/opencloud-eu/reva/v2/internal/http/services/ocmd"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/pkg/errors"
)
// ErrTokenInvalid is the error returned by the invite-accepted
// endpoint when the token is not valid.
var ErrTokenInvalid = errors.New("the invitation token is invalid")
// ErrServiceNotTrusted is the error returned by the invite-accepted
// endpoint when the service is not trusted to accept invitations.
var ErrServiceNotTrusted = errors.New("service is not trusted to accept invitations")
// ErrUserAlreadyAccepted is the error returned by the invite-accepted
// endpoint when a user is already know by the remote cloud.
var ErrUserAlreadyAccepted = errors.New("user already accepted an invitation token")
// ErrTokenNotFound is the error returned by the invite-accepted
// endpoint when the request is done using a not existing token.
var ErrTokenNotFound = errors.New("token not found")
// ErrInvalidParameters is the error returned by the shares endpoint
// when the request does not contain required properties.
var ErrInvalidParameters = errors.New("invalid parameters")
// OCMClient is the client for an OCM provider.
type OCMClient struct {
client *http.Client
}
// Config is the configuration to be used for the OCMClient.
type Config struct {
Timeout time.Duration
Insecure bool
}
// New returns a new OCMClient.
func New(c *Config) *OCMClient {
return &OCMClient{
client: rhttp.GetHTTPClient(
rhttp.Timeout(c.Timeout),
rhttp.Insecure(c.Insecure),
),
}
}
// InviteAcceptedRequest contains the parameters for accepting
// an invitation.
type InviteAcceptedRequest struct {
UserID string `json:"userID"`
Email string `json:"email"`
Name string `json:"name"`
RecipientProvider string `json:"recipientProvider"`
Token string `json:"token"`
}
// User contains the remote user's information when accepting
// an invitation.
type User struct {
UserID string `json:"userID"`
Email string `json:"email"`
Name string `json:"name"`
}
func (r *InviteAcceptedRequest) toJSON() (io.Reader, error) {
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(r); err != nil {
return nil, err
}
return &b, nil
}
// InviteAccepted informs the sender that the invitation was accepted to start sharing
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post
func (c *OCMClient) InviteAccepted(ctx context.Context, endpoint string, r *InviteAcceptedRequest) (*User, error) {
url, err := url.JoinPath(endpoint, "invite-accepted")
if err != nil {
return nil, err
}
body, err := r.toJSON()
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return nil, errors.Wrap(err, "error creating request")
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "error doing request")
}
defer resp.Body.Close()
return c.parseInviteAcceptedResponse(resp)
}
func (c *OCMClient) parseInviteAcceptedResponse(r *http.Response) (*User, error) {
switch r.StatusCode {
case http.StatusOK:
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, errors.Wrap(err, "error decoding response body")
}
return &u, nil
case http.StatusBadRequest:
return nil, ErrTokenInvalid
case http.StatusNotFound:
return nil, ErrTokenNotFound
case http.StatusConflict:
return nil, ErrUserAlreadyAccepted
case http.StatusForbidden:
return nil, ErrServiceNotTrusted
}
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, errors.Wrap(err, "error decoding response body")
}
return nil, errtypes.InternalError(string(body))
}
// NewShareRequest contains the parameters for creating a new OCM share.
type NewShareRequest struct {
ShareWith string `json:"shareWith"`
Name string `json:"name"`
Description string `json:"description"`
ProviderID string `json:"providerId"`
Owner string `json:"owner"`
Sender string `json:"sender"`
OwnerDisplayName string `json:"ownerDisplayName"`
SenderDisplayName string `json:"senderDisplayName"`
ShareType string `json:"shareType"`
Expiration uint64 `json:"expiration"`
ResourceType string `json:"resourceType"`
Protocols ocmd.Protocols `json:"protocol"`
}
func (r *NewShareRequest) toJSON() (io.Reader, error) {
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(r); err != nil {
return nil, err
}
return &b, nil
}
// NewShareResponse is the response returned when creating a new share.
type NewShareResponse struct {
RecipientDisplayName string `json:"recipientDisplayName"`
}
// NewShare creates a new share.
// https://github.com/cs3org/OCM-API/blob/develop/spec.yaml
func (c *OCMClient) NewShare(ctx context.Context, endpoint string, r *NewShareRequest) (*NewShareResponse, error) {
url, err := url.JoinPath(endpoint, "shares")
if err != nil {
return nil, err
}
body, err := r.toJSON()
if err != nil {
return nil, err
}
log := appctx.GetLogger(ctx)
log.Debug().Msgf("Sending OCM /shares POST to %s: %s", url, body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return nil, errors.Wrap(err, "error creating request")
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "error doing request")
}
defer resp.Body.Close()
return c.parseNewShareResponse(resp)
}
func (c *OCMClient) parseNewShareResponse(r *http.Response) (*NewShareResponse, error) {
switch r.StatusCode {
case http.StatusOK, http.StatusCreated:
var res NewShareResponse
err := json.NewDecoder(r.Body).Decode(&res)
return &res, err
case http.StatusBadRequest:
return nil, ErrInvalidParameters
case http.StatusUnauthorized, http.StatusForbidden:
return nil, ErrServiceNotTrusted
}
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, errors.Wrap(err, "error decoding response body")
}
return nil, errtypes.InternalError(string(body))
}
// Capabilities contains a set of properties exposed by
// a remote cloud storage.
type Capabilities struct {
Enabled bool `json:"enabled"`
APIVersion string `json:"apiVersion"`
EndPoint string `json:"endPoint"`
Provider string `json:"provider"`
ResourceTypes []struct {
Name string `json:"name"`
ShareTypes []string `json:"shareTypes"`
Protocols struct {
Webdav *string `json:"webdav"`
Webapp *string `json:"webapp"`
Datatx *string `json:"datatx"`
} `json:"protocols"`
} `json:"resourceTypes"`
Capabilities []string `json:"capabilities"`
}
// Discovery returns a number of properties used to discover the capabilities offered by a remote cloud storage.
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1ocm-provider/get
func (c *OCMClient) Discovery(ctx context.Context, endpoint string) (*Capabilities, error) {
url, err := url.JoinPath(endpoint, "shares")
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, errors.Wrap(err, "error creating request")
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "error doing request")
}
defer resp.Body.Close()
var cap Capabilities
if err := json.NewDecoder(resp.Body).Decode(&c); err != nil {
return nil, err
}
return &cap, nil
}
+58
View File
@@ -0,0 +1,58 @@
// Copyright 2018-2023 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 invite
import (
"context"
"errors"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
)
// Repository is the interfaces used to store the tokens and the invited users.
type Repository interface {
// AddToken stores the token in the repository.
AddToken(ctx context.Context, token *invitepb.InviteToken) error
// GetToken gets the token from the repository.
GetToken(ctx context.Context, token string) (*invitepb.InviteToken, error)
// ListTokens gets the valid tokens from the repository (i.e. not expired).
ListTokens(ctx context.Context, initiator *userpb.UserId) ([]*invitepb.InviteToken, error)
// AddRemoteUser stores the remote user.
AddRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.User) error
// GetRemoteUser retrieves details about a remote user who has accepted an invite to share.
GetRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUserID *userpb.UserId) (*userpb.User, error)
// FindRemoteUsers finds remote users who have accepted invites based on their attributes.
FindRemoteUsers(ctx context.Context, initiator *userpb.UserId, query string) ([]*userpb.User, error)
// DeleteRemoteUser removes from the remote user from the initiator's list.
DeleteRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.UserId) error
}
// ErrTokenNotFound is the error returned when the token does not exist.
var ErrTokenNotFound = errors.New("token not found")
// ErrUserAlreadyAccepted is the error returned when the user was
// already added to the accepted users list.
var ErrUserAlreadyAccepted = errors.New("user already added to accepted users")
@@ -0,0 +1,278 @@
// Copyright 2018-2023 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 json
import (
"context"
"encoding/json"
"io"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/invite"
"github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/registry"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
"github.com/opencloud-eu/reva/v2/pkg/utils/list"
"github.com/pkg/errors"
)
type inviteModel struct {
File string
Invites map[string]*invitepb.InviteToken `json:"invites"`
AcceptedUsers map[string][]*userpb.User `json:"accepted_users"`
}
type manager struct {
config *config
sync.RWMutex // concurrent access to the file
model *inviteModel
}
type config struct {
File string `mapstructure:"file"`
}
func init() {
registry.Register("json", New)
}
func (c *config) ApplyDefaults() {
if c.File == "" {
c.File = "/var/tmp/reva/ocm-invites.json"
}
}
// New returns a new invite manager object.
func New(m map[string]interface{}) (invite.Repository, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
// load or create file
model, err := loadOrCreate(c.File)
if err != nil {
return nil, errors.Wrap(err, "error loading the file containing the invites")
}
manager := &manager{
config: &c,
model: model,
}
return manager, nil
}
func loadOrCreate(file string) (*inviteModel, error) {
_, err := os.Stat(file)
if os.IsNotExist(err) {
if err := os.MkdirAll(filepath.Dir(file), 0700); err != nil {
return nil, errors.Wrap(err, "error creating the ocm storage dir: "+filepath.Dir(file))
}
if err := os.WriteFile(file, []byte("{}"), 0700); err != nil {
return nil, errors.Wrap(err, "error creating the invite storage file: "+file)
}
}
fd, err := os.OpenFile(file, os.O_CREATE, 0644)
if err != nil {
return nil, errors.Wrap(err, "error opening the invite storage file: "+file)
}
defer fd.Close()
data, err := io.ReadAll(fd)
if err != nil {
return nil, errors.Wrap(err, "error reading the data")
}
model := &inviteModel{}
if err := json.Unmarshal(data, model); err != nil {
return nil, errors.Wrap(err, "error decoding invite data to json")
}
if model.Invites == nil {
model.Invites = make(map[string]*invitepb.InviteToken)
}
if model.AcceptedUsers == nil {
model.AcceptedUsers = make(map[string][]*userpb.User)
}
model.File = file
return model, nil
}
func (model *inviteModel) save() error {
data, err := json.Marshal(model)
if err != nil {
return errors.Wrap(err, "error encoding invite data to json")
}
if err := os.WriteFile(model.File, data, 0644); err != nil {
return errors.Wrap(err, "error writing invite data to file: "+model.File)
}
return nil
}
func (m *manager) AddToken(ctx context.Context, token *invitepb.InviteToken) error {
m.Lock()
defer m.Unlock()
m.model.Invites[token.GetToken()] = token
if err := m.model.save(); err != nil {
return errors.Wrap(err, "json: error saving model")
}
return nil
}
func (m *manager) GetToken(ctx context.Context, token string) (*invitepb.InviteToken, error) {
m.RLock()
defer m.RUnlock()
if tkn, ok := m.model.Invites[token]; ok {
return tkn, nil
}
return nil, invite.ErrTokenNotFound
}
func (m *manager) ListTokens(ctx context.Context, initiator *userpb.UserId) ([]*invitepb.InviteToken, error) {
m.RLock()
defer m.RUnlock()
tokens := []*invitepb.InviteToken{}
for _, token := range m.model.Invites {
if utils.UserEqual(token.UserId, initiator) && !tokenIsExpired(token) {
tokens = append(tokens, token)
}
}
return tokens, nil
}
func tokenIsExpired(token *invitepb.InviteToken) bool {
return token.Expiration != nil && token.Expiration.Seconds < uint64(time.Now().Unix())
}
func (m *manager) AddRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.User) error {
m.Lock()
defer m.Unlock()
for _, acceptedUser := range m.model.AcceptedUsers[initiator.GetOpaqueId()] {
if acceptedUser.Id.GetOpaqueId() == remoteUser.Id.OpaqueId && idpsEqual(acceptedUser.Id.GetIdp(), remoteUser.Id.Idp) {
return invite.ErrUserAlreadyAccepted
}
}
m.model.AcceptedUsers[initiator.GetOpaqueId()] = append(m.model.AcceptedUsers[initiator.GetOpaqueId()], remoteUser)
if err := m.model.save(); err != nil {
return errors.Wrap(err, "json: error saving model")
}
return nil
}
func idpsEqual(idp1, idp2 string) bool {
normalizeIDP := func(s string) (string, error) {
u, err := url.Parse(s)
if err != nil {
return "", errors.New("could not parse url")
}
if u.Scheme == "" {
return strings.ToLower(u.Path), nil // the string is just a hostname
}
return strings.ToLower(u.Hostname()), nil
}
domain1, err := normalizeIDP(idp1)
if err != nil {
return false
}
domain2, err := normalizeIDP(idp2)
if err != nil {
return false
}
return domain1 == domain2
}
func (m *manager) GetRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUserID *userpb.UserId) (*userpb.User, error) {
m.RLock()
defer m.RUnlock()
log := appctx.GetLogger(ctx)
for _, acceptedUser := range m.model.AcceptedUsers[initiator.GetOpaqueId()] {
log.Info().Msgf("looking for '%s' at '%s' - considering '%s' at '%s'",
remoteUserID.OpaqueId,
remoteUserID.Idp,
acceptedUser.Id.GetOpaqueId(),
acceptedUser.Id.GetIdp(),
)
if (acceptedUser.Id.GetOpaqueId() == remoteUserID.OpaqueId) && (remoteUserID.Idp == "" || idpsEqual(acceptedUser.Id.GetIdp(), remoteUserID.Idp)) {
return acceptedUser, nil
}
}
return nil, errtypes.NotFound(remoteUserID.OpaqueId)
}
func (m *manager) FindRemoteUsers(ctx context.Context, initiator *userpb.UserId, query string) ([]*userpb.User, error) {
m.RLock()
defer m.RUnlock()
users := []*userpb.User{}
for _, acceptedUser := range m.model.AcceptedUsers[initiator.GetOpaqueId()] {
if query == "" || userContains(acceptedUser, query) {
users = append(users, acceptedUser)
}
}
return users, nil
}
func userContains(u *userpb.User, query string) bool {
query = strings.ToLower(query)
return strings.Contains(strings.ToLower(u.Username), query) || strings.Contains(strings.ToLower(u.DisplayName), query) ||
strings.Contains(strings.ToLower(u.Mail), query) || strings.Contains(strings.ToLower(u.Id.OpaqueId), query)
}
func (m *manager) DeleteRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.UserId) error {
m.Lock()
defer m.Unlock()
acceptedUsers, ok := m.model.AcceptedUsers[initiator.GetOpaqueId()]
if !ok {
return nil
}
for i, user := range acceptedUsers {
if (user.Id.GetOpaqueId() == remoteUser.OpaqueId) && (remoteUser.Idp == "" || user.Id.GetIdp() == remoteUser.Idp) {
acceptedUsers = list.Remove(acceptedUsers, i)
m.model.AcceptedUsers[initiator.GetOpaqueId()] = acceptedUsers
_ = m.model.save()
return nil
}
}
return nil
}
@@ -0,0 +1,26 @@
// Copyright 2018-2023 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 loader
import (
// Load core share manager drivers.
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/json"
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/memory"
// Add your own here.
)
@@ -0,0 +1,146 @@
// Copyright 2018-2023 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 memory
import (
"context"
"strings"
"sync"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/invite"
"github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/registry"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/list"
)
func init() {
registry.Register("memory", New)
}
// New returns a new invite manager.
func New(m map[string]interface{}) (invite.Repository, error) {
return &manager{
Invites: sync.Map{},
AcceptedUsers: sync.Map{},
}, nil
}
type manager struct {
Invites sync.Map
AcceptedUsers sync.Map
}
func (m *manager) AddToken(ctx context.Context, token *invitepb.InviteToken) error {
m.Invites.Store(token.GetToken(), token)
return nil
}
func (m *manager) GetToken(ctx context.Context, token string) (*invitepb.InviteToken, error) {
if v, ok := m.Invites.Load(token); ok {
return v.(*invitepb.InviteToken), nil
}
return nil, invite.ErrTokenNotFound
}
func (m *manager) ListTokens(ctx context.Context, initiator *userpb.UserId) ([]*invitepb.InviteToken, error) {
tokens := []*invitepb.InviteToken{}
m.Invites.Range(func(_, value any) bool {
token := value.(*invitepb.InviteToken)
if utils.UserEqual(token.UserId, initiator) {
tokens = append(tokens, token)
}
return true
})
return tokens, nil
}
func (m *manager) AddRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.User) error {
usersList, ok := m.AcceptedUsers.Load(initiator)
acceptedUsers := usersList.([]*userpb.User)
if ok {
for _, acceptedUser := range acceptedUsers {
if acceptedUser.Id.GetOpaqueId() == remoteUser.Id.OpaqueId && acceptedUser.Id.GetIdp() == remoteUser.Id.Idp {
return invite.ErrUserAlreadyAccepted
}
}
acceptedUsers = append(acceptedUsers, remoteUser)
m.AcceptedUsers.Store(initiator.GetOpaqueId(), acceptedUsers)
} else {
acceptedUsers := []*userpb.User{remoteUser}
m.AcceptedUsers.Store(initiator.GetOpaqueId(), acceptedUsers)
}
return nil
}
func (m *manager) GetRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUserID *userpb.UserId) (*userpb.User, error) {
usersList, ok := m.AcceptedUsers.Load(initiator)
if !ok {
return nil, errtypes.NotFound(remoteUserID.OpaqueId)
}
acceptedUsers := usersList.([]*userpb.User)
for _, acceptedUser := range acceptedUsers {
if (acceptedUser.Id.GetOpaqueId() == remoteUserID.OpaqueId) && (remoteUserID.Idp == "" || acceptedUser.Id.GetIdp() == remoteUserID.Idp) {
return acceptedUser, nil
}
}
return nil, errtypes.NotFound(remoteUserID.OpaqueId)
}
func (m *manager) FindRemoteUsers(ctx context.Context, initiator *userpb.UserId, query string) ([]*userpb.User, error) {
usersList, ok := m.AcceptedUsers.Load(initiator)
if !ok {
return []*userpb.User{}, nil
}
users := []*userpb.User{}
acceptedUsers := usersList.([]*userpb.User)
for _, acceptedUser := range acceptedUsers {
if query == "" || userContains(acceptedUser, query) {
users = append(users, acceptedUser)
}
}
return users, nil
}
func userContains(u *userpb.User, query string) bool {
query = strings.ToLower(query)
return strings.Contains(strings.ToLower(u.Username), query) || strings.Contains(strings.ToLower(u.DisplayName), query) ||
strings.Contains(strings.ToLower(u.Mail), query) || strings.Contains(strings.ToLower(u.Id.OpaqueId), query)
}
func (m *manager) DeleteRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.UserId) error {
usersList, ok := m.AcceptedUsers.Load(initiator)
if !ok {
return nil
}
acceptedUsers := usersList.([]*userpb.User)
for i, user := range acceptedUsers {
if (user.Id.GetOpaqueId() == remoteUser.OpaqueId) && (remoteUser.Idp == "" || user.Id.GetIdp() == remoteUser.Idp) {
m.AcceptedUsers.Store(initiator, list.Remove(acceptedUsers, i))
return nil
}
}
return nil
}
@@ -0,0 +1,36 @@
// Copyright 2018-2023 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 registry
import (
"github.com/opencloud-eu/reva/v2/pkg/ocm/invite"
)
// NewFunc is the function that invite repositories
// should register at init time.
type NewFunc func(map[string]interface{}) (invite.Repository, error)
// NewFuncs is a map containing all the registered invite repositories.
var NewFuncs = map[string]NewFunc{}
// Register registers a new invite repository new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
}
@@ -0,0 +1,233 @@
// Copyright 2018-2023 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 json
import (
"context"
"encoding/json"
"net"
"net/url"
"os"
"regexp"
"strings"
"sync"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/pkg/errors"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider/authorizer/registry"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
)
func init() {
registry.Register("json", New)
}
var (
ErrNoIP = errtypes.NotSupported("No IP provided")
)
// New returns a new authorizer object.
func New(m map[string]interface{}) (provider.Authorizer, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
providers := []*ocmprovider.ProviderInfo{}
f, err := os.ReadFile(c.Providers)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
} else {
err = json.Unmarshal(f, &providers)
if err != nil {
return nil, err
}
}
a := &authorizer{
providerIPs: sync.Map{},
conf: &c,
}
a.providers = a.getOCMProviders(providers)
return a, nil
}
type config struct {
Providers string `mapstructure:"providers"`
VerifyRequestHostname bool `mapstructure:"verify_request_hostname"`
}
func (c *config) ApplyTemplates() {
if c.Providers == "" {
c.Providers = "/etc/revad/ocm-providers.json"
}
}
type authorizer struct {
providers []*ocmprovider.ProviderInfo
providerIPs sync.Map
conf *config
}
func normalizeDomain(d string) (string, error) {
var urlString string
if strings.Contains(d, "://") {
urlString = d
} else {
urlString = "https://" + d
}
u, err := url.Parse(urlString)
if err != nil {
return "", err
}
return u.Host, nil
}
func (a *authorizer) GetInfoByDomain(_ context.Context, domain string) (*ocmprovider.ProviderInfo, error) {
normalizedDomain, err := normalizeDomain(domain)
if err != nil {
return nil, err
}
for _, p := range a.providers {
// we can exit early if this an exact match
if strings.Contains(p.Domain, normalizedDomain) {
return p, nil
}
// check if the domain matches a regex
if ok, err := regexp.MatchString(p.Domain, normalizedDomain); ok && err == nil {
// overwrite wildcards with the actual domain
for i, s := range p.Services {
s.Endpoint.Path = strings.ReplaceAll(s.Endpoint.Path, p.Domain, normalizedDomain)
s.Host = strings.ReplaceAll(s.Host, p.Domain, normalizedDomain)
p.Services[i] = s
}
p.Domain = normalizedDomain
return p, nil
}
}
return nil, errtypes.NotFound(domain)
}
func (a *authorizer) IsProviderAllowed(ctx context.Context, pi *ocmprovider.ProviderInfo) error {
log := appctx.GetLogger(ctx)
var err error
normalizedDomain, err := normalizeDomain(pi.Domain)
if err != nil {
return err
}
var providerAuthorized bool
if normalizedDomain != "" {
for _, p := range a.providers {
if ok, err := regexp.MatchString(p.Domain, normalizedDomain); ok && err == nil {
providerAuthorized = true
break
}
}
} else {
providerAuthorized = true
}
switch {
case !providerAuthorized:
return errtypes.NotFound(pi.GetDomain())
case !a.conf.VerifyRequestHostname:
return nil
case len(pi.Services) == 0:
return ErrNoIP
}
var ocmHost string
for _, p := range a.providers {
log.Debug().Msgf("Comparing '%s' to '%s'", p.Domain, normalizedDomain)
if p.Domain == normalizedDomain {
ocmHost, err = a.getOCMHost(p)
if err != nil {
return err
}
break
}
}
if ocmHost == "" {
return errtypes.InternalError("json: ocm host not specified for mesh provider")
}
providerAuthorized = false
var ipList []string
if hostIPs, ok := a.providerIPs.Load(ocmHost); ok {
ipList = hostIPs.([]string)
} else {
host, _, err := net.SplitHostPort(ocmHost)
if err != nil {
return errors.Wrap(err, "json: error looking up client IP")
}
addr, err := net.LookupIP(host)
if err != nil {
return errors.Wrap(err, "json: error looking up client IP")
}
for _, a := range addr {
ipList = append(ipList, a.String())
}
a.providerIPs.Store(ocmHost, ipList)
}
for _, ip := range ipList {
if ip == pi.Services[0].Host {
providerAuthorized = true
break
}
}
if !providerAuthorized {
return errtypes.NotFound("OCM Host")
}
return nil
}
func (a *authorizer) ListAllProviders(ctx context.Context) ([]*ocmprovider.ProviderInfo, error) {
return a.providers, nil
}
func (a *authorizer) getOCMProviders(providers []*ocmprovider.ProviderInfo) (po []*ocmprovider.ProviderInfo) {
for _, p := range providers {
_, err := a.getOCMHost(p)
if err == nil {
po = append(po, p)
}
}
return
}
func (a *authorizer) getOCMHost(pi *ocmprovider.ProviderInfo) (string, error) {
for _, s := range pi.Services {
if s.Endpoint.Type.Name == "OCM" {
return s.Host, nil
}
}
return "", errtypes.NotFound("OCM Host")
}
@@ -0,0 +1,27 @@
// Copyright 2018-2023 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 loader
import (
// Load core share manager drivers.
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/provider/authorizer/json"
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/provider/authorizer/mentix"
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/provider/authorizer/open"
// Add your own here.
)
@@ -0,0 +1,267 @@
// Copyright 2018-2023 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 mentix
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider/authorizer/registry"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
"github.com/pkg/errors"
)
func init() {
registry.Register("mentix", New)
}
// Client is a Mentix API client.
type Client struct {
BaseURL string
HTTPClient *http.Client
}
// New returns a new authorizer object.
func New(m map[string]interface{}) (provider.Authorizer, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
client := &Client{
BaseURL: c.URL,
HTTPClient: rhttp.GetHTTPClient(
rhttp.Context(context.Background()),
rhttp.Timeout(time.Duration(c.Timeout*int64(time.Second))),
rhttp.Insecure(c.Insecure),
),
}
return &authorizer{
client: client,
providerIPs: sync.Map{},
conf: &c,
}, nil
}
type config struct {
URL string `mapstructure:"url"`
Timeout int64 `mapstructure:"timeout"`
RefreshInterval int64 `mapstructure:"refresh"`
VerifyRequestHostname bool `mapstructure:"verify_request_hostname"`
Insecure bool `mapstructure:"insecure" docs:"false;Whether to skip certificate checks when sending requests."`
}
func (c *config) ApplyDefaults() {
if c.URL == "" {
c.URL = "http://localhost:9600/mentix/cs3"
}
}
type authorizer struct {
providers []*ocmprovider.ProviderInfo
providersExpiration int64
client *Client
providerIPs sync.Map
conf *config
}
func normalizeDomain(d string) (string, error) {
var urlString string
if strings.Contains(d, "://") {
urlString = d
} else {
urlString = "https://" + d
}
u, err := url.Parse(urlString)
if err != nil {
return "", err
}
return u.Hostname(), nil
}
func (a *authorizer) fetchProviders() ([]*ocmprovider.ProviderInfo, error) {
if (a.providers != nil) && (time.Now().Unix() < a.providersExpiration) {
return a.providers, nil
}
req, err := http.NewRequest(http.MethodGet, a.client.BaseURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json; charset=utf-8")
req.Header.Set("Content-Type", "application/json; charset=utf-8")
res, err := a.client.HTTPClient.Do(req)
if err != nil {
err = errors.Wrap(err,
fmt.Sprintf("mentix: error fetching provider list from: %s", a.client.BaseURL))
return nil, err
}
defer res.Body.Close()
providers := make([]*ocmprovider.ProviderInfo, 0)
if err = json.NewDecoder(res.Body).Decode(&providers); err != nil {
return nil, err
}
a.providers = a.getOCMProviders(providers)
if a.conf.RefreshInterval > 0 {
a.providersExpiration = time.Now().Unix() + a.conf.RefreshInterval
}
return a.providers, nil
}
func (a *authorizer) GetInfoByDomain(ctx context.Context, domain string) (*ocmprovider.ProviderInfo, error) {
normalizedDomain, err := normalizeDomain(domain)
if err != nil {
return nil, err
}
providers, err := a.fetchProviders()
if err != nil {
return nil, err
}
for _, p := range providers {
if strings.Contains(p.Domain, normalizedDomain) {
return p, nil
}
}
return nil, errtypes.NotFound(domain)
}
func (a *authorizer) IsProviderAllowed(ctx context.Context, pi *ocmprovider.ProviderInfo) error {
providers, err := a.fetchProviders()
if err != nil {
return err
}
normalizedDomain, err := normalizeDomain(pi.Domain)
if err != nil {
return err
}
var providerAuthorized bool
if normalizedDomain != "" {
for _, p := range providers {
if p.Domain == normalizedDomain {
providerAuthorized = true
break
}
}
} else {
providerAuthorized = true
}
switch {
case !providerAuthorized:
return errtypes.NotFound(pi.GetDomain())
case !a.conf.VerifyRequestHostname:
return nil
case len(pi.Services) == 0:
return errtypes.NotSupported(
fmt.Sprintf("mentix: provider %s has no supported services", pi.GetDomain()))
}
var ocmHost string
for _, p := range providers {
if p.Domain == normalizedDomain {
ocmHost, err = a.getOCMHost(p)
if err != nil {
return err
}
break
}
}
if ocmHost == "" {
return errtypes.NotSupported(
fmt.Sprintf("mentix: provider %s is missing OCM endpoint", pi.GetDomain()))
}
providerAuthorized = false
var ipList []string
if hostIPs, ok := a.providerIPs.Load(ocmHost); ok {
ipList = hostIPs.([]string)
} else {
addr, err := net.LookupIP(ocmHost)
if err != nil {
return errors.Wrap(err,
fmt.Sprintf("mentix: error looking up IPs for OCM endpoint %s", ocmHost))
}
for _, a := range addr {
ipList = append(ipList, a.String())
}
a.providerIPs.Store(ocmHost, ipList)
}
for _, ip := range ipList {
if ip == pi.Services[0].Host {
providerAuthorized = true
break
}
}
if !providerAuthorized {
return errtypes.BadRequest(
fmt.Sprintf(
"Invalid requesting OCM endpoint IP %s of provider %s",
pi.Services[0].Host, pi.GetDomain()))
}
return nil
}
func (a *authorizer) ListAllProviders(ctx context.Context) ([]*ocmprovider.ProviderInfo, error) {
providers, err := a.fetchProviders()
if err != nil {
return nil, err
}
return providers, nil
}
func (a *authorizer) getOCMProviders(providers []*ocmprovider.ProviderInfo) (po []*ocmprovider.ProviderInfo) {
for _, p := range providers {
_, err := a.getOCMHost(p)
if err == nil {
po = append(po, p)
}
}
return
}
func (a *authorizer) getOCMHost(provider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range provider.Services {
if s.Endpoint.Type.Name == "OCM" {
return s.Host, nil
}
}
return "", errtypes.NotFound("OCM Host")
}
@@ -0,0 +1,110 @@
// Copyright 2018-2023 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 open
import (
"context"
"encoding/json"
"os"
"strings"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider/authorizer/registry"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
)
func init() {
registry.Register("open", New)
}
// New returns a new authorizer object.
func New(m map[string]interface{}) (provider.Authorizer, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
f, err := os.ReadFile(c.Providers)
if err != nil {
return nil, err
}
providers := []*ocmprovider.ProviderInfo{}
err = json.Unmarshal(f, &providers)
if err != nil {
return nil, err
}
a := &authorizer{}
a.providers = a.getOCMProviders(providers)
return a, nil
}
type config struct {
// Users holds a path to a file containing json conforming the Users struct
Providers string `mapstructure:"providers"`
}
func (c *config) ApplyDefaults() {
if c.Providers == "" {
c.Providers = "/etc/revad/ocm-providers.json"
}
}
type authorizer struct {
providers []*ocmprovider.ProviderInfo
}
func (a *authorizer) GetInfoByDomain(ctx context.Context, domain string) (*ocmprovider.ProviderInfo, error) {
for _, p := range a.providers {
if strings.Contains(p.Domain, domain) {
return p, nil
}
}
return nil, errtypes.NotFound(domain)
}
func (a *authorizer) IsProviderAllowed(ctx context.Context, provider *ocmprovider.ProviderInfo) error {
return nil
}
func (a *authorizer) ListAllProviders(ctx context.Context) ([]*ocmprovider.ProviderInfo, error) {
return a.providers, nil
}
func (a *authorizer) getOCMProviders(providers []*ocmprovider.ProviderInfo) (po []*ocmprovider.ProviderInfo) {
for _, p := range providers {
_, err := a.getOCMHost(p)
if err == nil {
po = append(po, p)
}
}
return
}
func (a *authorizer) getOCMHost(provider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range provider.Services {
if s.Endpoint.Type.Name == "OCM" {
return s.Host, nil
}
}
return "", errtypes.NotFound("OCM Host")
}
@@ -0,0 +1,36 @@
// Copyright 2018-2023 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 registry
import (
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider"
)
// NewFunc is the function that provider authorizers
// should register at init time.
type NewFunc func(map[string]interface{}) (provider.Authorizer, error)
// NewFuncs is a map containing all the registered provider authorizers.
var NewFuncs = map[string]NewFunc{}
// Register registers a new provider authorizer's new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
}
@@ -0,0 +1,37 @@
// Copyright 2018-2023 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 provider
import (
"context"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
)
// Authorizer provides provisions to verify and add sync'n'share system providers.
type Authorizer interface {
// GetInfoByDomain returns the information of the provider identified by a specific domain.
GetInfoByDomain(ctx context.Context, domain string) (*ocmprovider.ProviderInfo, error)
// IsProviderAllowed checks if a given system provider is integrated into the OCM or not.
IsProviderAllowed(ctx context.Context, provider *ocmprovider.ProviderInfo) error
// ListAllProviders returns the information of all the providers registered in the mesh.
ListAllProviders(ctx context.Context) ([]*ocmprovider.ProviderInfo, error)
}
@@ -0,0 +1,604 @@
// Copyright 2018-2023 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 json
import (
"context"
"encoding/json"
"io"
"os"
"path/filepath"
"sync"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/google/uuid"
"github.com/pkg/errors"
"google.golang.org/genproto/protobuf/field_mask"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/share"
"github.com/opencloud-eu/reva/v2/pkg/ocm/share/repository/registry"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
)
func init() {
registry.Register("json", New)
}
// New returns a new authorizer object.
func New(m map[string]interface{}) (share.Repository, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
// load or create file
model, err := loadOrCreate(c.File)
if err != nil {
err = errors.Wrap(err, "error loading the file containing the shares")
return nil, err
}
mgr := &mgr{
c: &c,
model: model,
}
return mgr, nil
}
func loadOrCreate(file string) (*shareModel, error) {
_, err := os.Stat(file)
if os.IsNotExist(err) {
if err := os.MkdirAll(filepath.Dir(file), 0700); err != nil {
return nil, errors.Wrap(err, "error creating the base directory: "+filepath.Dir(file))
}
if err := os.WriteFile(file, []byte("{}"), 0700); err != nil {
return nil, errors.Wrap(err, "error creating the file: "+file)
}
}
f, err := os.OpenFile(file, os.O_RDONLY, 0644)
if err != nil {
err = errors.Wrap(err, "error opening the file: "+file)
return nil, err
}
defer f.Close()
var m shareModel
if err := json.NewDecoder(f).Decode(&m); err != nil {
if err != io.EOF {
return nil, errors.Wrap(err, "error decoding data to json")
}
}
if m.Shares == nil {
m.Shares = map[string]*ocm.Share{}
}
if m.ReceivedShares == nil {
m.ReceivedShares = map[string]*ocm.ReceivedShare{}
}
return &m, nil
}
type shareModel struct {
Shares map[string]*ocm.Share `json:"shares"` // share_id -> share
ReceivedShares map[string]*ocm.ReceivedShare `json:"received_shares"` // share_id -> share
}
func (s *shareModel) UnmarshalJSON(d []byte) error {
m := struct {
Shares map[string]json.RawMessage `json:"shares"`
ReceivedShares map[string]json.RawMessage `json:"received_shares"`
}{}
if err := json.Unmarshal(d, &m); err != nil {
return err
}
share := map[string]*ocm.Share{}
for k, v := range m.Shares {
var s ocm.Share
if err := utils.UnmarshalJSONToProtoV1(v, &s); err != nil {
return err
}
share[k] = &s
}
received := map[string]*ocm.ReceivedShare{}
for k, v := range m.ReceivedShares {
var s ocm.ReceivedShare
if err := utils.UnmarshalJSONToProtoV1(v, &s); err != nil {
return err
}
received[k] = &s
}
*s = shareModel{
Shares: share,
ReceivedShares: received,
}
return nil
}
func (s *shareModel) MarshalJSON() ([]byte, error) {
shares := map[string]json.RawMessage{}
for k, v := range s.Shares {
d, err := utils.MarshalProtoV1ToJSON(v)
if err != nil {
return nil, err
}
shares[k] = d
}
received := map[string]json.RawMessage{}
for k, v := range s.ReceivedShares {
d, err := utils.MarshalProtoV1ToJSON(v)
if err != nil {
return nil, err
}
received[k] = d
}
return json.Marshal(map[string]any{
"shares": shares,
"received_shares": received,
})
}
type config struct {
File string `mapstructure:"file"`
}
func (c *config) ApplyDefaults() {
if c.File == "" {
c.File = "/var/tmp/reva/ocm-shares.json"
}
}
type mgr struct {
c *config
sync.Mutex // concurrent access to the file
model *shareModel
}
func (m *mgr) save() error {
f, err := os.OpenFile(m.c.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return errors.Wrap(err, "error opening file "+m.c.File)
}
defer f.Close()
if err := json.NewEncoder(f).Encode(m.model); err != nil {
return errors.Wrap(err, "error encoding to json")
}
return f.Sync()
}
func (m *mgr) load() error {
f, err := os.OpenFile(m.c.File, os.O_RDONLY, 0644)
if err != nil {
return errors.Wrap(err, "error opening file "+m.c.File)
}
defer f.Close()
d, err := io.ReadAll(f)
if err != nil {
return err
}
var model shareModel
if err := json.Unmarshal(d, &model); err != nil {
return err
}
m.model = &model
return nil
}
func genID() string {
return uuid.New().String()
}
func (m *mgr) StoreShare(ctx context.Context, ocmshare *ocm.Share) (*ocm.Share, error) {
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return nil, err
}
if _, err := m.getByKey(ctx, &ocm.ShareKey{
Owner: ocmshare.Owner,
ResourceId: ocmshare.ResourceId,
Grantee: ocmshare.Grantee,
}); err == nil {
return nil, share.ErrShareAlreadyExisting
}
ocmshare.Id = &ocm.ShareId{OpaqueId: genID()}
clone, err := cloneShare(ocmshare)
if err != nil {
return nil, err
}
m.model.Shares[ocmshare.Id.OpaqueId] = clone
if err := m.save(); err != nil {
return nil, errors.Wrap(err, "error saving share")
}
return ocmshare, nil
}
func cloneShare(s *ocm.Share) (*ocm.Share, error) {
d, err := utils.MarshalProtoV1ToJSON(s)
if err != nil {
return nil, errtypes.InternalError("failed to marshal ocm share")
}
var cloned ocm.Share
if err := utils.UnmarshalJSONToProtoV1(d, &cloned); err != nil {
return nil, errtypes.InternalError("failed to unmarshal ocm share")
}
return &cloned, nil
}
func cloneReceivedShare(s *ocm.ReceivedShare) (*ocm.ReceivedShare, error) {
d, err := utils.MarshalProtoV1ToJSON(s)
if err != nil {
return nil, errtypes.InternalError("failed to marshal ocm received share")
}
var cloned ocm.ReceivedShare
if err := utils.UnmarshalJSONToProtoV1(d, &cloned); err != nil {
return nil, errtypes.InternalError("failed to unmarshal ocm received share")
}
return &cloned, nil
}
func (m *mgr) GetShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.Share, error) {
m.Lock()
defer m.Unlock()
var (
s *ocm.Share
err error
)
if err := m.load(); err != nil {
return nil, err
}
switch {
case ref.GetId() != nil:
s, err = m.getByID(ctx, ref.GetId())
case ref.GetKey() != nil:
s, err = m.getByKey(ctx, ref.GetKey())
case ref.GetToken() != "":
return m.getByToken(ctx, ref.GetToken())
default:
err = errtypes.NotFound(ref.String())
}
if err != nil {
return nil, err
}
// check if we are the owner
if utils.UserEqual(user.Id, s.Owner) || utils.UserEqual(user.Id, s.Creator) {
return s, nil
}
return nil, share.ErrShareNotFound
}
func (m *mgr) getByToken(ctx context.Context, token string) (*ocm.Share, error) {
for _, share := range m.model.Shares {
if share.Token == token {
return share, nil
}
}
return nil, errtypes.NotFound(token)
}
func (m *mgr) getByID(ctx context.Context, id *ocm.ShareId) (*ocm.Share, error) {
if share, ok := m.model.Shares[id.OpaqueId]; ok {
return share, nil
}
return nil, errtypes.NotFound(id.String())
}
func (m *mgr) getByKey(ctx context.Context, key *ocm.ShareKey) (*ocm.Share, error) {
for _, share := range m.model.Shares {
if (utils.UserEqual(key.Owner, share.Owner) || utils.UserEqual(key.Owner, share.Creator)) &&
utils.ResourceIDEqual(key.ResourceId, share.ResourceId) && utils.GranteeEqual(key.Grantee, share.Grantee) {
return share, nil
}
}
return nil, share.ErrShareNotFound
}
func (m *mgr) DeleteShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) error {
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return err
}
for id, share := range m.model.Shares {
if sharesEqual(ref, share) {
if utils.UserEqual(user.Id, share.Owner) || utils.UserEqual(user.Id, share.Creator) {
delete(m.model.Shares, id)
return m.save()
}
}
}
return errtypes.NotFound(ref.String())
}
func sharesEqual(ref *ocm.ShareReference, s *ocm.Share) bool {
if ref.GetId() != nil && s.Id != nil {
if ref.GetId().OpaqueId == s.Id.OpaqueId {
return true
}
} else if ref.GetKey() != nil {
if (utils.UserEqual(ref.GetKey().Owner, s.Owner) || utils.UserEqual(ref.GetKey().Owner, s.Creator)) &&
utils.ResourceIDEqual(ref.GetKey().ResourceId, s.ResourceId) && utils.GranteeEqual(ref.GetKey().Grantee, s.Grantee) {
return true
}
}
return false
}
func receivedShareEqual(ref *ocm.ShareReference, s *ocm.ReceivedShare) bool {
if ref.GetId() != nil && s.Id != nil {
if ref.GetId().OpaqueId == s.Id.OpaqueId {
return true
}
}
return false
}
// UpdateShare updates the share with the given fields.
func (m *mgr) UpdateShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference, fields ...*ocm.UpdateOCMShareRequest_UpdateField) (*ocm.Share, error) {
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return nil, err
}
for _, s := range m.model.Shares {
if sharesEqual(ref, s) {
if utils.UserEqual(user.Id, s.Owner) || utils.UserEqual(user.Id, s.Creator) {
for _, f := range fields {
if exp := f.GetExpiration(); exp != nil {
s.Expiration = exp
}
if am := f.GetAccessMethods(); am != nil {
var (
webdavOptions *ocm.WebDAVAccessMethod
webappOptions *ocm.WebappAccessMethod
// TODO: *AccessMethod_GenericOptions
newWebdavOptions *ocm.WebDAVAccessMethod
newWebappOptions *ocm.WebappAccessMethod
// TODO: *AccessMethod_GenericOptions
)
for _, sm := range s.GetAccessMethods() {
webdavOptions = sm.GetWebdavOptions()
webappOptions = sm.GetWebappOptions()
}
newWebdavOptions = am.GetWebdavOptions()
newWebappOptions = am.GetWebappOptions()
newAccesMethods := []*ocm.AccessMethod{}
if newWebdavOptions != nil {
newAccesMethods = append(newAccesMethods, &ocm.AccessMethod{
Term: &ocm.AccessMethod_WebdavOptions{
WebdavOptions: newWebdavOptions,
},
})
} else if webdavOptions != nil {
newAccesMethods = append(newAccesMethods, &ocm.AccessMethod{
Term: &ocm.AccessMethod_WebdavOptions{
WebdavOptions: webdavOptions,
},
})
}
if newWebappOptions != nil {
newAccesMethods = append(newAccesMethods, &ocm.AccessMethod{
Term: &ocm.AccessMethod_WebappOptions{
WebappOptions: newWebappOptions,
},
})
} else if webappOptions != nil {
newAccesMethods = append(newAccesMethods, &ocm.AccessMethod{
Term: &ocm.AccessMethod_WebappOptions{
WebappOptions: webappOptions,
},
})
}
s.AccessMethods = newAccesMethods
}
}
clone, err := cloneShare(s)
if err != nil {
return nil, err
}
m.model.Shares[s.Id.OpaqueId] = clone
if err := m.save(); err != nil {
return nil, errors.Wrap(err, "error saving share")
}
return clone, nil
}
}
}
return nil, errtypes.NotFound(ref.String())
}
func (m *mgr) ListShares(ctx context.Context, user *userpb.User, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error) {
var ss []*ocm.Share
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return nil, err
}
for _, share := range m.model.Shares {
if utils.UserEqual(user.Id, share.Owner) || utils.UserEqual(user.Id, share.Creator) || utils.UserEqual(user.Id, share.GetGrantee().GetUserId()) {
// no filter we return earlier
if len(filters) == 0 {
ss = append(ss, share)
} else {
// check filters
// TODO(labkode): add the rest of filters.
for _, f := range filters {
if f.Type == ocm.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID {
if utils.ResourceIDEqual(share.ResourceId, f.GetResourceId()) {
ss = append(ss, share)
}
}
}
}
}
}
return ss, nil
}
func (m *mgr) StoreReceivedShare(ctx context.Context, share *ocm.ReceivedShare) (*ocm.ReceivedShare, error) {
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return nil, err
}
now := time.Now().UnixNano()
ts := &typespb.Timestamp{
Seconds: uint64(now / 1000000000),
Nanos: uint32(now % 1000000000),
}
share.Id = &ocm.ShareId{
OpaqueId: genID(),
}
share.Ctime = ts
share.Mtime = ts
clone, err := cloneReceivedShare(share)
if err != nil {
return nil, err
}
m.model.ReceivedShares[share.Id.OpaqueId] = clone
if err := m.save(); err != nil {
return nil, err
}
return share, nil
}
func (m *mgr) ListReceivedShares(ctx context.Context, user *userpb.User) ([]*ocm.ReceivedShare, error) {
var rss []*ocm.ReceivedShare
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return nil, err
}
for _, share := range m.model.ReceivedShares {
if utils.UserEqual(user.Id, share.Owner) || utils.UserEqual(user.Id, share.Creator) {
// omit shares created by me
continue
}
if share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && utils.UserEqual(user.Id, share.Grantee.GetUserId()) {
rss = append(rss, share)
}
}
return rss, nil
}
func (m *mgr) GetReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) {
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return nil, err
}
for _, share := range m.model.ReceivedShares {
if receivedShareEqual(ref, share) {
if share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && utils.UserEqual(user.Id, share.Grantee.GetUserId()) {
return share, nil
}
}
}
return nil, errtypes.NotFound(ref.String())
}
func (m *mgr) UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) {
rs, err := m.GetReceivedShare(ctx, user, &ocm.ShareReference{Spec: &ocm.ShareReference_Id{Id: share.Id}})
if err != nil {
return nil, err
}
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return nil, err
}
for _, mask := range fieldMask.Paths {
switch mask {
case "state":
rs.State = share.State
m.model.ReceivedShares[share.Id.OpaqueId].State = share.State
// TODO case "mount_point":
default:
return nil, errtypes.NotSupported("updating " + mask + " is not supported")
}
}
if err := m.save(); err != nil {
return nil, errors.Wrap(err, "error saving model")
}
return rs, nil
}
@@ -0,0 +1,26 @@
// Copyright 2018-2023 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 loader
import (
// Load core share repository drivers.
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/share/repository/json"
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/share/repository/nextcloud"
// Add your own here.
)
@@ -0,0 +1,436 @@
// Copyright 2018-2023 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"
"fmt"
"io"
"net/http"
"strings"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/ocm/share"
"github.com/opencloud-eu/reva/v2/pkg/ocm/share/repository/registry"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
"github.com/pkg/errors"
"google.golang.org/genproto/protobuf/field_mask"
)
func init() {
registry.Register("nextcloud", New)
}
// Manager is the Nextcloud-based implementation of the share.Repository interface
// see https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57
type Manager struct {
client *http.Client
sharedSecret string
webDAVHost string
endPoint string
}
// ShareManagerConfig contains config for a Nextcloud-based ShareManager.
type ShareManagerConfig struct {
EndPoint string `mapstructure:"endpoint" docs:";The Nextcloud backend endpoint for user check"`
SharedSecret string `mapstructure:"shared_secret"`
WebDAVHost string `mapstructure:"webdav_host"`
MockHTTP bool `mapstructure:"mock_http"`
}
// Action describes a REST request to forward to the Nextcloud backend.
type Action struct {
verb string
argS string
}
// GranteeAltMap is an alternative map to JSON-unmarshal a Grantee
// Grantees are hard to unmarshal, so unmarshalling into a map[string]interface{} first,
// see also https://github.com/pondersource/sciencemesh-nextcloud/issues/27
type GranteeAltMap struct {
ID *provider.Grantee_UserId `json:"id"`
}
// ShareAltMap is an alternative map to JSON-unmarshal a Share.
type ShareAltMap struct {
ID *ocm.ShareId `json:"id"`
RemoteShareID string `json:"remote_share_id"`
Permissions *ocm.SharePermissions `json:"permissions"`
Grantee *GranteeAltMap `json:"grantee"`
Owner *userpb.UserId `json:"owner"`
Creator *userpb.UserId `json:"creator"`
Ctime *typespb.Timestamp `json:"ctime"`
Mtime *typespb.Timestamp `json:"mtime"`
}
// ReceivedShareAltMap is an alternative map to JSON-unmarshal a ReceivedShare.
type ReceivedShareAltMap struct {
Share *ShareAltMap `json:"share"`
State ocm.ShareState `json:"state"`
}
// New returns a share manager implementation that verifies against a Nextcloud backend.
func New(m map[string]interface{}) (share.Repository, error) {
var c ShareManagerConfig
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
return NewShareManager(&c)
}
// NewShareManager returns a new Nextcloud-based ShareManager.
func NewShareManager(c *ShareManagerConfig) (*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.ocmshareprovider.drivers.nextcloud]' and '[grpc.services.ocmcore.drivers.nextcloud]'")
}
client = &http.Client{}
}
return &Manager{
endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/"
sharedSecret: c.SharedSecret,
client: client,
webDAVHost: c.WebDAVHost,
}, nil
}
// SetHTTPClient sets the HTTP client.
func (sm *Manager) SetHTTPClient(c *http.Client) {
sm.client = c
}
// StoreShare stores a share.
func (sm *Manager) StoreShare(ctx context.Context, share *ocm.Share) (*ocm.Share, error) {
encShare, err := utils.MarshalProtoV1ToJSON(share)
if err != nil {
return nil, err
}
_, body, err := sm.do(ctx, Action{"addSentShare", string(encShare)}, getUsername(&userpb.User{Id: share.Creator}))
if err != nil {
return nil, err
}
share.Id = &ocm.ShareId{
OpaqueId: string(body),
}
return share, nil
}
// GetShare gets the information for a share by the given ref.
func (sm *Manager) GetShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.Share, error) {
data, err := json.Marshal(ref)
if err != nil {
return nil, err
}
_, body, err := sm.do(ctx, Action{"GetShare", string(data)}, getUsername(user))
if err != nil {
return nil, err
}
altResult := &ShareAltMap{}
if err := json.Unmarshal(body, &altResult); err != nil {
return nil, err
}
return &ocm.Share{
Id: altResult.ID,
Grantee: &provider.Grantee{
Id: altResult.Grantee.ID,
},
Owner: altResult.Owner,
Creator: altResult.Creator,
Ctime: altResult.Ctime,
Mtime: altResult.Mtime,
}, nil
}
// DeleteShare deletes the share pointed by ref.
func (sm *Manager) DeleteShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) error {
bodyStr, err := json.Marshal(ref)
if err != nil {
return err
}
_, _, err = sm.do(ctx, Action{"Unshare", string(bodyStr)}, getUsername(user))
return err
}
// UpdateShare updates the mode of the given share.
func (sm *Manager) UpdateShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference, f ...*ocm.UpdateOCMShareRequest_UpdateField) (*ocm.Share, error) {
type paramsObj struct {
Ref *ocm.ShareReference `json:"ref"`
P *ocm.SharePermissions `json:"p"`
}
bodyObj := &paramsObj{
Ref: ref,
}
data, err := json.Marshal(bodyObj)
if err != nil {
return nil, err
}
_, body, err := sm.do(ctx, Action{"UpdateShare", string(data)}, getUsername(user))
if err != nil {
return nil, err
}
altResult := &ShareAltMap{}
if err := json.Unmarshal(body, &altResult); err != nil {
return nil, err
}
return &ocm.Share{
Id: altResult.ID,
Grantee: &provider.Grantee{
Id: altResult.Grantee.ID,
},
Owner: altResult.Owner,
Creator: altResult.Creator,
Ctime: altResult.Ctime,
Mtime: altResult.Mtime,
}, nil
}
// ListShares returns the shares created by the user. If md is provided is not nil,
// it returns only shares attached to the given resource.
func (sm *Manager) ListShares(ctx context.Context, user *userpb.User, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error) {
data, err := json.Marshal(filters)
if err != nil {
return nil, err
}
_, respBody, err := sm.do(ctx, Action{"ListShares", string(data)}, getUsername(user))
if err != nil {
return nil, err
}
var respArr []ShareAltMap
if err := json.Unmarshal(respBody, &respArr); err != nil {
return nil, err
}
var lst = make([]*ocm.Share, 0, len(respArr))
for _, altResult := range respArr {
lst = append(lst, &ocm.Share{
Id: altResult.ID,
Grantee: &provider.Grantee{
Id: altResult.Grantee.ID,
},
Owner: altResult.Owner,
Creator: altResult.Creator,
Ctime: altResult.Ctime,
Mtime: altResult.Mtime,
})
}
return lst, nil
}
// StoreReceivedShare stores a received share.
func (sm *Manager) StoreReceivedShare(ctx context.Context, share *ocm.ReceivedShare) (*ocm.ReceivedShare, error) {
data, err := utils.MarshalProtoV1ToJSON(share)
if err != nil {
return nil, err
}
_, body, err := sm.do(ctx, Action{"addReceivedShare", string(data)}, getUsername(&userpb.User{Id: share.Grantee.GetUserId()}))
if err != nil {
return nil, err
}
share.Id = &ocm.ShareId{
OpaqueId: string(body),
}
return share, nil
}
// ListReceivedShares returns the list of shares the user has access.
func (sm *Manager) ListReceivedShares(ctx context.Context, user *userpb.User) ([]*ocm.ReceivedShare, error) {
log := appctx.GetLogger(ctx)
_, respBody, err := sm.do(ctx, Action{"ListReceivedShares", ""}, getUsername(user))
if err != nil {
return nil, err
}
var respArr []ReceivedShareAltMap
if err := json.Unmarshal(respBody, &respArr); err != nil {
return nil, err
}
res := make([]*ocm.ReceivedShare, 0, len(respArr))
for _, share := range respArr {
altResultShare := share.Share
log.Info().Msgf("Unpacking share object %+v\n", altResultShare)
if altResultShare == nil {
continue
}
res = append(res, &ocm.ReceivedShare{
Id: altResultShare.ID,
RemoteShareId: altResultShare.RemoteShareID, // sic, see https://github.com/cs3org/reva/pull/3852#discussion_r1189681465
Grantee: &provider.Grantee{
Id: altResultShare.Grantee.ID,
},
Owner: altResultShare.Owner,
Creator: altResultShare.Creator,
Ctime: altResultShare.Ctime,
Mtime: altResultShare.Mtime,
State: share.State,
})
}
return res, nil
}
// GetReceivedShare returns the information for a received share the user has access.
func (sm *Manager) GetReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) {
data, err := json.Marshal(ref)
if err != nil {
return nil, err
}
_, respBody, err := sm.do(ctx, Action{"GetReceivedShare", string(data)}, getUsername(user))
if err != nil {
return nil, err
}
var altResult ReceivedShareAltMap
if err := json.Unmarshal(respBody, &altResult); err != nil {
return nil, err
}
altResultShare := altResult.Share
if altResultShare == nil {
return &ocm.ReceivedShare{
State: altResult.State,
}, nil
}
return &ocm.ReceivedShare{
Id: altResultShare.ID,
RemoteShareId: altResultShare.RemoteShareID, // sic, see https://github.com/cs3org/reva/pull/3852#discussion_r1189681465
Grantee: &provider.Grantee{
Id: altResultShare.Grantee.ID,
},
Owner: altResultShare.Owner,
Creator: altResultShare.Creator,
Ctime: altResultShare.Ctime,
Mtime: altResultShare.Mtime,
State: altResult.State,
}, nil
}
// UpdateReceivedShare updates the received share with share state.
func (sm *Manager) UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) {
type paramsObj struct {
ReceivedShare *ocm.ReceivedShare `json:"received_share"`
FieldMask *field_mask.FieldMask `json:"field_mask"`
}
bodyObj := &paramsObj{
ReceivedShare: share,
FieldMask: fieldMask,
}
bodyStr, err := json.Marshal(bodyObj)
if err != nil {
return nil, err
}
_, respBody, err := sm.do(ctx, Action{"UpdateReceivedShare", string(bodyStr)}, getUsername(user))
if err != nil {
return nil, err
}
var altResult ReceivedShareAltMap
err = json.Unmarshal(respBody, &altResult)
if err != nil {
return nil, err
}
altResultShare := altResult.Share
if altResultShare == nil {
return &ocm.ReceivedShare{
State: altResult.State,
}, nil
}
return &ocm.ReceivedShare{
Id: altResultShare.ID,
RemoteShareId: altResultShare.RemoteShareID, // sic, see https://github.com/cs3org/reva/pull/3852#discussion_r1189681465
Grantee: &provider.Grantee{
Id: altResultShare.Grantee.ID,
},
Owner: altResultShare.Owner,
Creator: altResultShare.Creator,
Ctime: altResultShare.Ctime,
Mtime: altResultShare.Mtime,
State: altResult.State,
}, nil
}
func getUsername(user *userpb.User) string {
if user != nil && len(user.Username) > 0 {
return user.Username
}
if user != nil && len(user.Id.OpaqueId) > 0 {
return user.Id.OpaqueId
}
return "empty-username"
}
func (sm *Manager) do(ctx context.Context, a Action, username string) (int, []byte, error) {
url := sm.endPoint + "~" + username + "/api/ocm/" + a.verb
log := appctx.GetLogger(ctx)
log.Info().Msgf("am.do %s %s", url, a.argS)
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS))
if err != nil {
return 0, nil, err
}
req.Header.Set("X-Reva-Secret", sm.sharedSecret)
req.Header.Set("Content-Type", "application/json")
resp, err := sm.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
}
// curl -i -H 'application/json' -H 'X-Reva-Secret: shared-secret-1' -d '{"md":{"opaque_id":"fileid-/other/q/as"},"g":{"grantee":{"type":1,"Id":{"UserId":{"idp":"revanc2.docker","opaque_id":"marie"}}},"permissions":{"permissions":{"get_path":true,"initiate_file_download":true,"list_container":true,"list_file_versions":true,"stat":true}}},"provider_domain":"cern.ch","resource_type":"file","provider_id":2,"owner_opaque_id":"einstein","owner_display_name":"Albert Einstein","protocol":{"name":"webdav","options":{"sharedSecret":"secret","permissions":"webdav-property"}}}' https://nc1.docker/index.php/apps/sciencemesh/~/api/ocm/addSentShare
log.Info().Msgf("am.do response %d %s", resp.StatusCode, body)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return 0, nil, fmt.Errorf("unexpected response code from EFSS API: %d", resp.StatusCode)
}
return resp.StatusCode, body, nil
}
@@ -0,0 +1,116 @@
// Copyright 2018-2023 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/~tester/api/ocm/addSentShare {"resourceId":{"opaqueId":"fileid-/some/path"},"name":"Some Name","permissions":{"permissions":{"getPath":true}},"grantee":{"userId":{"idp":"0.0.0.0:19000","opaqueId":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":"USER_TYPE_PRIMARY"},"opaque":{"map":{"sharedSecret":{"decoder":"plain","value":"bGdFU1hjR0VtTg=="}}}},"owner":{"idp":"0.0.0.0:19000","opaqueId":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":"USER_TYPE_PRIMARY"},"creator":{"idp":"0.0.0.0:19000","opaqueId":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":"USER_TYPE_PRIMARY"},"shareType":"SHARE_TYPE_REGULAR"}`: {200, `{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/ocm/addReceivedShare {"md":{"opaque_id":"fileid-/some/path"},"g":{"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"permissions":{"permissions":{"get_path":true}}},"provider_domain":"cern.ch","resource_type":"file","provider_id":2,"owner_opaque_id":"einstein","owner_display_name":"Albert Einstein","protocol":{"name":"webdav","options":{"sharedSecret":"secret","permissions":"webdav-property"}}}`: {200, `{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/ocm/GetShare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`: {200, `{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/ocm/Unshare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`: {200, ``, serverStateHome},
`POST /apps/sciencemesh/~tester/api/ocm/UpdateShare {"ref":{"Spec":{"Id":{"opaque_id":"some-share-id"}}},"p":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}}}`: {200, `{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/ocm/ListShares [{"type":4,"Term":{"Creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}}]`: {200, `[{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}]`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/ocm/ListReceivedShares `: {200, `[{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}]`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/ocm/GetReceivedShare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`: {200, `{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}`, serverStateHome},
`POST /apps/sciencemesh/~tester/api/ocm/UpdateReceivedShare {"received_share":{"id":{},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890},"state":2},"field_mask":{"paths":["state"]}}`: {200, `{"share":{"id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}`, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/addReceivedShare {"md":{"opaque_id":"fileid-/some/path"},"g":{"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"permissions":{"permissions":{"get_path":true}}},"provider_domain":"cern.ch","resource_type":"file","provider_id":2,"owner_opaque_id":"einstein","owner_display_name":"Albert Einstein","protocol":{"name":"webdav","options":{"sharedSecret":"secret","permissions":"webdav-property"}}}`: {200, `{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}`, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/GetShare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`: {200, `{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}`, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/Unshare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`: {200, ``, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/UpdateShare {"ref":{"Spec":{"Id":{"opaque_id":"some-share-id"}}},"p":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}}}`: {200, `{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}`, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/ListShares [{"type":4,"Term":{"Creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}}]`: {200, `[{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}]`, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/ListReceivedShares `: {200, `[{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}]`, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/GetReceivedShare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`: {200, `{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}`, serverStateHome},
`POST /index.php/apps/sciencemesh/~marie/api/ocm/UpdateReceivedShare {"received_share":{"share":{"id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2},"field_mask":{"paths":["state"]}}`: {200, `{"share":{"id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}`, 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\n", 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
}
@@ -0,0 +1,36 @@
// Copyright 2018-2023 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 registry
import (
"github.com/opencloud-eu/reva/v2/pkg/ocm/share"
)
// NewFunc is the function that share repositories
// should register at init time.
type NewFunc func(map[string]interface{}) (share.Repository, error)
// NewFuncs is a map containing all the registered share repositories.
var NewFuncs = map[string]NewFunc{}
// Register registers a new share repository new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright 2018-2023 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 share
import (
"context"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"google.golang.org/genproto/protobuf/field_mask"
)
// Repository is the interface that manipulates the OCM shares repository.
type Repository interface {
// StoreShare stores a share.
StoreShare(ctx context.Context, share *ocm.Share) (*ocm.Share, error)
// GetShare gets the information for a share by the given ref.
GetShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.Share, error)
// DeleteShare deletes the share pointed by ref.
DeleteShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) error
// UpdateShare updates the mode of the given share.
UpdateShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference, f ...*ocm.UpdateOCMShareRequest_UpdateField) (*ocm.Share, error)
// ListShares returns the shares created by the user. If md is provided is not nil,
// it returns only shares attached to the given resource.
ListShares(ctx context.Context, user *userpb.User, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error)
// StoreReceivedShare stores a received share.
StoreReceivedShare(ctx context.Context, share *ocm.ReceivedShare) (*ocm.ReceivedShare, error)
// ListReceivedShares returns the list of shares the user has access.
ListReceivedShares(ctx context.Context, user *userpb.User) ([]*ocm.ReceivedShare, error)
// GetReceivedShare returns the information for a received share the user has access.
GetReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.ReceivedShare, error)
// UpdateReceivedShare updates the received share with share state.
UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error)
}
// ResourceIDFilter is an abstraction for creating filter by resource id.
func ResourceIDFilter(id *provider.ResourceId) *ocm.ListOCMSharesRequest_Filter {
return &ocm.ListOCMSharesRequest_Filter{
Type: ocm.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID,
Term: &ocm.ListOCMSharesRequest_Filter_ResourceId{
ResourceId: id,
},
}
}
// ErrShareAlreadyExisting is the error returned when the share already exists
// for the 3-tuple consisting of (owner, resource, grantee).
var ErrShareAlreadyExisting = errtypes.AlreadyExists("share already exists")
// ErrShareNotFound is the error returned where the share does not exist.
var ErrShareNotFound = errtypes.NotFound("share not found")
+72
View File
@@ -0,0 +1,72 @@
// Copyright 2018-2023 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 share
import (
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
// NewWebDAVProtocol is an abstraction for creating a WebDAV protocol.
func NewWebDAVProtocol(uri, shareSecred string, perms *ocm.SharePermissions) *ocm.Protocol {
return &ocm.Protocol{
Term: &ocm.Protocol_WebdavOptions{
WebdavOptions: &ocm.WebDAVProtocol{
Uri: uri,
SharedSecret: shareSecred,
Permissions: perms,
},
},
}
}
// NewWebappProtocol is an abstraction for creating a Webapp protocol.
func NewWebappProtocol(uriTemplate string, viewMode appprovider.ViewMode) *ocm.Protocol {
return &ocm.Protocol{
Term: &ocm.Protocol_WebappOptions{
WebappOptions: &ocm.WebappProtocol{
Uri: uriTemplate,
ViewMode: viewMode,
},
},
}
}
// NewWebDavAccessMethod is an abstraction for creating a WebDAV access method.
func NewWebDavAccessMethod(perms *provider.ResourcePermissions) *ocm.AccessMethod {
return &ocm.AccessMethod{
Term: &ocm.AccessMethod_WebdavOptions{
WebdavOptions: &ocm.WebDAVAccessMethod{
Permissions: perms,
},
},
}
}
// NewWebappAccessMethod is an abstraction for creating a Webapp access method.
func NewWebappAccessMethod(mode appprovider.ViewMode) *ocm.AccessMethod {
return &ocm.AccessMethod{
Term: &ocm.AccessMethod_WebappOptions{
WebappOptions: &ocm.WebappAccessMethod{
ViewMode: mode,
},
},
}
}
@@ -0,0 +1,619 @@
// Copyright 2018-2023 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 ocm
import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/xml"
"io"
"io/fs"
"net/http"
"net/url"
"path/filepath"
"regexp"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocmpb "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typepb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/rs/zerolog"
"github.com/studio-b12/gowebdav"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/mime"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storage"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/registry"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
)
func init() {
registry.Register("ocmreceived", New)
}
type driver struct {
c *config
gateway gateway.GatewayAPIClient
}
type config struct {
GatewaySVC string `mapstructure:"gatewaysvc"`
Insecure bool `mapstructure:"insecure"`
StorageRoot string `mapstructure:"storage_root"`
ServiceAccountID string `mapstructure:"service_account_id"`
ServiceAccountSecret string `mapstructure:"service_account_secret"`
}
func (c *config) ApplyDefaults() {
c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC)
}
// BearerAuthenticator represents an authenticator that adds a Bearer token to the Authorization header of HTTP requests.
type BearerAuthenticator struct {
Token string
}
// Authorize adds the Bearer token to the Authorization header of the provided HTTP request.
func (b BearerAuthenticator) Authorize(_ *http.Client, r *http.Request, _ string) error {
r.Header.Add("Authorization", "Bearer "+b.Token)
return nil
}
// Verify is not implemented for the BearerAuthenticator. It always returns false and nil error.
func (BearerAuthenticator) Verify(*http.Client, *http.Response, string) (bool, error) {
return false, nil
}
// Clone creates a new instance of the BearerAuthenticator.
func (b BearerAuthenticator) Clone() gowebdav.Authenticator {
return BearerAuthenticator{Token: b.Token}
}
// Close is not implemented for the BearerAuthenticator. It always returns nil.
func (BearerAuthenticator) Close() error {
return nil
}
// New creates an OCM storage driver.
func New(m map[string]interface{}, _ events.Stream, _ *zerolog.Logger) (storage.FS, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
gateway, err := pool.GetGatewayServiceClient(c.GatewaySVC)
if err != nil {
return nil, err
}
d := &driver{
c: &c,
gateway: gateway,
}
return d, nil
}
func shareInfoFromPath(path string) (*ocmpb.ShareId, string) {
// the path is of the type /share_id[/rel_path]
shareID, rel := router.ShiftPath(path)
return &ocmpb.ShareId{OpaqueId: shareID}, rel
}
func shareInfoFromReference(ref *provider.Reference) (*ocmpb.ShareId, string) {
if ref.ResourceId == nil {
return shareInfoFromPath(ref.Path)
}
if ref.ResourceId.SpaceId == ref.ResourceId.OpaqueId {
return &ocmpb.ShareId{OpaqueId: ref.ResourceId.SpaceId}, ref.Path
}
decodedBytes, err := base64.StdEncoding.DecodeString(ref.ResourceId.OpaqueId)
if err != nil {
// this should never happen
return &ocmpb.ShareId{OpaqueId: ref.ResourceId.SpaceId}, ref.Path
}
return &ocmpb.ShareId{OpaqueId: ref.ResourceId.SpaceId}, filepath.Join(string(decodedBytes), ref.Path)
}
func (d *driver) getWebDAVFromShare(ctx context.Context, forUser *userpb.UserId, shareID *ocmpb.ShareId) (*ocmpb.ReceivedShare, string, string, error) {
// TODO: we may want to cache the share
req := &ocmpb.GetReceivedOCMShareRequest{
Ref: &ocmpb.ShareReference{
Spec: &ocmpb.ShareReference_Id{
Id: shareID,
},
},
}
if forUser != nil {
req.Opaque = utils.AppendJSONToOpaque(nil, "userid", forUser)
}
res, err := d.gateway.GetReceivedOCMShare(ctx, req)
if err != nil {
return nil, "", "", err
}
if res.Status.Code != rpc.Code_CODE_OK {
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
return nil, "", "", errtypes.NotFound("share not found")
}
return nil, "", "", errtypes.InternalError(res.Status.Message)
}
dav, ok := getWebDAVProtocol(res.Share.Protocols)
if !ok {
return nil, "", "", errtypes.NotFound("share does not contain a WebDAV endpoint")
}
return res.Share, dav.Uri, dav.SharedSecret, nil
}
func getWebDAVProtocol(protocols []*ocmpb.Protocol) (*ocmpb.WebDAVProtocol, bool) {
for _, p := range protocols {
if dav, ok := p.Term.(*ocmpb.Protocol_WebdavOptions); ok {
return dav.WebdavOptions, true
}
}
return nil, false
}
func (d *driver) webdavClient(ctx context.Context, forUser *userpb.UserId, ref *provider.Reference) (*gowebdav.Client, *ocmpb.ReceivedShare, string, error) {
id, rel := shareInfoFromReference(ref)
share, endpoint, secret, err := d.getWebDAVFromShare(ctx, forUser, id)
if err != nil {
return nil, nil, "", err
}
endpoint, err = url.PathUnescape(endpoint)
if err != nil {
return nil, nil, "", err
}
// FIXME: it's still not clear from the OCM APIs how to use the shared secret
// will use as a token in the bearer authentication as this is the reva implementation
c := gowebdav.NewAuthClient(endpoint, gowebdav.NewPreemptiveAuth(BearerAuthenticator{Token: secret}))
if d.c.Insecure {
c.SetTransport(&http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
})
}
return c, share, rel, nil
}
func (d *driver) CreateDir(ctx context.Context, ref *provider.Reference) error {
client, _, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return err
}
return client.MkdirAll(rel, 0)
}
func (d *driver) Delete(ctx context.Context, ref *provider.Reference) error {
client, _, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return err
}
return client.RemoveAll(rel)
}
func (d *driver) TouchFile(ctx context.Context, ref *provider.Reference, markprocessing bool, mtime string) error {
client, _, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return err
}
return client.Write(rel, []byte{}, 0)
}
func (d *driver) Move(ctx context.Context, oldRef, newRef *provider.Reference) error {
client, _, relOld, err := d.webdavClient(ctx, nil, oldRef)
if err != nil {
return err
}
_, relNew := shareInfoFromReference(newRef)
return client.Rename(relOld, relNew, false)
}
func getPathFromShareIDAndRelPath(shareID *ocmpb.ShareId, relPath string) string {
return filepath.Join("/", shareID.OpaqueId, relPath)
}
func convertStatToResourceInfo(ref *provider.Reference, f fs.FileInfo, share *ocmpb.ReceivedShare) (*provider.ResourceInfo, error) {
t := provider.ResourceType_RESOURCE_TYPE_FILE
if f.IsDir() {
t = provider.ResourceType_RESOURCE_TYPE_CONTAINER
}
webdavFile, ok := f.(gowebdav.File)
if !ok {
return nil, errtypes.InternalError("could not get webdav props")
}
var name string
switch {
case share.ResourceType == provider.ResourceType_RESOURCE_TYPE_FILE: //nolint:staticcheck // we will update our ocm implementation later
name = share.Name
case webdavFile.Path() == "/":
name = share.Name
default:
name = webdavFile.Name()
}
opaqueid := base64.StdEncoding.EncodeToString([]byte(webdavFile.Path()))
// ids are of the format <ocmstorageproviderid>$<shareid>!<opaqueid>
id := &provider.ResourceId{
StorageId: utils.OCMStorageProviderID,
SpaceId: share.Id.OpaqueId,
OpaqueId: opaqueid,
}
webdavProtocol, _ := getWebDAVProtocol(share.Protocols)
ri := provider.ResourceInfo{
Type: t,
Id: id,
MimeType: mime.Detect(f.IsDir(), f.Name()),
Path: name,
Name: name,
Size: uint64(f.Size()),
Mtime: &typepb.Timestamp{
Seconds: uint64(f.ModTime().Unix()),
},
Etag: webdavFile.ETag(),
Owner: share.Creator,
PermissionSet: webdavProtocol.Permissions.Permissions,
}
if t == provider.ResourceType_RESOURCE_TYPE_FILE {
// get SHA1 checksum from OpenCloud specific properties if available
propstat := webdavFile.Sys().(gowebdav.Props)
ri.Checksum = extractChecksum(propstat)
}
if f.(gowebdav.File).StatusCode() == 425 {
ri.Opaque = utils.AppendPlainToOpaque(ri.Opaque, "status", "processing")
}
return &ri, nil
}
func extractChecksum(props gowebdav.Props) *provider.ResourceChecksum {
checksums := props.GetString(xml.Name{Space: "http://owncloud.org/ns", Local: "checksums"})
if checksums == "" {
return &provider.ResourceChecksum{
Type: provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_INVALID,
}
}
re := regexp.MustCompile("SHA1:(.*)")
matches := re.FindStringSubmatch(checksums)
if len(matches) == 2 {
return &provider.ResourceChecksum{
Type: provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_SHA1,
Sum: matches[1],
}
}
return &provider.ResourceChecksum{
Type: provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_INVALID,
}
}
func (d *driver) GetMD(ctx context.Context, ref *provider.Reference, _ []string, _ []string) (*provider.ResourceInfo, error) {
client, share, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return nil, err
}
info, err := client.StatWithProps(rel, []string{}) // request all properties by giving an empty list
if err != nil {
if gowebdav.IsErrNotFound(err) {
return nil, errtypes.NotFound(ref.GetPath())
}
return nil, err
}
return convertStatToResourceInfo(ref, info, share)
}
func (d *driver) ListFolder(ctx context.Context, ref *provider.Reference, _ []string, _ []string) ([]*provider.ResourceInfo, error) {
client, share, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return nil, err
}
list, err := client.ReadDirWithProps(rel, []string{}) // request all properties by giving an empty list
if err != nil {
return nil, err
}
res := make([]*provider.ResourceInfo, 0, len(list))
for _, r := range list {
info, err := convertStatToResourceInfo(ref, r, share)
if err != nil {
return nil, err
}
res = append(res, info)
}
return res, nil
}
func (d *driver) Download(ctx context.Context, ref *provider.Reference, openReaderfunc func(*provider.ResourceInfo) bool) (*provider.ResourceInfo, io.ReadCloser, error) {
client, share, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return nil, nil, err
}
info, err := client.StatWithProps(rel, []string{}) // request all properties by giving an empty list
if err != nil {
if gowebdav.IsErrNotFound(err) {
return nil, nil, errtypes.NotFound(ref.GetPath())
}
return nil, nil, err
}
md, err := convertStatToResourceInfo(ref, info, share)
if err != nil {
return nil, nil, err
}
if !openReaderfunc(md) {
return md, nil, nil
}
reader, err := client.ReadStream(rel)
return md, reader, err
}
func (d *driver) GetPathByID(ctx context.Context, id *provider.ResourceId) (string, error) {
shareID, rel := shareInfoFromReference(&provider.Reference{
ResourceId: id,
})
return getPathFromShareIDAndRelPath(shareID, rel), nil
}
func (d *driver) Shutdown(ctx context.Context) error {
return nil
}
func (d *driver) CreateHome(ctx context.Context) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) GetHome(ctx context.Context) (string, error) {
return "", errtypes.NotSupported("operation not supported")
}
func (d *driver) ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) {
return nil, errtypes.NotSupported("operation not supported")
}
func (d *driver) DownloadRevision(ctx context.Context, ref *provider.Reference, key string, openReaderFunc func(md *provider.ResourceInfo) bool) (*provider.ResourceInfo, io.ReadCloser, error) {
return nil, nil, errtypes.NotSupported("operation not supported")
}
func (d *driver) RestoreRevision(ctx context.Context, ref *provider.Reference, key string) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error) {
return nil, errtypes.NotSupported("operation not supported")
}
func (d *driver) RestoreRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string, restoreRef *provider.Reference) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) PurgeRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) EmptyRecycle(ctx context.Context, ref *provider.Reference) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) AddGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) UpdateGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error) {
return nil, errtypes.NotSupported("operation not supported")
}
func (d *driver) GetQuota(ctx context.Context, ref *provider.Reference) ( /*TotalBytes*/ uint64 /*UsedBytes*/, uint64, uint64, error) {
return 0, 0, 0, errtypes.NotSupported("operation not supported")
}
func (d *driver) CreateReference(ctx context.Context, path string, targetURI *url.URL) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error {
return errtypes.NotSupported("operation not supported")
}
// SetLock sets a lock on a file
func (d *driver) SetLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error {
client, _, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return err
}
return client.Lock(rel, lock.GetLockId())
}
func (d *driver) GetLock(ctx context.Context, ref *provider.Reference) (*provider.Lock, error) {
client, _, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return nil, err
}
token, err := client.GetLock(rel)
if err != nil {
return nil, err
}
return &provider.Lock{LockId: token, Type: provider.LockType_LOCK_TYPE_EXCL}, nil
}
func (d *driver) RefreshLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock, existingLockID string) error {
client, _, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return err
}
return client.RefreshLock(rel, lock.GetLockId())
}
// Unlock removes a lock from a file
func (d *driver) Unlock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error {
client, _, rel, err := d.webdavClient(ctx, nil, ref)
if err != nil {
return err
}
return client.Unlock(rel, lock.GetLockId())
}
func (d *driver) ListStorageSpaces(ctx context.Context, filters []*provider.ListStorageSpacesRequest_Filter, _ bool) ([]*provider.StorageSpace, error) {
spaceTypes := map[string]struct{}{}
var exists = struct{}{}
appendTypes := []string{}
for _, f := range filters {
if f.Type == provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE {
spaceType := f.GetSpaceType()
if spaceType == "+mountpoint" {
appendTypes = append(appendTypes, strings.TrimPrefix(spaceType, "+"))
continue
}
spaceTypes[spaceType] = exists
}
}
lrsRes, err := d.gateway.ListReceivedOCMShares(ctx, &ocmpb.ListReceivedOCMSharesRequest{})
if err != nil {
return nil, err
}
// FIXME This might have to be ListOCMShares
// these are grants
lsRes, err := d.gateway.ListOCMShares(ctx, &ocmpb.ListOCMSharesRequest{})
if err != nil {
return nil, err
}
if len(spaceTypes) == 0 {
spaceTypes["mountpoint"] = exists
}
for _, s := range appendTypes {
spaceTypes[s] = exists
}
spaces := []*provider.StorageSpace{}
for k := range spaceTypes {
if k == "mountpoint" {
for _, share := range lrsRes.Shares {
root := &provider.ResourceId{
StorageId: utils.OCMStorageProviderID,
SpaceId: share.Id.OpaqueId,
OpaqueId: share.Id.OpaqueId,
}
space := &provider.StorageSpace{
Id: &provider.StorageSpaceId{
OpaqueId: storagespace.FormatResourceID(root),
},
SpaceType: "mountpoint",
Owner: &userpb.User{
Id: share.Grantee.GetUserId(),
},
Root: root,
}
spaces = append(spaces, space)
}
for _, share := range lsRes.Shares {
root := &provider.ResourceId{
StorageId: utils.OCMStorageProviderID,
SpaceId: share.Id.OpaqueId,
OpaqueId: share.Id.OpaqueId,
}
space := &provider.StorageSpace{
Id: &provider.StorageSpaceId{
OpaqueId: storagespace.FormatResourceID(root),
},
SpaceType: "mountpoint",
Owner: &userpb.User{
Id: share.Grantee.GetUserId(),
},
Root: root,
}
spaces = append(spaces, space)
}
}
}
return spaces, nil
}
func (d *driver) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) {
return nil, errtypes.NotSupported("operation not supported")
}
func (d *driver) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) {
return nil, errtypes.NotSupported("operation not supported")
}
func (d *driver) DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) error {
return errtypes.NotSupported("operation not supported")
}
func (d *driver) AddLabel(ctx context.Context, ref *provider.Reference, userID *userpb.UserId, label string) error {
return errtypes.NotSupported("AddLabel not implemented")
}
func (d *driver) RemoveLabel(ctx context.Context, ref *provider.Reference, userID *userpb.UserId, label string) error {
return errtypes.NotSupported("RemoveLabel not implemented")
}
@@ -0,0 +1,410 @@
// Copyright 2018-2023 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 ocm
import (
"context"
"crypto/md5"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"hash"
"hash/adler32"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/google/uuid"
tusd "github.com/tus/tusd/v2/pkg/handler"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/storage"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
var defaultFilePerm = os.FileMode(0664)
func (d *driver) ListUploadSessions(ctx context.Context, filter storage.UploadSessionFilter) ([]storage.UploadSession, error) {
return []storage.UploadSession{}, nil
}
func (d *driver) InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (map[string]string, error) {
shareID, rel := shareInfoFromReference(ref)
p := getPathFromShareIDAndRelPath(shareID, rel)
info := tusd.FileInfo{
MetaData: tusd.MetaData{
"filename": filepath.Base(p),
"dir": filepath.Dir(p),
},
Size: uploadLength,
}
upload, err := d.NewUpload(ctx, info)
if err != nil {
return nil, err
}
info, _ = upload.GetInfo(ctx)
return map[string]string{
"simple": info.ID,
"tus": info.ID,
}, nil
}
func (d *driver) Upload(ctx context.Context, req storage.UploadRequest, _ storage.UploadFinishedFunc) (*provider.ResourceInfo, error) {
shareID, _ := shareInfoFromReference(req.Ref)
u, err := d.GetUpload(ctx, shareID.OpaqueId)
if err != nil {
return &provider.ResourceInfo{}, err
}
info, err := u.GetInfo(ctx)
if err != nil {
return &provider.ResourceInfo{}, err
}
defer cleanup(&upload{Info: info})
client, _, rel, err := d.webdavClient(ctx, nil, &provider.Reference{
Path: filepath.Join(info.MetaData["dir"], info.MetaData["filename"]),
})
if err != nil {
return &provider.ResourceInfo{}, err
}
client.SetInterceptor(func(method string, rq *http.Request) {
// Set the content length on the request struct directly instead of the header.
// The content-length header gets reset by the golang http library before
// sendind out the request, resulting in chunked encoding to be used which
// breaks the quota checks in ocdav.
if method == "PUT" {
rq.ContentLength = req.Length
}
})
locktoken, _ := ctxpkg.ContextGetLockID(ctx)
return &provider.ResourceInfo{}, client.WriteStream(rel, req.Body, 0, locktoken)
}
// UseIn tells the tus upload middleware which extensions it supports.
func (d *driver) UseIn(composer *tusd.StoreComposer) {
composer.UseCore(d)
composer.UseTerminater(d)
composer.UseConcater(d)
composer.UseLengthDeferrer(d)
}
// AsTerminatableUpload returns a TerminatableUpload
// To implement the termination extension as specified in https://tus.io/protocols/resumable-upload.html#termination
// the storage needs to implement AsTerminatableUpload
func (d *driver) AsTerminatableUpload(up tusd.Upload) tusd.TerminatableUpload {
return up.(*upload)
}
// AsLengthDeclarableUpload returns a LengthDeclarableUpload
// To implement the creation-defer-length extension as specified in https://tus.io/protocols/resumable-upload.html#creation
// the storage needs to implement AsLengthDeclarableUpload
func (d *driver) AsLengthDeclarableUpload(up tusd.Upload) tusd.LengthDeclarableUpload {
return up.(*upload)
}
// AsConcatableUpload returns a ConcatableUpload
// To implement the concatenation extension as specified in https://tus.io/protocols/resumable-upload.html#concatenation
// the storage needs to implement AsConcatableUpload
func (d *driver) AsConcatableUpload(up tusd.Upload) tusd.ConcatableUpload {
return up.(*upload)
}
// To implement the core tus.io protocol as specified in https://tus.io/protocols/resumable-upload.html#core-protocol
// - the storage needs to implement NewUpload and GetUpload
// - the upload needs to implement the tusd.Upload interface: WriteChunk, GetInfo, GetReader and FinishUpload
// NewUpload returns a new tus Upload instance
func (d *driver) NewUpload(ctx context.Context, info tusd.FileInfo) (tusd.Upload, error) {
return NewUpload(ctx, d, d.c.StorageRoot, info)
}
// GetUpload returns the Upload for the given upload id
func (d *driver) GetUpload(ctx context.Context, id string) (tusd.Upload, error) {
return GetUpload(ctx, d, d.c.StorageRoot, id)
}
func NewUpload(ctx context.Context, d *driver, storageRoot string, info tusd.FileInfo) (tusd.Upload, error) {
if info.MetaData["filename"] == "" {
return nil, errors.New("decomposedfs: missing filename in metadata")
}
if info.MetaData["dir"] == "" {
return nil, errors.New("decomposedfs: missing dir in metadata")
}
uploadRoot := filepath.Join(storageRoot, "uploads")
info.ID = uuid.New().String()
user, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("no user in context")
}
info.MetaData["user"] = user.GetId().GetOpaqueId()
info.MetaData["idp"] = user.GetId().GetIdp()
info.Storage = map[string]string{
"Type": "OCM",
"Path": uploadRoot,
}
u := &upload{
Info: info,
Ctx: ctx,
d: d,
}
err := os.MkdirAll(uploadRoot, 0755)
if err != nil {
return nil, err
}
file, err := os.OpenFile(u.BinPath(), os.O_CREATE|os.O_WRONLY, defaultFilePerm)
if err != nil {
return nil, err
}
defer file.Close()
err = u.Persist()
if err != nil {
return nil, err
}
return u, nil
}
func GetUpload(ctx context.Context, d *driver, storageRoot string, id string) (tusd.Upload, error) {
info := tusd.FileInfo{}
data, err := os.ReadFile(filepath.Join(storageRoot, "uploads", id+".info"))
if err != nil {
return nil, err
}
err = json.Unmarshal(data, &info)
if err != nil {
return nil, err
}
upload := &upload{
Info: info,
Ctx: ctx,
d: d,
}
return upload, nil
}
type upload struct {
Info tusd.FileInfo
Ctx context.Context
d *driver
}
func (u *upload) InfoPath() string {
return filepath.Join(u.Info.Storage["Path"], u.Info.ID+".info")
}
func (u *upload) BinPath() string {
return filepath.Join(u.Info.Storage["Path"], u.Info.ID)
}
func (u *upload) Persist() error {
data, err := json.Marshal(u.Info)
if err != nil {
return err
}
return os.WriteFile(u.InfoPath(), data, defaultFilePerm)
}
func (u *upload) WriteChunk(ctx context.Context, offset int64, src io.Reader) (int64, error) {
file, err := os.OpenFile(u.BinPath(), os.O_WRONLY|os.O_APPEND, defaultFilePerm)
if err != nil {
return 0, err
}
defer file.Close()
// calculate cheksum here? needed for the TUS checksum extension. https://tus.io/protocols/resumable-upload.html#checksum
// TODO but how do we get the `Upload-Checksum`? WriteChunk() only has a context, offset and the reader ...
// It is sent with the PATCH request, well or in the POST when the creation-with-upload extension is used
// but the tus handler uses a context.Background() so we cannot really check the header and put it in the context ...
n, err := io.Copy(file, src)
// If the HTTP PATCH request gets interrupted in the middle (e.g. because
// the user wants to pause the upload), Go's net/http returns an io.ErrUnexpectedEOF.
// However, for the ocm driver it's not important whether the stream has ended
// on purpose or accidentally.
if err != nil && err != io.ErrUnexpectedEOF {
return n, err
}
u.Info.Offset += n
return n, u.Persist()
}
func (u *upload) GetInfo(ctx context.Context) (tusd.FileInfo, error) {
return u.Info, nil
}
func (u *upload) GetReader(ctx context.Context) (io.ReadCloser, error) {
return os.Open(u.BinPath())
}
func (u *upload) FinishUpload(ctx context.Context) error {
log := appctx.GetLogger(u.Ctx)
// calculate the checksum of the written bytes
// they will all be written to the metadata later, so we cannot omit any of them
// TODO only calculate the checksum in sync that was requested to match, the rest could be async ... but the tests currently expect all to be present
// TODO the hashes all implement BinaryMarshaler so we could try to persist the state for resumable upload. we would neet do keep track of the copied bytes ...
sha1h := sha1.New()
md5h := md5.New()
adler32h := adler32.New()
{
f, err := os.Open(u.BinPath())
if err != nil {
// we can continue if no oc checksum header is set
log.Info().Err(err).Str("binPath", u.BinPath()).Msg("error opening binPath")
}
defer f.Close()
r1 := io.TeeReader(f, sha1h)
r2 := io.TeeReader(r1, md5h)
_, err = io.Copy(adler32h, r2)
if err != nil {
log.Info().Err(err).Msg("error copying checksums")
}
}
defer cleanup(u)
// compare if they match the sent checksum
// TODO the tus checksum extension would do this on every chunk, but I currently don't see an easy way to pass in the requested checksum. for now we do it in FinishUpload which is also called for chunked uploads
if u.Info.MetaData["checksum"] != "" {
var err error
parts := strings.SplitN(u.Info.MetaData["checksum"], " ", 2)
if len(parts) != 2 {
return errtypes.BadRequest("invalid checksum format. must be '[algorithm] [checksum]'")
}
switch parts[0] {
case "sha1":
err = u.checkHash(parts[1], sha1h)
case "md5":
err = u.checkHash(parts[1], md5h)
case "adler32":
err = u.checkHash(parts[1], adler32h)
default:
err = errtypes.BadRequest("unsupported checksum algorithm: " + parts[0])
}
if err != nil {
return err
}
}
// send to the remote storage via webdav
// shareID, rel := shareInfoFromReference(u.Info.MetaData["ref"])
// p := getPathFromShareIDAndRelPath(shareID, rel)
serviceUserCtx, err := utils.GetServiceUserContextWithContext(context.Background(), u.d.gateway, u.d.c.ServiceAccountID, u.d.c.ServiceAccountSecret)
if err != nil {
return err
}
client, _, rel, err := u.d.webdavClient(serviceUserCtx, &userpb.UserId{
OpaqueId: u.Info.MetaData["user"],
Idp: u.Info.MetaData["idp"],
}, &provider.Reference{
Path: filepath.Join(u.Info.MetaData["dir"], u.Info.MetaData["filename"]),
})
if err != nil {
return err
}
client.SetInterceptor(func(method string, rq *http.Request) {
// Set the content length on the request struct directly instead of the header.
// The content-length header gets reset by the golang http library before
// sendind out the request, resulting in chunked encoding to be used which
// breaks the quota checks in ocdav.
if method == "PUT" {
rq.ContentLength = u.Info.Size
}
})
f, err := os.Open(u.BinPath())
if err != nil {
return err
}
defer f.Close()
return client.WriteStream(rel, f, 0, "")
}
func (u *upload) Terminate(ctx context.Context) error {
cleanup(u)
return nil
}
func (u *upload) ConcatUploads(_ context.Context, uploads []tusd.Upload) error {
file, err := os.OpenFile(u.BinPath(), os.O_WRONLY|os.O_APPEND, defaultFilePerm)
if err != nil {
return err
}
defer file.Close()
for _, partialUpload := range uploads {
fileUpload := partialUpload.(*upload)
src, err := os.Open(fileUpload.BinPath())
if err != nil {
return err
}
defer src.Close()
if _, err := io.Copy(file, src); err != nil {
return err
}
}
return nil
}
func (u *upload) DeclareLength(ctx context.Context, length int64) error {
u.Info.Size = length
u.Info.SizeIsDeferred = false
return nil
}
func (u *upload) checkHash(expected string, h hash.Hash) error {
if expected != hex.EncodeToString(h.Sum(nil)) {
return errtypes.ChecksumMismatch(fmt.Sprintf("invalid checksum: expected %s got %x", u.Info.MetaData["checksum"], h.Sum(nil)))
}
return nil
}
func cleanup(u *upload) {
if u == nil {
return
}
_ = os.Remove(u.BinPath())
_ = os.Remove(u.InfoPath())
}
+58
View File
@@ -0,0 +1,58 @@
package user
import (
"fmt"
"net/url"
"strings"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
)
// LocalUserFederatedID creates a federated id for local users by
// 1. stripping the protocol from the domain and
// 2. if the domain is different from the idp, add the idp to the opaque id
func LocalUserFederatedID(id *userpb.UserId, domain string) *userpb.UserId {
if u, err := url.Parse(domain); err == nil && u.Host != "" {
domain = u.Host
}
u := &userpb.UserId{
Type: userpb.UserType_USER_TYPE_FEDERATED,
Idp: id.GetIdp(),
OpaqueId: id.GetOpaqueId(),
}
if domain != "" && id.GetIdp() != domain {
if id.GetIdp() != "" {
u.OpaqueId = id.GetOpaqueId() + "@" + id.GetIdp()
}
u.Idp = domain
}
return u
}
// DecodeRemoteUserFederatedID decodes opaque id into remote user's federated id by
// splitting the opaque id at the last @ to get the opaque id and the domain
func DecodeRemoteUserFederatedID(id *userpb.UserId) *userpb.UserId {
remoteId := &userpb.UserId{
Type: userpb.UserType_USER_TYPE_PRIMARY,
Idp: id.Idp,
OpaqueId: id.OpaqueId,
}
remote := id.OpaqueId
last := strings.LastIndex(remote, "@")
if last == -1 {
return remoteId
}
remoteId.OpaqueId = remote[:last]
remoteId.Idp = remote[last+1:]
return remoteId
}
// FormatOCMUser formats a user id in the form of <opaque-id>@<idp> used by the OCM API in shareWith, owner and creator fields
func FormatOCMUser(u *userpb.UserId) string {
if u.Idp == "" {
return u.OpaqueId
}
return fmt.Sprintf("%s@%s", u.OpaqueId, u.Idp)
}