github.com/cs3org/reva/v2@v2.27.7/internal/grpc/services/ocminvitemanager/ocminvitemanager.go (about)

     1  // Copyright 2018-2023 CERN
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  package ocminvitemanager
    20  
    21  import (
    22  	"context"
    23  	"time"
    24  
    25  	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
    26  	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    27  	invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
    28  	ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
    29  	rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    30  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    31  	"github.com/cs3org/reva/v2/pkg/errtypes"
    32  	"github.com/cs3org/reva/v2/pkg/ocm/client"
    33  	"github.com/cs3org/reva/v2/pkg/ocm/invite"
    34  	"github.com/cs3org/reva/v2/pkg/ocm/invite/repository/registry"
    35  	ocmuser "github.com/cs3org/reva/v2/pkg/ocm/user"
    36  	"github.com/cs3org/reva/v2/pkg/rgrpc"
    37  	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
    38  	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
    39  	"github.com/cs3org/reva/v2/pkg/sharedconf"
    40  	"github.com/cs3org/reva/v2/pkg/utils"
    41  	"github.com/cs3org/reva/v2/pkg/utils/cfg"
    42  	"github.com/pkg/errors"
    43  	"github.com/rs/zerolog"
    44  	"google.golang.org/grpc"
    45  )
    46  
    47  func init() {
    48  	rgrpc.Register("ocminvitemanager", New)
    49  }
    50  
    51  type config struct {
    52  	Driver            string                            `mapstructure:"driver"`
    53  	Drivers           map[string]map[string]interface{} `mapstructure:"drivers"`
    54  	TokenExpiration   string                            `mapstructure:"token_expiration"`
    55  	OCMClientTimeout  int                               `mapstructure:"ocm_timeout"`
    56  	OCMClientInsecure bool                              `mapstructure:"ocm_insecure"`
    57  	GatewaySVC        string                            `mapstructure:"gatewaysvc"       validate:"required"`
    58  	ProviderDomain    string                            `mapstructure:"provider_domain"  validate:"required" docs:"The same domain registered in the provider authorizer"`
    59  
    60  	tokenExpiration time.Duration
    61  }
    62  
    63  type service struct {
    64  	conf            *config
    65  	repo            invite.Repository
    66  	ocmClient       *client.OCMClient
    67  	gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
    68  }
    69  
    70  func (c *config) ApplyDefaults() {
    71  	if c.Driver == "" {
    72  		c.Driver = "json"
    73  	}
    74  	if c.TokenExpiration == "" {
    75  		c.TokenExpiration = "24h"
    76  	}
    77  
    78  	c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC)
    79  }
    80  
    81  func (s *service) Register(ss *grpc.Server) {
    82  	invitepb.RegisterInviteAPIServer(ss, s)
    83  }
    84  
    85  func getInviteRepository(c *config) (invite.Repository, error) {
    86  	if f, ok := registry.NewFuncs[c.Driver]; ok {
    87  		return f(c.Drivers[c.Driver])
    88  	}
    89  	return nil, errtypes.NotFound("driver not found: " + c.Driver)
    90  }
    91  
    92  // New creates a new OCM invite manager svc.
    93  func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
    94  	var c config
    95  	if err := cfg.Decode(m, &c); err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	p, err := time.ParseDuration(c.TokenExpiration)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	c.tokenExpiration = p
   104  
   105  	repo, err := getInviteRepository(&c)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  
   110  	gatewaySelector, err := pool.GatewaySelector(c.GatewaySVC)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	service := &service{
   116  		conf: &c,
   117  		repo: repo,
   118  		ocmClient: client.New(&client.Config{
   119  			Timeout:  time.Duration(c.OCMClientTimeout) * time.Second,
   120  			Insecure: c.OCMClientInsecure,
   121  		}),
   122  		gatewaySelector: gatewaySelector,
   123  	}
   124  	return service, nil
   125  }
   126  
   127  func (s *service) Close() error {
   128  	return nil
   129  }
   130  
   131  func (s *service) UnprotectedEndpoints() []string {
   132  	return []string{"/cs3.ocm.invite.v1beta1.InviteAPI/AcceptInvite", "/cs3.ocm.invite.v1beta1.InviteAPI/GetAcceptedUser"}
   133  }
   134  
   135  func (s *service) GenerateInviteToken(ctx context.Context, req *invitepb.GenerateInviteTokenRequest) (*invitepb.GenerateInviteTokenResponse, error) {
   136  	user := ctxpkg.ContextMustGetUser(ctx)
   137  	token := CreateToken(s.conf.tokenExpiration, user.GetId(), req.Description)
   138  
   139  	if err := s.repo.AddToken(ctx, token); err != nil {
   140  		return &invitepb.GenerateInviteTokenResponse{
   141  			Status: status.NewInternal(ctx, "error generating invite token"),
   142  		}, nil
   143  	}
   144  
   145  	return &invitepb.GenerateInviteTokenResponse{
   146  		Status:      status.NewOK(ctx),
   147  		InviteToken: token,
   148  	}, nil
   149  }
   150  
   151  func (s *service) ListInviteTokens(ctx context.Context, req *invitepb.ListInviteTokensRequest) (*invitepb.ListInviteTokensResponse, error) {
   152  	user := ctxpkg.ContextMustGetUser(ctx)
   153  	tokens, err := s.repo.ListTokens(ctx, user.Id)
   154  	if err != nil {
   155  		return &invitepb.ListInviteTokensResponse{
   156  			Status: status.NewInternal(ctx, "error listing tokens"),
   157  		}, nil
   158  	}
   159  	return &invitepb.ListInviteTokensResponse{
   160  		Status:       status.NewOK(ctx),
   161  		InviteTokens: tokens,
   162  	}, nil
   163  }
   164  
   165  func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInviteRequest) (*invitepb.ForwardInviteResponse, error) {
   166  	user := ctxpkg.ContextMustGetUser(ctx)
   167  
   168  	ocmEndpoint, err := getOCMEndpoint(req.GetOriginSystemProvider())
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	if req.GetOriginSystemProvider().Domain == s.conf.ProviderDomain {
   174  		return &invitepb.ForwardInviteResponse{
   175  			Status: status.NewInvalid(ctx, "can not accept an invite from the same instance"),
   176  		}, nil
   177  	}
   178  
   179  	// Accept the invitation on the remote OCM provider
   180  	remoteUser, err := s.ocmClient.InviteAccepted(ctx, ocmEndpoint, &client.InviteAcceptedRequest{
   181  		Token:             req.InviteToken.GetToken(),
   182  		RecipientProvider: s.conf.ProviderDomain,
   183  		// The UserID is only a string here. To not loose the IDP information we use the FederatedID encoding
   184  		// i.e. base64(UserID@IDP)
   185  		UserID: ocmuser.FederatedID(user.GetId(), "").GetOpaqueId(),
   186  		Email:  user.GetMail(),
   187  		Name:   user.GetDisplayName(),
   188  	})
   189  	if err != nil {
   190  		switch {
   191  		case errors.Is(err, client.ErrTokenInvalid):
   192  			return &invitepb.ForwardInviteResponse{
   193  				Status: status.NewInvalid(ctx, "token not valid"),
   194  			}, nil
   195  		case errors.Is(err, client.ErrTokenNotFound):
   196  			return &invitepb.ForwardInviteResponse{
   197  				Status: status.NewNotFound(ctx, "token not found"),
   198  			}, nil
   199  		case errors.Is(err, client.ErrUserAlreadyAccepted):
   200  			return &invitepb.ForwardInviteResponse{
   201  				Status: status.NewAlreadyExists(ctx, err, err.Error()),
   202  			}, nil
   203  		case errors.Is(err, client.ErrServiceNotTrusted):
   204  			return &invitepb.ForwardInviteResponse{
   205  				Status: status.NewPermissionDenied(ctx, err, err.Error()),
   206  			}, nil
   207  		default:
   208  			return &invitepb.ForwardInviteResponse{
   209  				Status: status.NewInternal(ctx, err.Error()),
   210  			}, nil
   211  		}
   212  	}
   213  
   214  	// create a link between the user that accepted the share (in ctx)
   215  	// and the remote one (the initiator), so at the end of the invitation workflow they
   216  	// know each other
   217  
   218  	// remoteUser.UserID is the federated ID (just a string), to get a unique CS3 userid
   219  	// we're using the provider domain as the IDP part of the ID
   220  	remoteUserID := &userpb.UserId{
   221  		Type:     userpb.UserType_USER_TYPE_FEDERATED,
   222  		Idp:      req.GetOriginSystemProvider().Domain,
   223  		OpaqueId: remoteUser.UserID,
   224  	}
   225  
   226  	if err := s.repo.AddRemoteUser(ctx, user.Id, &userpb.User{
   227  		Id:          remoteUserID,
   228  		Mail:        remoteUser.Email,
   229  		DisplayName: remoteUser.Name,
   230  	}); err != nil {
   231  		if errors.Is(err, invite.ErrUserAlreadyAccepted) {
   232  			return &invitepb.ForwardInviteResponse{
   233  				Status: status.NewAlreadyExists(ctx, err, err.Error()),
   234  			}, nil
   235  		}
   236  		return &invitepb.ForwardInviteResponse{
   237  			Status: status.NewInternal(ctx, err.Error()),
   238  		}, nil
   239  	}
   240  
   241  	return &invitepb.ForwardInviteResponse{
   242  		Status:      status.NewOK(ctx),
   243  		UserId:      remoteUserID,
   244  		Email:       remoteUser.Email,
   245  		DisplayName: remoteUser.Name,
   246  	}, nil
   247  }
   248  
   249  func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) {
   250  	for _, s := range originProvider.Services {
   251  		if s.Endpoint.Type.Name == "OCM" {
   252  			return s.Endpoint.Path, nil
   253  		}
   254  	}
   255  	return "", errors.New("ocm endpoint not specified for mesh provider")
   256  }
   257  
   258  func (s *service) AcceptInvite(ctx context.Context, req *invitepb.AcceptInviteRequest) (*invitepb.AcceptInviteResponse, error) {
   259  	token, err := s.repo.GetToken(ctx, req.InviteToken.Token)
   260  	if err != nil {
   261  		if errors.Is(err, invite.ErrTokenNotFound) {
   262  			return &invitepb.AcceptInviteResponse{
   263  				Status: status.NewNotFound(ctx, "token not found"),
   264  			}, nil
   265  		}
   266  		return &invitepb.AcceptInviteResponse{
   267  			Status: status.NewInternal(ctx, err.Error()),
   268  		}, nil
   269  	}
   270  
   271  	if !isTokenValid(token) {
   272  		return &invitepb.AcceptInviteResponse{
   273  			Status: status.NewInvalid(ctx, "token is not valid"),
   274  		}, nil
   275  	}
   276  
   277  	initiator, err := s.getUserInfo(ctx, token.UserId)
   278  	if err != nil {
   279  		return &invitepb.AcceptInviteResponse{
   280  			Status: status.NewInternal(ctx, err.Error()),
   281  		}, nil
   282  	}
   283  
   284  	remoteUser := req.GetRemoteUser()
   285  
   286  	if err := s.repo.AddRemoteUser(ctx, token.GetUserId(), remoteUser); err != nil {
   287  		if !errors.Is(err, invite.ErrUserAlreadyAccepted) {
   288  			// skip error if user was already accepted
   289  			return &invitepb.AcceptInviteResponse{
   290  				Status: status.NewInternal(ctx, err.Error()),
   291  			}, nil
   292  		}
   293  	}
   294  
   295  	return &invitepb.AcceptInviteResponse{
   296  		Status:      status.NewOK(ctx),
   297  		UserId:      initiator.GetId(),
   298  		Email:       initiator.Mail,
   299  		DisplayName: initiator.DisplayName,
   300  	}, nil
   301  }
   302  
   303  func (s *service) getUserInfo(ctx context.Context, id *userpb.UserId) (*userpb.User, error) {
   304  	gw, err := s.gatewaySelector.Next()
   305  	if err != nil {
   306  		return nil, nil
   307  	}
   308  	res, err := gw.GetUser(ctx, &userpb.GetUserRequest{
   309  		UserId: id,
   310  	})
   311  	if err != nil {
   312  		return nil, err
   313  	}
   314  	if res.Status.Code != rpcv1beta1.Code_CODE_OK {
   315  		return nil, errors.New(res.Status.Message)
   316  	}
   317  
   318  	return res.User, nil
   319  }
   320  
   321  func isTokenValid(token *invitepb.InviteToken) bool {
   322  	return time.Now().Unix() < int64(token.Expiration.Seconds)
   323  }
   324  
   325  func (s *service) GetAcceptedUser(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*invitepb.GetAcceptedUserResponse, error) {
   326  	user, ok := getUserFilter(ctx, req)
   327  	if !ok {
   328  		return &invitepb.GetAcceptedUserResponse{
   329  			Status: status.NewInvalidArg(ctx, "user not found"),
   330  		}, nil
   331  	}
   332  	remoteUser, err := s.repo.GetRemoteUser(ctx, user.GetId(), req.GetRemoteUserId())
   333  	if err != nil {
   334  		return &invitepb.GetAcceptedUserResponse{
   335  			Status: status.NewInternal(ctx, "error fetching remote user details"),
   336  		}, nil
   337  	}
   338  
   339  	return &invitepb.GetAcceptedUserResponse{
   340  		Status:     status.NewOK(ctx),
   341  		RemoteUser: remoteUser,
   342  	}, nil
   343  }
   344  
   345  func getUserFilter(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*userpb.User, bool) {
   346  	user, ok := ctxpkg.ContextGetUser(ctx)
   347  	if ok {
   348  		return user, true
   349  	}
   350  
   351  	if req.Opaque == nil || req.Opaque.Map == nil {
   352  		return nil, false
   353  	}
   354  
   355  	v, ok := req.Opaque.Map["user-filter"]
   356  	if !ok {
   357  		return nil, false
   358  	}
   359  
   360  	var u userpb.UserId
   361  	if err := utils.UnmarshalJSONToProtoV1(v.Value, &u); err != nil {
   362  		return nil, false
   363  	}
   364  	return &userpb.User{Id: &u}, true
   365  }
   366  
   367  func (s *service) FindAcceptedUsers(ctx context.Context, req *invitepb.FindAcceptedUsersRequest) (*invitepb.FindAcceptedUsersResponse, error) {
   368  	user := ctxpkg.ContextMustGetUser(ctx)
   369  	acceptedUsers, err := s.repo.FindRemoteUsers(ctx, user.GetId(), req.GetFilter())
   370  	if err != nil {
   371  		return &invitepb.FindAcceptedUsersResponse{
   372  			Status: status.NewInternal(ctx, "error finding remote users: "+err.Error()),
   373  		}, nil
   374  	}
   375  
   376  	return &invitepb.FindAcceptedUsersResponse{
   377  		Status:        status.NewOK(ctx),
   378  		AcceptedUsers: acceptedUsers,
   379  	}, nil
   380  }
   381  
   382  func (s *service) DeleteAcceptedUser(ctx context.Context, req *invitepb.DeleteAcceptedUserRequest) (*invitepb.DeleteAcceptedUserResponse, error) {
   383  	user := ctxpkg.ContextMustGetUser(ctx)
   384  	if err := s.repo.DeleteRemoteUser(ctx, user.Id, req.RemoteUserId); err != nil {
   385  		return &invitepb.DeleteAcceptedUserResponse{
   386  			Status: status.NewInternal(ctx, "error deleting remote users: "+err.Error()),
   387  		}, nil
   388  	}
   389  
   390  	return &invitepb.DeleteAcceptedUserResponse{
   391  		Status: status.NewOK(ctx),
   392  	}, nil
   393  }