Skip to content

Commit

Permalink
feat(api): add expiration to member invites
Browse files Browse the repository at this point in the history
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
  • Loading branch information
heiytor committed Sep 11, 2024
1 parent 3a2b47e commit e5a359f
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 15 deletions.
13 changes: 9 additions & 4 deletions api/services/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"strings"
"time"

"github.com/shellhub-io/shellhub/api/store"
"github.com/shellhub-io/shellhub/api/store/mongo"
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions api/services/namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
},
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 9 additions & 4 deletions api/store/mongo/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 10 additions & 4 deletions pkg/models/member.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

0 comments on commit e5a359f

Please sign in to comment.