Initial QSfera import
This commit is contained in:
+280
@@ -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
@@ -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")
|
||||
Generated
Vendored
+278
@@ -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
|
||||
}
|
||||
Generated
Vendored
+26
@@ -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.
|
||||
)
|
||||
Generated
Vendored
+146
@@ -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
|
||||
}
|
||||
Generated
Vendored
+36
@@ -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
|
||||
}
|
||||
Generated
Vendored
+233
@@ -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")
|
||||
}
|
||||
Generated
Vendored
+27
@@ -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.
|
||||
)
|
||||
Generated
Vendored
+267
@@ -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")
|
||||
}
|
||||
Generated
Vendored
+110
@@ -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")
|
||||
}
|
||||
Generated
Vendored
+36
@@ -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
|
||||
}
|
||||
+37
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+604
@@ -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
|
||||
}
|
||||
Generated
Vendored
+26
@@ -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.
|
||||
)
|
||||
Generated
Vendored
+436
@@ -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 := ¶msObj{
|
||||
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 := ¶msObj{
|
||||
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
|
||||
}
|
||||
Generated
Vendored
+116
@@ -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
|
||||
}
|
||||
Generated
Vendored
+36
@@ -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
@@ -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
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
+619
@@ -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")
|
||||
}
|
||||
+410
@@ -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
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user