From e5a359fbeb1f607ffc014c7d049c031483587464 Mon Sep 17 00:00:00 2001 From: Heitor Danilo Date: Wed, 11 Sep 2024 11:04:25 -0300 Subject: [PATCH] feat(api): add expiration to member invites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In cloud instances, members with a `pending` status must accept their invite within 7 days before they can perform any namespace actions. To handle this, we’ve added an expiration date to the member model. - Added `ExpiresAt` field to the member model - Implemented expiration setting for invites --- api/services/namespace.go | 13 +++++++++---- api/services/namespace_test.go | 6 +++--- api/store/mongo/namespace.go | 13 +++++++++---- pkg/models/member.go | 14 ++++++++++---- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/api/services/namespace.go b/api/services/namespace.go index 781d0daea40..2ea1b294dff 100644 --- a/api/services/namespace.go +++ b/api/services/namespace.go @@ -4,6 +4,7 @@ import ( "context" "errors" "strings" + "time" "github.com/shellhub-io/shellhub/api/store" "github.com/shellhub-io/shellhub/api/store/mongo" @@ -247,11 +248,15 @@ func (s *service) AddNamespaceMember(ctx context.Context, req *requests.Namespac return nil, NewErrUserNotFound(req.MemberEmail, err) } + addedAt := clock.Now() + expiresAt := addedAt.Add(7 * (24 * time.Hour)) + member := &models.Member{ - ID: passiveUser.ID, - AddedAt: clock.Now(), - Role: req.MemberRole, - Status: models.MemberStatusAccepted, + ID: passiveUser.ID, + AddedAt: addedAt, + ExpiresAt: expiresAt, + Role: req.MemberRole, + Status: models.MemberStatusAccepted, } // In cloud instances, the member must accept the invite before enter in the namespace. diff --git a/api/services/namespace_test.go b/api/services/namespace_test.go index e9d6f4f0d8c..6e71fb860ea 100644 --- a/api/services/namespace_test.go +++ b/api/services/namespace_test.go @@ -1321,7 +1321,7 @@ func TestAddNamespaceMember(t *testing.T) { Return("false"). Once() storeMock. - On("NamespaceAddMember", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000001", Role: authorizer.RoleObserver, Status: models.MemberStatusAccepted, AddedAt: now}). + On("NamespaceAddMember", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000001", Role: authorizer.RoleObserver, Status: models.MemberStatusAccepted, AddedAt: now, ExpiresAt: now.Add(7 * (24 * time.Hour))}). Return(errors.New("error")). Once() }, @@ -1374,7 +1374,7 @@ func TestAddNamespaceMember(t *testing.T) { Return("false"). Once() storeMock. - On("NamespaceAddMember", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000001", Role: authorizer.RoleObserver, Status: models.MemberStatusAccepted, AddedAt: now}). + On("NamespaceAddMember", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000001", Role: authorizer.RoleObserver, Status: models.MemberStatusAccepted, AddedAt: now, ExpiresAt: now.Add(7 * (24 * time.Hour))}). Return(nil). Once() storeMock. @@ -1516,7 +1516,7 @@ func TestAddNamespaceMember(t *testing.T) { Return(nil). Once() storeMock. - On("NamespaceAddMember", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000001", Role: authorizer.RoleObserver, Status: models.MemberStatusPending, AddedAt: now}). + On("NamespaceAddMember", ctx, "00000000-0000-4000-0000-000000000000", &models.Member{ID: "000000000000000000000001", Role: authorizer.RoleObserver, Status: models.MemberStatusPending, AddedAt: now, ExpiresAt: now.Add(7 * (24 * time.Hour))}). Return(nil). Once() storeMock. diff --git a/api/store/mongo/namespace.go b/api/store/mongo/namespace.go index 91bb1ca517c..117eddcf8d0 100644 --- a/api/store/mongo/namespace.go +++ b/api/store/mongo/namespace.go @@ -294,10 +294,11 @@ func (s *Store) NamespaceAddMember(ctx context.Context, tenantID string, member } memberBson := bson.M{ - "id": member.ID, - "added_at": member.AddedAt, - "role": member.Role, - "status": member.Status, + "id": member.ID, + "added_at": member.AddedAt, + "expires_at": member.ExpiresAt, + "role": member.Role, + "status": member.Status, } res, err := s.db. @@ -330,6 +331,10 @@ func (s *Store) NamespaceUpdateMember(ctx context.Context, tenantID string, memb update["members.$.status"] = changes.Status } + if changes.ExpiresAt != nil { + update["members.$.expires_at"] = *changes.ExpiresAt + } + ns, err := s.db.Collection("namespaces").UpdateOne(ctx, filter, bson.M{"$set": update}) if err != nil { return FromMongoError(err) diff --git a/pkg/models/member.go b/pkg/models/member.go index 0007bae024b..afd4c5c220e 100644 --- a/pkg/models/member.go +++ b/pkg/models/member.go @@ -14,14 +14,20 @@ const ( ) type Member struct { - ID string `json:"id,omitempty" bson:"id,omitempty"` - AddedAt time.Time `json:"added_at" bson:"added_at"` + ID string `json:"id,omitempty" bson:"id,omitempty"` + AddedAt time.Time `json:"added_at" bson:"added_at"` + + // ExpiresAt specifies the expiration date of the invite. This attribute is only applicable in *Cloud* instances, + // and it is ignored for members whose status is not 'pending'. + ExpiresAt time.Time `json:"expires_at" bson:"expires_at"` + Username string `json:"username,omitempty" bson:"username,omitempty" validate:"username"` // TODO: remove Role authorizer.Role `json:"role" bson:"role" validate:"required,oneof=administrator operator observer"` Status MemberStatus `json:"status" bson:"status"` } type MemberChanges struct { - Role authorizer.Role `bson:"role,omitempty"` - Status MemberStatus `bson:"status,omitempty"` + Role authorizer.Role `bson:"role,omitempty"` + Status MemberStatus `bson:"status,omitempty"` + ExpiresAt *time.Time `bson:"expires_at,omitempty"` }