Skip to content

Commit

Permalink
Revamp OCM invitation workflow (#3611)
Browse files Browse the repository at this point in the history
* Refactored OCM Invite manager
* Updated /invite-accepted endpoint to the last OCM APIs spec (OCM-API#54)
   * Introduced mutual exchange of user information on invitation accept
* Add errors as specified by the OCM and cs3apis specs (OCM-API#58, cs3apis#198)
* Allow user to specify a description when generating an invitation token
* Split old ocmd http service in two services (OCM and sciencemesh)
* OCM: required changes to comply with the invitation workflow #3540
    * ocm services contain only the implementation of the OCM APIs (https://cs3org.github.io/OCM-API/docs.html)
    * sciencemesh is meant for reva clients (like web) for interacting with tokens and shares in OCM
* Written integration tests for the invitation workflow (both grpc and http)
  • Loading branch information
gmgigi96 authored Jan 24, 2023
1 parent 019b352 commit 14c9c5f
Show file tree
Hide file tree
Showing 38 changed files with 1,957 additions and 1,191 deletions.
4 changes: 4 additions & 0 deletions changelog/unreleased/revamp-ocm-invitation-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Enhancement: Revamp OCM invitation workflow

https://github.com/cs3org/reva/pull/3611
https://github.com/cs3org/reva/issues/3540
2 changes: 1 addition & 1 deletion cmd/reva/ocm-invite-forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func ocmInviteForwardCommand() *command {
if forwardToken.Status.Code != rpc.Code_CODE_OK {
return formatError(forwardToken.Status)
}
fmt.Println("OK")
fmt.Println(forwardToken)
return nil
}
return cmd
Expand Down
2 changes: 1 addition & 1 deletion cmd/revad/runtime/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import (
_ "github.com/cs3org/reva/pkg/datatx/manager/loader"
_ "github.com/cs3org/reva/pkg/group/manager/loader"
_ "github.com/cs3org/reva/pkg/metrics/driver/loader"
_ "github.com/cs3org/reva/pkg/ocm/invite/manager/loader"
_ "github.com/cs3org/reva/pkg/ocm/invite/repository/loader"
_ "github.com/cs3org/reva/pkg/ocm/provider/authorizer/loader"
_ "github.com/cs3org/reva/pkg/ocm/share/manager/loader"
_ "github.com/cs3org/reva/pkg/permission/manager/loader"
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/cheggaaa/pb v1.0.29
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e
github.com/cs3org/go-cs3apis v0.0.0-20221004162747-f20ee4756d90
github.com/cs3org/go-cs3apis v0.0.0-20230124153659-5dc78d32c9b7
github.com/dgraph-io/ristretto v0.1.1
github.com/eventials/go-tus v0.0.0-20200718001131-45c7ec8f5d59
github.com/gdexlab/go-render v1.0.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJffz4pz0o1WuQxJ28+5x5JgaHD8=
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4=
github.com/cs3org/go-cs3apis v0.0.0-20221004162747-f20ee4756d90 h1:zYg2UzwpChLgXktwt7MJEMv46GQPtluifRnynkSw80Y=
github.com/cs3org/go-cs3apis v0.0.0-20221004162747-f20ee4756d90/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/cs3org/go-cs3apis v0.0.0-20230124153659-5dc78d32c9b7 h1:QShkOi9aBptnhYN4W0lueiWTlNtc7O69D6GRpYfZodg=
github.com/cs3org/go-cs3apis v0.0.0-20230124153659-5dc78d32c9b7/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down
203 changes: 181 additions & 22 deletions internal/grpc/services/ocminvitemanager/ocminvitemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,21 @@ package ocminvitemanager

import (
"context"
"time"

userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ctxpkg "github.com/cs3org/reva/pkg/ctx"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/ocm/client"
"github.com/cs3org/reva/pkg/ocm/invite"
"github.com/cs3org/reva/pkg/ocm/invite/manager/registry"
"github.com/cs3org/reva/pkg/ocm/invite/repository/registry"
"github.com/cs3org/reva/pkg/rgrpc"
"github.com/cs3org/reva/pkg/rgrpc/status"
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/pkg/sharedconf"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"google.golang.org/grpc"
Expand All @@ -37,26 +45,46 @@ func init() {
}

type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
TokenExpiration string `mapstructure:"token_expiration"`
OCMClientTimeout int `mapstructure:"ocm_timeout"`
OCMClientInsecure bool `mapstructure:"ocm_insecure"`
GatewaySVC string `mapstructure:"gateway_svc"`

tokenExpiration time.Duration
}

type service struct {
conf *config
im invite.Manager
conf *config
repo invite.Repository
ocmClient *client.OCMClient
}

func (c *config) init() {
func (c *config) init() error {
if c.Driver == "" {
c.Driver = "json"
}
if c.TokenExpiration == "" {
c.TokenExpiration = "24h"
}

p, err := time.ParseDuration(c.TokenExpiration)
if err != nil {
return err
}
c.tokenExpiration = p

c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC)

return nil
}

func (s *service) Register(ss *grpc.Server) {
invitepb.RegisterInviteAPIServer(ss, s)
}

func getInviteManager(c *config) (invite.Manager, error) {
func getInviteRepository(c *config) (invite.Repository, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
Expand All @@ -78,16 +106,22 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) {
if err != nil {
return nil, err
}
c.init()
if err := c.init(); err != nil {
return nil, err
}

im, err := getInviteManager(c)
repo, err := getInviteRepository(c)
if err != nil {
return nil, err
}

service := &service{
conf: c,
im: im,
repo: repo,
ocmClient: client.New(&client.Config{
Timeout: time.Duration(c.OCMClientTimeout) * time.Second,
Insecure: c.OCMClientInsecure,
}),
}
return service, nil
}
Expand All @@ -101,8 +135,10 @@ func (s *service) UnprotectedEndpoints() []string {
}

func (s *service) GenerateInviteToken(ctx context.Context, req *invitepb.GenerateInviteTokenRequest) (*invitepb.GenerateInviteTokenResponse, error) {
token, err := s.im.GenerateToken(ctx)
if err != nil {
user := ctxpkg.ContextMustGetUser(ctx)
token := CreateToken(s.conf.tokenExpiration, user.GetId(), req.Description)

if err := s.repo.AddToken(ctx, token); err != nil {
return &invitepb.GenerateInviteTokenResponse{
Status: status.NewInternal(ctx, err, "error generating invite token"),
}, nil
Expand All @@ -115,33 +151,155 @@ func (s *service) GenerateInviteToken(ctx context.Context, req *invitepb.Generat
}

func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInviteRequest) (*invitepb.ForwardInviteResponse, error) {
err := s.im.ForwardInvite(ctx, req.InviteToken, req.OriginSystemProvider)
user := ctxpkg.ContextMustGetUser(ctx)

ocmEndpoint, err := getOCMEndpoint(req.GetOriginSystemProvider())
if err != nil {
return &invitepb.ForwardInviteResponse{
Status: status.NewInternal(ctx, err, "error forwarding invite:"+err.Error()),
}, nil
return nil, err
}

remoteUser, err := s.ocmClient.InviteAccepted(ctx, ocmEndpoint, &client.InviteAcceptedRequest{
Token: req.InviteToken.GetToken(),
RecipientProvider: user.GetId().GetIdp(),
UserID: user.GetId().GetOpaqueId(),
Email: user.GetMail(),
Name: user.GetDisplayName(),
})
if err != nil {
switch {
case errors.Is(err, client.ErrTokenInvalid):
return &invitepb.ForwardInviteResponse{
Status: status.NewInvalid(ctx, "token not valid"),
}, nil
case errors.Is(err, client.ErrTokenNotFound):
return &invitepb.ForwardInviteResponse{
Status: status.NewNotFound(ctx, "token not found"),
}, nil
case errors.Is(err, client.ErrUserAlreadyAccepted):
return &invitepb.ForwardInviteResponse{
Status: status.NewAlreadyExists(ctx, err, err.Error()),
}, nil
case errors.Is(err, client.ErrServiceNotTrusted):
return &invitepb.ForwardInviteResponse{
Status: status.NewPermissionDenied(ctx, err, err.Error()),
}, nil
default:
return &invitepb.ForwardInviteResponse{
Status: status.NewInternal(ctx, err, err.Error()),
}, nil
}
}

// create a link between the user that accepted the share (in ctx)
// and the remote one (the initiator), so at the end of the invitation workflow they
// know each other

remoteUserID := &userpb.UserId{
Type: userpb.UserType_USER_TYPE_PRIMARY,
Idp: req.GetOriginSystemProvider().Domain,
OpaqueId: remoteUser.UserID,
}

if err := s.repo.AddRemoteUser(ctx, user.Id, &userpb.User{
Id: remoteUserID,
Mail: remoteUser.Email,
DisplayName: remoteUser.Name,
}); err != nil {
if !errors.Is(err, invite.ErrUserAlreadyAccepted) {
// skip error if user was already accepted
return &invitepb.ForwardInviteResponse{
Status: status.NewInternal(ctx, err, err.Error()),
}, nil
}
}

return &invitepb.ForwardInviteResponse{
Status: status.NewOK(ctx),
Status: status.NewOK(ctx),
UserId: remoteUserID,
Email: remoteUser.Email,
DisplayName: remoteUser.Name,
}, nil
}

func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range originProvider.Services {
if s.Endpoint.Type.Name == "OCM" {
return s.Endpoint.Path, nil
}
}
return "", errors.New("ocm endpoint not specified for mesh provider")
}

func (s *service) AcceptInvite(ctx context.Context, req *invitepb.AcceptInviteRequest) (*invitepb.AcceptInviteResponse, error) {
err := s.im.AcceptInvite(ctx, req.InviteToken, req.RemoteUser)
token, err := s.repo.GetToken(ctx, req.InviteToken.Token)
if err != nil {
if errors.Is(err, invite.ErrTokenNotFound) {
return &invitepb.AcceptInviteResponse{
Status: status.NewNotFound(ctx, "token not found"),
}, nil
}
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, err, "error accepting invite"),
Status: status.NewInternal(ctx, err, err.Error()),
}, nil
}

if !isTokenValid(token) {
return &invitepb.AcceptInviteResponse{
Status: status.NewInvalid(ctx, "token is not valid"),
}, nil
}

initiator, err := s.getUserInfo(ctx, token.UserId)
if err != nil {
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, err, err.Error()),
}, nil
}

if err := s.repo.AddRemoteUser(ctx, token.GetUserId(), req.GetRemoteUser()); err != nil {
if errors.Is(err, invite.ErrUserAlreadyAccepted) {
return &invitepb.AcceptInviteResponse{
Status: status.NewAlreadyExists(ctx, err, err.Error()),
}, nil
}
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, err, err.Error()),
}, nil
}

return &invitepb.AcceptInviteResponse{
Status: status.NewOK(ctx),
Status: status.NewOK(ctx),
UserId: initiator.GetId(),
Email: initiator.Mail,
DisplayName: initiator.DisplayName,
}, nil
}

func (s *service) getUserInfo(ctx context.Context, id *userpb.UserId) (*userpb.User, error) {
gw, err := pool.GetGatewayServiceClient(pool.Endpoint(s.conf.GatewaySVC))
if err != nil {
return nil, err
}
res, err := gw.GetUser(ctx, &userpb.GetUserRequest{
UserId: id,
})
if err != nil {
return nil, err
}
if res.Status.Code != rpcv1beta1.Code_CODE_OK {
return nil, errors.New(res.Status.Message)
}

return res.User, nil
}

func isTokenValid(token *invitepb.InviteToken) bool {
return time.Now().Unix() < int64(token.Expiration.Seconds)
}

func (s *service) GetAcceptedUser(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*invitepb.GetAcceptedUserResponse, error) {
remoteUser, err := s.im.GetAcceptedUser(ctx, req.RemoteUserId)
user := ctxpkg.ContextMustGetUser(ctx)
remoteUser, err := s.repo.GetRemoteUser(ctx, user.GetId(), req.GetRemoteUserId())
if err != nil {
return &invitepb.GetAcceptedUserResponse{
Status: status.NewInternal(ctx, err, "error fetching remote user details"),
Expand All @@ -155,7 +313,8 @@ func (s *service) GetAcceptedUser(ctx context.Context, req *invitepb.GetAccepted
}

func (s *service) FindAcceptedUsers(ctx context.Context, req *invitepb.FindAcceptedUsersRequest) (*invitepb.FindAcceptedUsersResponse, error) {
acceptedUsers, err := s.im.FindAcceptedUsers(ctx, req.Filter)
user := ctxpkg.ContextMustGetUser(ctx)
acceptedUsers, err := s.repo.FindRemoteUsers(ctx, user.GetId(), req.GetFilter())
if err != nil {
return &invitepb.FindAcceptedUsersResponse{
Status: status.NewInternal(ctx, err, "error finding remote users: "+err.Error()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package token
package ocminvitemanager

import (
"time"
Expand All @@ -25,32 +25,21 @@ import (
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/google/uuid"
"github.com/pkg/errors"
)

// DefaultExpirationTime is the expiration time to be used when unspecified in the config.
const DefaultExpirationTime = "24h"

// CreateToken creates a InviteToken object for the userID indicated by userID.
func CreateToken(expiration string, userID *userpb.UserId) (*invitepb.InviteToken, error) {
// Parse time of expiration
duration, err := time.ParseDuration(expiration)
if err != nil {
return nil, errors.Wrap(err, "error parsing time of expiration")
}

func CreateToken(expiration time.Duration, userID *userpb.UserId, description string) *invitepb.InviteToken {
tokenID := uuid.New().String()
now := time.Now()
expirationTime := now.Add(duration)
expirationTime := now.Add(expiration)

token := invitepb.InviteToken{
return &invitepb.InviteToken{
Token: tokenID,
UserId: userID,
Expiration: &typesv1beta1.Timestamp{
Seconds: uint64(expirationTime.Unix()),
Nanos: 0,
},
Description: description,
}

return &token, nil
}
Loading

0 comments on commit 14c9c5f

Please sign in to comment.