Skip to content

Commit a521f61

Browse files
heiytorgustavosbarreto
authored andcommitted
feat(api): generate new user token upon exiting authenticated namespace
Automatically generate a fresh token when users exit the authenticated namespace.
1 parent 3063a3d commit a521f61

File tree

5 files changed

+139
-31
lines changed

5 files changed

+139
-31
lines changed

api/routes/nsadm.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,15 @@ func (h *Handler) LeaveNamespace(c gateway.Context) error {
186186
return err
187187
}
188188

189-
if err := h.service.LeaveNamespace(c.Ctx(), req); err != nil {
189+
res, err := h.service.LeaveNamespace(c.Ctx(), req)
190+
switch {
191+
case err != nil:
190192
return err
193+
case res != nil:
194+
return c.JSON(http.StatusOK, res)
195+
default:
196+
return c.NoContent(http.StatusOK)
191197
}
192-
193-
return c.NoContent(http.StatusOK)
194198
}
195199

196200
func (h *Handler) EditNamespaceMember(c gateway.Context) error {

api/routes/nsadm_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ func TestHandler_LeaveNamespace(t *testing.T) {
416416
requiredMocks: func() {
417417
svcMock.
418418
On("LeaveNamespace", gomock.Anything, &requests.LeaveNamespace{UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", AuthenticatedTenantID: "00000000-0000-4000-0000-000000000000"}).
419-
Return(errors.New("error")).
419+
Return(nil, errors.New("error")).
420420
Once()
421421
},
422422
expected: http.StatusInternalServerError,
@@ -431,7 +431,7 @@ func TestHandler_LeaveNamespace(t *testing.T) {
431431
requiredMocks: func() {
432432
svcMock.
433433
On("LeaveNamespace", gomock.Anything, &requests.LeaveNamespace{UserID: "000000000000000000000000", TenantID: "00000000-0000-4000-0000-000000000000", AuthenticatedTenantID: "00000000-0000-4000-0000-000000000000"}).
434-
Return(nil).
434+
Return(nil, nil).
435435
Once()
436436
},
437437
expected: http.StatusOK,

api/services/member.go

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ type MemberService interface {
4545
// LeaveNamespace allows an authenticated user to remove themselves from a namespace. Owners cannot leave a namespace.
4646
// If the user attempts to leave the namespace they are authenticated to, their authentication token will be invalidated.
4747
// Returns an error, if any.
48-
LeaveNamespace(ctx context.Context, req *requests.LeaveNamespace) error
48+
LeaveNamespace(ctx context.Context, req *requests.LeaveNamespace) (*models.UserAuthResponse, error)
4949
}
5050

5151
func (s *service) AddNamespaceMember(ctx context.Context, req *requests.NamespaceAddMember) (*models.Namespace, error) {
@@ -232,30 +232,43 @@ func (s *service) RemoveNamespaceMember(ctx context.Context, req *requests.Names
232232
return s.store.NamespaceGet(ctx, req.TenantID, s.store.Options().CountAcceptedDevices(), s.store.Options().EnrichMembersData())
233233
}
234234

235-
func (s *service) LeaveNamespace(ctx context.Context, req *requests.LeaveNamespace) error {
235+
func (s *service) LeaveNamespace(ctx context.Context, req *requests.LeaveNamespace) (*models.UserAuthResponse, error) {
236236
ns, err := s.store.NamespaceGet(ctx, req.TenantID)
237237
if err != nil {
238-
return NewErrNamespaceNotFound(req.TenantID, err)
238+
return nil, NewErrNamespaceNotFound(req.TenantID, err)
239239
}
240240

241241
if m, ok := ns.FindMember(req.UserID); !ok || m.Role == authorizer.RoleOwner {
242-
return NewErrAuthForbidden()
242+
return nil, NewErrAuthForbidden()
243243
}
244244

245245
if err := s.removeMember(ctx, ns, req.UserID); err != nil { //nolint:revive
246-
return err
246+
return nil, err
247247
}
248248

249-
if req.TenantID == req.AuthenticatedTenantID {
250-
if err := s.AuthUncacheToken(ctx, req.TenantID, req.UserID); err != nil {
251-
log.WithError(err).
252-
WithField("tenant_id", req.TenantID).
253-
WithField("user_id", req.UserID).
254-
Error("failed to uncache the token")
255-
}
249+
// If the user is attempting to leave a namespace other than the authenticated one,
250+
// there is no need to generate a new token.
251+
if req.TenantID != req.AuthenticatedTenantID {
252+
return nil, nil
256253
}
257254

258-
return nil
255+
emptyString := "" // just to be used as a pointer
256+
if err := s.store.UserUpdate(ctx, req.UserID, &models.UserChanges{PreferredNamespace: &emptyString}); err != nil {
257+
log.WithError(err).
258+
WithField("tenant_id", req.TenantID).
259+
WithField("user_id", req.UserID).
260+
Error("failed to reset user's preferred namespace")
261+
}
262+
263+
if err := s.AuthUncacheToken(ctx, req.TenantID, req.UserID); err != nil {
264+
log.WithError(err).
265+
WithField("tenant_id", req.TenantID).
266+
WithField("user_id", req.UserID).
267+
Error("failed to uncache the token")
268+
}
269+
270+
// TODO: make this method a util function
271+
return s.CreateUserToken(ctx, &requests.CreateUserToken{UserID: req.UserID})
259272
}
260273

261274
func (s *service) removeMember(ctx context.Context, ns *models.Namespace, userID string) error {

api/services/member_test.go

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,6 +1562,7 @@ func TestRemoveNamespaceMember(t *testing.T) {
15621562

15631563
func TestService_LeaveNamespace(t *testing.T) {
15641564
type Expected struct {
1565+
res *models.UserAuthResponse
15651566
err error
15661567
}
15671568

@@ -1587,7 +1588,10 @@ func TestService_LeaveNamespace(t *testing.T) {
15871588
Return(nil, ErrNamespaceNotFound).
15881589
Once()
15891590
},
1590-
expected: Expected{err: NewErrNamespaceNotFound("00000000-0000-4000-0000-000000000000", ErrNamespaceNotFound)},
1591+
expected: Expected{
1592+
res: nil,
1593+
err: NewErrNamespaceNotFound("00000000-0000-4000-0000-000000000000", ErrNamespaceNotFound),
1594+
},
15911595
},
15921596
{
15931597
description: "fails when the user is not on the namespace",
@@ -1607,7 +1611,10 @@ func TestService_LeaveNamespace(t *testing.T) {
16071611
}, nil).
16081612
Once()
16091613
},
1610-
expected: Expected{err: NewErrAuthForbidden()},
1614+
expected: Expected{
1615+
res: nil,
1616+
err: NewErrAuthForbidden(),
1617+
},
16111618
},
16121619
{
16131620
description: "fails when the user is owner",
@@ -1632,7 +1639,10 @@ func TestService_LeaveNamespace(t *testing.T) {
16321639
}, nil).
16331640
Once()
16341641
},
1635-
expected: Expected{err: NewErrAuthForbidden()},
1642+
expected: Expected{
1643+
res: nil,
1644+
err: NewErrAuthForbidden(),
1645+
},
16361646
},
16371647
{
16381648
description: "fails when cannot remove the member",
@@ -1661,7 +1671,10 @@ func TestService_LeaveNamespace(t *testing.T) {
16611671
Return(errors.New("error")).
16621672
Once()
16631673
},
1664-
expected: Expected{errors.New("error")},
1674+
expected: Expected{
1675+
res: nil,
1676+
err: errors.New("error"),
1677+
},
16651678
},
16661679
{
16671680
description: "succeeds",
@@ -1690,7 +1703,10 @@ func TestService_LeaveNamespace(t *testing.T) {
16901703
Return(nil).
16911704
Once()
16921705
},
1693-
expected: Expected{err: nil},
1706+
expected: Expected{
1707+
res: nil,
1708+
err: nil,
1709+
},
16941710
},
16951711
{
16961712
description: "succeeds when TenantID is equal to AuthenticatedTenantID",
@@ -1718,12 +1734,69 @@ func TestService_LeaveNamespace(t *testing.T) {
17181734
On("NamespaceRemoveMember", ctx, "00000000-0000-4000-0000-000000000000", "000000000000000000000000").
17191735
Return(nil).
17201736
Once()
1737+
emptyString := ""
1738+
storeMock.
1739+
On("UserUpdate", ctx, "000000000000000000000000", &models.UserChanges{PreferredNamespace: &emptyString}).
1740+
Return(nil).
1741+
Once()
17211742
cacheMock.
17221743
On("Delete", ctx, "token_00000000-0000-4000-0000-000000000000000000000000000000000000").
17231744
Return(nil).
17241745
Once()
1746+
1747+
// NOTE: This test is a replica of TestService_CreateUserToken because this method
1748+
// internally calls it to create another token. Since this functionality is already tested,
1749+
// we are duplicating the test here to prevent failures. The important tests are all in the lines above.
1750+
storeMock.
1751+
On("UserGetByID", ctx, "000000000000000000000000", false).
1752+
Return(
1753+
&models.User{
1754+
ID: "000000000000000000000000",
1755+
Status: models.UserStatusConfirmed,
1756+
LastLogin: now,
1757+
MFA: models.UserMFA{
1758+
Enabled: false,
1759+
},
1760+
UserData: models.UserData{
1761+
Username: "john_doe",
1762+
1763+
Name: "john doe",
1764+
},
1765+
Password: models.UserPassword{
1766+
Hash: "$2a$10$V/6N1wsjheBVvWosPfv02uf4WAOb9lmp8YWQCIa2UYuFV4OJby7Yi",
1767+
},
1768+
Preferences: models.UserPreferences{
1769+
PreferredNamespace: "",
1770+
},
1771+
},
1772+
0,
1773+
nil,
1774+
).
1775+
Once()
1776+
storeMock.
1777+
On("NamespaceGetPreferred", ctx, "000000000000000000000000").
1778+
Return(nil, store.ErrNoDocuments).
1779+
Once()
1780+
clockMock := new(clockmock.Clock)
1781+
clock.DefaultBackend = clockMock
1782+
clockMock.On("Now").Return(now)
1783+
cacheMock.
1784+
On("Set", ctx, "token_000000000000000000000000", mock.Anything, time.Hour*72).
1785+
Return(nil).
1786+
Once()
1787+
},
1788+
expected: Expected{
1789+
res: &models.UserAuthResponse{
1790+
ID: "000000000000000000000000",
1791+
Name: "john doe",
1792+
User: "john_doe",
1793+
1794+
Tenant: "",
1795+
Role: "",
1796+
Token: "must ignore",
1797+
},
1798+
err: nil,
17251799
},
1726-
expected: Expected{err: nil},
17271800
},
17281801
}
17291802

@@ -1734,8 +1807,14 @@ func TestService_LeaveNamespace(t *testing.T) {
17341807
ctx := context.TODO()
17351808
tc.requiredMocks(ctx)
17361809

1737-
err := s.LeaveNamespace(ctx, tc.req)
1738-
assert.Equal(t, tc.expected, Expected{err})
1810+
res, err := s.LeaveNamespace(ctx, tc.req)
1811+
// Since the resulting token is not crucial for the assertion and
1812+
// difficult to mock, it is safe to ignore this field.
1813+
if res != nil {
1814+
res.Token = "must ignore"
1815+
}
1816+
1817+
assert.Equal(t, tc.expected, Expected{res, err})
17391818
})
17401819
}
17411820

api/services/mocks/services.go

Lines changed: 17 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)