Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamp OCM invitation workflow #3611

Merged
merged 29 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
53ce075
implemented ocm client
gmgigi96 Jan 12, 2023
c9d69f4
refactored ocm invite manager
gmgigi96 Jan 12, 2023
119969d
load invite repository at runtime
gmgigi96 Jan 13, 2023
9d593b2
add sciencemesh http provider to generate token (and send email), acc…
gmgigi96 Jan 16, 2023
676bc74
fix linter
gmgigi96 Jan 16, 2023
11f6e8c
moved ocm endpoint in sciencemesh http service
gmgigi96 Jan 16, 2023
ced02b7
return initiator's info when accepting invite
gmgigi96 Jan 16, 2023
adb30aa
return remote user info on ForwardInvite
gmgigi96 Jan 16, 2023
2614d5d
return user ingo on AcceptInvite
gmgigi96 Jan 16, 2023
58f2491
fix get user
gmgigi96 Jan 16, 2023
98169c8
complete invitation workflow storing the initiator
gmgigi96 Jan 17, 2023
8da841e
set a description when creating an invitation token
gmgigi96 Jan 17, 2023
8c59fa4
add errors according to apis
gmgigi96 Jan 18, 2023
e518a39
add ocm invitation workflow tests
gmgigi96 Jan 18, 2023
2733019
add config for two ocm servers
gmgigi96 Jan 19, 2023
6b0ed29
register sciencemesh service
gmgigi96 Jan 19, 2023
d74dd46
fix error ocm
gmgigi96 Jan 19, 2023
aca6dde
add errors when accepting an invite in science mesh service
gmgigi96 Jan 19, 2023
d30f4af
remoted unprotected endpoints in sciencemesh service
gmgigi96 Jan 19, 2023
098f928
allows files to be added when starting revad for tests
gmgigi96 Jan 19, 2023
5282d15
renamed toml files used for integration tests for ocm
gmgigi96 Jan 19, 2023
ae087ef
add more test for ocm and sciencemesh services
gmgigi96 Jan 19, 2023
7d50e0f
fix other tests
gmgigi96 Jan 19, 2023
2e04447
use fork for go cs3apis bindings
gmgigi96 Jan 19, 2023
d675bb2
fixes to linter
gmgigi96 Jan 19, 2023
5a369ab
fix linter
gmgigi96 Jan 20, 2023
2b55772
add changelog
gmgigi96 Jan 20, 2023
cf446c9
fixes
gmgigi96 Jan 20, 2023
ce11c22
update go-cs3apis
gmgigi96 Jan 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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