storj/satellite/console/service_test.go
Cameron 0633aca607 satellite/console: create new consoleauth service
We want to send email verification reminders to users from the satellite
core, but some of the functionality required to do so exists in the
satellite console service. We could simply import the console service
into the core to achieve this, but the service requires a lot of
dependencies that would go unused just to be able to send these emails.

Instead, we break out the needed functionality into a new service which
can be imported separately by the console service and the future email
chore.

The consoleauth service creates, signs, and checks the expiration of auth
tokens.

Change-Id: I2ad794b7fd256f8af24c1a8d73a203d508069078
2022-05-13 16:27:07 +00:00

869 lines
30 KiB
Go

// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
package console_test
import (
"context"
"math"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"storj.io/common/macaroon"
"storj.io/common/memory"
"storj.io/common/storj"
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/common/uuid"
"storj.io/storj/private/testplanet"
"storj.io/storj/satellite"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/console/consoleauth"
)
func TestService(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 2,
},
func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
service := sat.API.Console.Service
up1Pro1, err := sat.API.DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID)
require.NoError(t, err)
up2Pro1, err := sat.API.DB.Console().Projects().Get(ctx, planet.Uplinks[1].Projects[0].ID)
require.NoError(t, err)
up2User, err := sat.API.DB.Console().Users().Get(ctx, up2Pro1.OwnerID)
require.NoError(t, err)
require.NotEqual(t, up1Pro1.ID, up2Pro1.ID)
require.NotEqual(t, up1Pro1.OwnerID, up2Pro1.OwnerID)
authCtx1, err := sat.AuthenticatedContext(ctx, up1Pro1.OwnerID)
require.NoError(t, err)
authCtx2, err := sat.AuthenticatedContext(ctx, up2Pro1.OwnerID)
require.NoError(t, err)
t.Run("TestGetProject", func(t *testing.T) {
// Getting own project details should work
project, err := service.GetProject(authCtx1, up1Pro1.ID)
require.NoError(t, err)
require.Equal(t, up1Pro1.ID, project.ID)
// Getting someone else project details should not work
project, err = service.GetProject(authCtx1, up2Pro1.ID)
require.Error(t, err)
require.Nil(t, project)
})
t.Run("TestUpdateProject", func(t *testing.T) {
updatedName := "newName"
updatedDescription := "newDescription"
updatedStorageLimit := memory.Size(100)
updatedBandwidthLimit := memory.Size(100)
// user should be in free tier
user, err := service.GetUser(ctx, up1Pro1.OwnerID)
require.NoError(t, err)
require.False(t, user.PaidTier)
// get context
authCtx1, err := sat.AuthenticatedContext(ctx, user.ID)
require.NoError(t, err)
// add a credit card to put the user in the paid tier
err = service.Payments().AddCreditCard(authCtx1, "test-cc-token")
require.NoError(t, err)
// update auth ctx
authCtx1, err = sat.AuthenticatedContext(ctx, user.ID)
require.NoError(t, err)
// Updating own project should work
updatedProject, err := service.UpdateProject(authCtx1, up1Pro1.ID, console.ProjectInfo{
Name: updatedName,
Description: updatedDescription,
StorageLimit: updatedStorageLimit,
BandwidthLimit: updatedBandwidthLimit,
})
require.NoError(t, err)
require.NotEqual(t, up1Pro1.Name, updatedProject.Name)
require.Equal(t, updatedName, updatedProject.Name)
require.NotEqual(t, up1Pro1.Description, updatedProject.Description)
require.Equal(t, updatedDescription, updatedProject.Description)
require.NotEqual(t, *up1Pro1.StorageLimit, *updatedProject.StorageLimit)
require.Equal(t, updatedStorageLimit, *updatedProject.StorageLimit)
require.NotEqual(t, *up1Pro1.BandwidthLimit, *updatedProject.BandwidthLimit)
require.Equal(t, updatedBandwidthLimit, *updatedProject.BandwidthLimit)
// Updating someone else project details should not work
updatedProject, err = service.UpdateProject(authCtx1, up2Pro1.ID, console.ProjectInfo{
Name: "newName",
Description: "TestUpdate",
StorageLimit: memory.Size(100),
BandwidthLimit: memory.Size(100),
})
require.Error(t, err)
require.Nil(t, updatedProject)
// attempting to update a project with bandwidth or storage limits set to 0 should fail
size0 := new(memory.Size)
*size0 = 0
size100 := new(memory.Size)
*size100 = memory.Size(100)
up1Pro1.StorageLimit = size0
err = sat.DB.Console().Projects().Update(ctx, up1Pro1)
require.NoError(t, err)
updateInfo := console.ProjectInfo{
Name: "a b c",
Description: "1 2 3",
StorageLimit: memory.Size(123),
BandwidthLimit: memory.Size(123),
}
updatedProject, err = service.UpdateProject(authCtx1, up1Pro1.ID, updateInfo)
require.Error(t, err)
require.Nil(t, updatedProject)
up1Pro1.StorageLimit = size100
up1Pro1.BandwidthLimit = size0
err = sat.DB.Console().Projects().Update(ctx, up1Pro1)
require.NoError(t, err)
updatedProject, err = service.UpdateProject(authCtx1, up1Pro1.ID, updateInfo)
require.Error(t, err)
require.Nil(t, updatedProject)
up1Pro1.StorageLimit = size100
up1Pro1.BandwidthLimit = size100
err = sat.DB.Console().Projects().Update(ctx, up1Pro1)
require.NoError(t, err)
updatedProject, err = service.UpdateProject(authCtx1, up1Pro1.ID, updateInfo)
require.NoError(t, err)
require.Equal(t, updateInfo.Name, updatedProject.Name)
require.Equal(t, updateInfo.Description, updatedProject.Description)
require.NotNil(t, updatedProject.StorageLimit)
require.NotNil(t, updatedProject.BandwidthLimit)
require.Equal(t, updateInfo.StorageLimit, *updatedProject.StorageLimit)
require.Equal(t, updateInfo.BandwidthLimit, *updatedProject.BandwidthLimit)
project, err := service.GetProject(authCtx1, up1Pro1.ID)
require.NoError(t, err)
require.Equal(t, updateInfo.StorageLimit, *project.StorageLimit)
require.Equal(t, updateInfo.BandwidthLimit, *project.BandwidthLimit)
})
t.Run("TestAddProjectMembers", func(t *testing.T) {
// Adding members to own project should work
addedUsers, err := service.AddProjectMembers(authCtx1, up1Pro1.ID, []string{up2User.Email})
require.NoError(t, err)
require.Len(t, addedUsers, 1)
require.Contains(t, addedUsers, up2User)
// Adding members to someone else project should not work
addedUsers, err = service.AddProjectMembers(authCtx1, up2Pro1.ID, []string{up2User.Email})
require.Error(t, err)
require.Nil(t, addedUsers)
})
t.Run("TestGetProjectMembers", func(t *testing.T) {
// Getting the project members of an own project that one is a part of should work
userPage, err := service.GetProjectMembers(authCtx1, up1Pro1.ID, console.ProjectMembersCursor{Page: 1, Limit: 10})
require.NoError(t, err)
require.Len(t, userPage.ProjectMembers, 2)
// Getting the project members of a foreign project that one is a part of should work
userPage, err = service.GetProjectMembers(authCtx2, up1Pro1.ID, console.ProjectMembersCursor{Page: 1, Limit: 10})
require.NoError(t, err)
require.Len(t, userPage.ProjectMembers, 2)
// Getting the project members of a foreign project that one is not a part of should not work
userPage, err = service.GetProjectMembers(authCtx1, up2Pro1.ID, console.ProjectMembersCursor{Page: 1, Limit: 10})
require.Error(t, err)
require.Nil(t, userPage)
})
t.Run("TestDeleteProjectMembers", func(t *testing.T) {
// Deleting project members of an own project should work
err := service.DeleteProjectMembers(authCtx1, up1Pro1.ID, []string{up2User.Email})
require.NoError(t, err)
// Deleting Project members of someone else project should not work
err = service.DeleteProjectMembers(authCtx1, up2Pro1.ID, []string{up2User.Email})
require.Error(t, err)
})
t.Run("TestDeleteProject", func(t *testing.T) {
// Deleting the own project should not work before deleting the API-Key
err := service.DeleteProject(authCtx1, up1Pro1.ID)
require.Error(t, err)
keys, err := service.GetAPIKeys(authCtx1, up1Pro1.ID, console.APIKeyCursor{Page: 1, Limit: 10})
require.NoError(t, err)
require.Len(t, keys.APIKeys, 1)
err = service.DeleteAPIKeys(authCtx1, []uuid.UUID{keys.APIKeys[0].ID})
require.NoError(t, err)
// Deleting the own project should now work
err = service.DeleteProject(authCtx1, up1Pro1.ID)
require.NoError(t, err)
// Deleting someone else project should not work
err = service.DeleteProject(authCtx1, up2Pro1.ID)
require.Error(t, err)
err = planet.Uplinks[1].CreateBucket(ctx, sat, "testbucket")
require.NoError(t, err)
// deleting a project with a bucket should fail
err = service.DeleteProject(authCtx2, up2Pro1.ID)
require.Error(t, err)
require.Equal(t, "console service: project usage: some buckets still exist", err.Error())
})
t.Run("TestChangeEmail", func(t *testing.T) {
const newEmail = "newEmail@example.com"
err = service.ChangeEmail(authCtx2, newEmail)
require.NoError(t, err)
user, _, err := service.GetUserByEmailWithUnverified(authCtx2, newEmail)
require.NoError(t, err)
require.Equal(t, newEmail, user.Email)
err = service.ChangeEmail(authCtx2, newEmail)
require.Error(t, err)
})
t.Run("TestGetAllBucketNames", func(t *testing.T) {
bucket1 := storj.Bucket{
ID: testrand.UUID(),
Name: "testBucket1",
ProjectID: up2Pro1.ID,
}
bucket2 := storj.Bucket{
ID: testrand.UUID(),
Name: "testBucket2",
ProjectID: up2Pro1.ID,
}
_, err := sat.API.Buckets.Service.CreateBucket(authCtx2, bucket1)
require.NoError(t, err)
_, err = sat.API.Buckets.Service.CreateBucket(authCtx2, bucket2)
require.NoError(t, err)
bucketNames, err := service.GetAllBucketNames(authCtx2, up2Pro1.ID)
require.NoError(t, err)
require.Equal(t, bucket1.Name, bucketNames[0])
require.Equal(t, bucket2.Name, bucketNames[1])
// Getting someone else buckets should not work
bucketsForUnauthorizedUser, err := service.GetAllBucketNames(authCtx1, up2Pro1.ID)
require.Error(t, err)
require.Nil(t, bucketsForUnauthorizedUser)
})
t.Run("TestDeleteAPIKeyByNameAndProjectID", func(t *testing.T) {
secret, err := macaroon.NewSecret()
require.NoError(t, err)
key, err := macaroon.NewAPIKey(secret)
require.NoError(t, err)
apikey := console.APIKeyInfo{
Name: "test",
ProjectID: up2Pro1.ID,
Secret: secret,
}
createdKey, err := sat.DB.Console().APIKeys().Create(ctx, key.Head(), apikey)
require.NoError(t, err)
info, err := sat.DB.Console().APIKeys().Get(ctx, createdKey.ID)
require.NoError(t, err)
require.NotNil(t, info)
// Deleting someone else api keys should not work
err = service.DeleteAPIKeyByNameAndProjectID(authCtx1, apikey.Name, up2Pro1.ID)
require.Error(t, err)
err = service.DeleteAPIKeyByNameAndProjectID(authCtx2, apikey.Name, up2Pro1.ID)
require.NoError(t, err)
info, err = sat.DB.Console().APIKeys().Get(ctx, createdKey.ID)
require.Error(t, err)
require.Nil(t, info)
})
})
}
func TestPaidTier(t *testing.T) {
usageConfig := console.UsageLimitsConfig{
Storage: console.StorageLimitConfig{
Free: memory.GB,
Paid: memory.TB,
},
Bandwidth: console.BandwidthLimitConfig{
Free: 2 * memory.GB,
Paid: 2 * memory.TB,
},
Segment: console.SegmentLimitConfig{
Free: 10,
Paid: 50,
},
Project: console.ProjectLimitConfig{
Free: 1,
Paid: 3,
},
}
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
Reconfigure: testplanet.Reconfigure{
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
config.Console.UsageLimits = usageConfig
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
service := sat.API.Console.Service
// project should have free tier usage limits
proj1, err := sat.API.DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID)
require.NoError(t, err)
require.Equal(t, usageConfig.Storage.Free, *proj1.StorageLimit)
require.Equal(t, usageConfig.Bandwidth.Free, *proj1.BandwidthLimit)
require.Equal(t, usageConfig.Segment.Free, *proj1.SegmentLimit)
// user should be in free tier
user, err := service.GetUser(ctx, proj1.OwnerID)
require.NoError(t, err)
require.False(t, user.PaidTier)
authCtx, err := sat.AuthenticatedContext(ctx, user.ID)
require.NoError(t, err)
// add a credit card to the user
err = service.Payments().AddCreditCard(authCtx, "test-cc-token")
require.NoError(t, err)
// expect user to be in paid tier
user, err = service.GetUser(ctx, user.ID)
require.NoError(t, err)
require.True(t, user.PaidTier)
require.Equal(t, usageConfig.Project.Paid, user.ProjectLimit)
// update auth ctx
authCtx, err = sat.AuthenticatedContext(ctx, user.ID)
require.NoError(t, err)
// expect project to be migrated to paid tier usage limits
proj1, err = service.GetProject(authCtx, proj1.ID)
require.NoError(t, err)
require.Equal(t, usageConfig.Storage.Paid, *proj1.StorageLimit)
require.Equal(t, usageConfig.Bandwidth.Paid, *proj1.BandwidthLimit)
require.Equal(t, usageConfig.Segment.Paid, *proj1.SegmentLimit)
// expect new project to be created with paid tier usage limits
proj2, err := service.CreateProject(authCtx, console.ProjectInfo{Name: "Project 2"})
require.NoError(t, err)
require.Equal(t, usageConfig.Storage.Paid, *proj2.StorageLimit)
})
}
func TestMFA(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
service := sat.API.Console.Service
user, err := sat.AddUser(ctx, console.CreateUser{
FullName: "MFA Test User",
Email: "mfauser@mail.test",
}, 1)
require.NoError(t, err)
getNewAuthorization := func() (context.Context, console.Authorization) {
authCtx, err := sat.AuthenticatedContext(ctx, user.ID)
require.NoError(t, err)
auth, err := console.GetAuth(authCtx)
require.NoError(t, err)
return authCtx, auth
}
authCtx, auth := getNewAuthorization()
var key string
t.Run("TestResetMFASecretKey", func(t *testing.T) {
key, err = service.ResetMFASecretKey(authCtx)
require.NoError(t, err)
authCtx, auth = getNewAuthorization()
require.NotEmpty(t, auth.User.MFASecretKey)
})
t.Run("TestEnableUserMFABadPasscode", func(t *testing.T) {
// Expect MFA-enabling attempt to be rejected when providing stale passcode.
badCode, err := console.NewMFAPasscode(key, time.Time{}.Add(time.Hour))
require.NoError(t, err)
err = service.EnableUserMFA(authCtx, badCode, time.Time{})
require.True(t, console.ErrValidation.Has(err))
authCtx, auth = getNewAuthorization()
_, err = service.ResetMFARecoveryCodes(authCtx)
require.True(t, console.ErrUnauthorized.Has(err))
authCtx, auth = getNewAuthorization()
require.False(t, auth.User.MFAEnabled)
})
t.Run("TestEnableUserMFAGoodPasscode", func(t *testing.T) {
// Expect MFA-enabling attempt to succeed when providing valid passcode.
goodCode, err := console.NewMFAPasscode(key, time.Time{})
require.NoError(t, err)
authCtx, auth = getNewAuthorization()
err = service.EnableUserMFA(authCtx, goodCode, time.Time{})
require.NoError(t, err)
authCtx, auth = getNewAuthorization()
require.True(t, auth.User.MFAEnabled)
require.Equal(t, auth.User.MFASecretKey, key)
})
t.Run("TestMFAGetToken", func(t *testing.T) {
request := console.AuthUser{Email: user.Email, Password: user.FullName}
// Expect no token due to lack of MFA passcode.
token, err := service.Token(ctx, request)
require.True(t, console.ErrMFAMissing.Has(err))
require.Empty(t, token)
// Expect no token due to bad MFA passcode.
wrongCode, err := console.NewMFAPasscode(key, time.Now().Add(time.Hour))
require.NoError(t, err)
request.MFAPasscode = wrongCode
token, err = service.Token(ctx, request)
require.True(t, console.ErrMFAPasscode.Has(err))
require.Empty(t, token)
// Expect token when providing valid passcode.
goodCode, err := console.NewMFAPasscode(key, time.Now())
require.NoError(t, err)
request.MFAPasscode = goodCode
token, err = service.Token(ctx, request)
require.NoError(t, err)
require.NotEmpty(t, token)
})
t.Run("TestMFARecoveryCodes", func(t *testing.T) {
_, err = service.ResetMFARecoveryCodes(authCtx)
require.NoError(t, err)
authCtx, auth = getNewAuthorization()
require.Len(t, auth.User.MFARecoveryCodes, console.MFARecoveryCodeCount)
for _, code := range auth.User.MFARecoveryCodes {
// Ensure code is of the form XXXX-XXXX-XXXX where X is A-Z or 0-9.
require.Regexp(t, "^([A-Z0-9]{4})((-[A-Z0-9]{4})){2}$", code)
// Expect token when providing valid recovery code.
request := console.AuthUser{Email: user.Email, Password: user.FullName, MFARecoveryCode: code}
token, err := service.Token(ctx, request)
require.NoError(t, err)
require.NotEmpty(t, token)
// Expect no token due to providing previously-used recovery code.
token, err = service.Token(ctx, request)
require.True(t, console.ErrMFARecoveryCode.Has(err))
require.Empty(t, token)
authCtx, auth = getNewAuthorization()
}
_, err = service.ResetMFARecoveryCodes(authCtx)
require.NoError(t, err)
})
t.Run("TestDisableUserMFABadPasscode", func(t *testing.T) {
// Expect MFA-disabling attempt to fail when providing valid passcode.
badCode, err := console.NewMFAPasscode(key, time.Time{}.Add(time.Hour))
require.NoError(t, err)
authCtx, auth = getNewAuthorization()
err = service.DisableUserMFA(authCtx, badCode, time.Time{}, "")
require.True(t, console.ErrValidation.Has(err))
authCtx, auth = getNewAuthorization()
require.True(t, auth.User.MFAEnabled)
require.NotEmpty(t, auth.User.MFASecretKey)
require.NotEmpty(t, auth.User.MFARecoveryCodes)
})
t.Run("TestDisableUserMFAConflict", func(t *testing.T) {
// Expect MFA-disabling attempt to fail when providing both recovery code and passcode.
goodCode, err := console.NewMFAPasscode(key, time.Time{})
require.NoError(t, err)
authCtx, auth = getNewAuthorization()
err = service.DisableUserMFA(authCtx, goodCode, time.Time{}, auth.User.MFARecoveryCodes[0])
require.True(t, console.ErrMFAConflict.Has(err))
authCtx, auth = getNewAuthorization()
require.True(t, auth.User.MFAEnabled)
require.NotEmpty(t, auth.User.MFASecretKey)
require.NotEmpty(t, auth.User.MFARecoveryCodes)
})
t.Run("TestDisableUserMFAGoodPasscode", func(t *testing.T) {
// Expect MFA-disabling attempt to succeed when providing valid passcode.
goodCode, err := console.NewMFAPasscode(key, time.Time{})
require.NoError(t, err)
authCtx, auth = getNewAuthorization()
err = service.DisableUserMFA(authCtx, goodCode, time.Time{}, "")
require.NoError(t, err)
authCtx, auth = getNewAuthorization()
require.False(t, auth.User.MFAEnabled)
require.Empty(t, auth.User.MFASecretKey)
require.Empty(t, auth.User.MFARecoveryCodes)
})
t.Run("TestDisableUserMFAGoodRecoveryCode", func(t *testing.T) {
// Expect MFA-disabling attempt to succeed when providing valid recovery code.
// Enable MFA
key, err = service.ResetMFASecretKey(authCtx)
require.NoError(t, err)
goodCode, err := console.NewMFAPasscode(key, time.Time{})
require.NoError(t, err)
authCtx, auth = getNewAuthorization()
err = service.EnableUserMFA(authCtx, goodCode, time.Time{})
require.NoError(t, err)
authCtx, auth = getNewAuthorization()
_, err = service.ResetMFARecoveryCodes(authCtx)
require.NoError(t, err)
authCtx, auth = getNewAuthorization()
require.True(t, auth.User.MFAEnabled)
require.NotEmpty(t, auth.User.MFASecretKey)
require.NotEmpty(t, auth.User.MFARecoveryCodes)
// Disable MFA
err = service.DisableUserMFA(authCtx, "", time.Time{}, auth.User.MFARecoveryCodes[0])
require.NoError(t, err)
authCtx, auth = getNewAuthorization()
require.False(t, auth.User.MFAEnabled)
require.Empty(t, auth.User.MFASecretKey)
require.Empty(t, auth.User.MFARecoveryCodes)
})
})
}
func TestResetPassword(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
service := sat.API.Console.Service
user, err := sat.AddUser(ctx, console.CreateUser{
FullName: "Test User",
Email: "test@mail.test",
}, 1)
require.NoError(t, err)
newPass := user.FullName
getNewResetToken := func() *console.ResetPasswordToken {
token, err := sat.DB.Console().ResetPasswordTokens().Create(ctx, user.ID)
require.NoError(t, err)
require.NotNil(t, token)
return token
}
token := getNewResetToken()
// Expect error when providing bad token.
err = service.ResetPassword(ctx, "badToken", newPass, "", "", token.CreatedAt)
require.True(t, console.ErrRecoveryToken.Has(err))
// Expect error when providing good but expired token.
err = service.ResetPassword(ctx, token.Secret.String(), newPass, "", "", token.CreatedAt.Add(sat.Config.ConsoleAuth.TokenExpirationTime).Add(time.Second))
require.True(t, console.ErrTokenExpiration.Has(err))
// Expect error when providing good token with bad (too short) password.
err = service.ResetPassword(ctx, token.Secret.String(), "bad", "", "", token.CreatedAt)
require.True(t, console.ErrValidation.Has(err))
// Expect success when providing good token and good password.
err = service.ResetPassword(ctx, token.Secret.String(), newPass, "", "", token.CreatedAt)
require.NoError(t, err)
token = getNewResetToken()
// Enable MFA.
getNewAuthorization := func() (context.Context, console.Authorization) {
authCtx, err := sat.AuthenticatedContext(ctx, user.ID)
require.NoError(t, err)
auth, err := console.GetAuth(authCtx)
require.NoError(t, err)
return authCtx, auth
}
authCtx, _ := getNewAuthorization()
key, err := service.ResetMFASecretKey(authCtx)
require.NoError(t, err)
authCtx, auth := getNewAuthorization()
passcode, err := console.NewMFAPasscode(key, token.CreatedAt)
require.NoError(t, err)
err = service.EnableUserMFA(authCtx, passcode, token.CreatedAt)
require.NoError(t, err)
// Expect error when providing bad passcode.
badPasscode, err := console.NewMFAPasscode(key, token.CreatedAt.Add(time.Hour))
require.NoError(t, err)
err = service.ResetPassword(ctx, token.Secret.String(), newPass, badPasscode, "", token.CreatedAt)
require.True(t, console.ErrMFAPasscode.Has(err))
for _, recoveryCode := range auth.User.MFARecoveryCodes {
// Expect success when providing bad passcode and good recovery code.
err = service.ResetPassword(ctx, token.Secret.String(), newPass, badPasscode, recoveryCode, token.CreatedAt)
require.NoError(t, err)
token = getNewResetToken()
// Expect error when providing bad passcode and already-used recovery code.
err = service.ResetPassword(ctx, token.Secret.String(), newPass, badPasscode, recoveryCode, token.CreatedAt)
require.True(t, console.ErrMFARecoveryCode.Has(err))
}
// Expect success when providing good passcode.
err = service.ResetPassword(ctx, token.Secret.String(), newPass, passcode, "", token.CreatedAt)
require.NoError(t, err)
})
}
// TestActivateAccountToken ensures that the token returned after activating an account can be used to authorize user activity.
// i.e. a user does not need to acquire an authorization separate from the account activation step.
func TestActivateAccountToken(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
service := sat.API.Console.Service
createUser := console.CreateUser{
FullName: "Alice",
ShortName: "Alice",
Email: "alice@mail.test",
Password: "123a123",
}
regToken, err := service.CreateRegToken(ctx, 1)
require.NoError(t, err)
rootUser, err := service.CreateUser(ctx, createUser, regToken.Secret)
require.NoError(t, err)
activationToken, err := service.GenerateActivationToken(ctx, rootUser.ID, rootUser.Email)
require.NoError(t, err)
authToken, err := service.ActivateAccount(ctx, activationToken)
require.NoError(t, err)
_, err = service.Authorize(consoleauth.WithAPIKey(ctx, []byte(authToken)))
require.NoError(t, err)
})
}
func TestRESTKeys(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
service := sat.API.Console.Service
proj1, err := sat.API.DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID)
require.NoError(t, err)
user, err := service.GetUser(ctx, proj1.OwnerID)
require.NoError(t, err)
authCtx, err := sat.AuthenticatedContext(ctx, user.ID)
require.NoError(t, err)
now := time.Now()
expires := 5 * time.Hour
apiKey, expiresAt, err := service.CreateRESTKey(authCtx, expires)
require.NoError(t, err)
require.NotEmpty(t, apiKey)
require.True(t, expiresAt.After(now))
require.True(t, expiresAt.Before(now.Add(expires+time.Hour)))
// test revocation
require.NoError(t, service.RevokeRESTKey(authCtx, apiKey))
// test revoke non existent key
nonexistent := testrand.UUID()
err = service.RevokeRESTKey(authCtx, nonexistent.String())
require.Error(t, err)
})
}
// TestLockAccount ensures user's gets locked when incorrect credentials are provided.
func TestLockAccount(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
service := sat.API.Console.Service
usersDB := sat.DB.Console().Users()
consoleConfig := sat.Config.Console
newUser := console.CreateUser{
FullName: "token test",
Email: "token_test@mail.test",
}
user, err := sat.AddUser(ctx, newUser, 1)
require.NoError(t, err)
getNewAuthorization := func() (context.Context, console.Authorization) {
authCtx, err := sat.AuthenticatedContext(ctx, user.ID)
require.NoError(t, err)
auth, err := console.GetAuth(authCtx)
require.NoError(t, err)
return authCtx, auth
}
authCtx, _ := getNewAuthorization()
secret, err := service.ResetMFASecretKey(authCtx)
require.NoError(t, err)
goodCode0, err := console.NewMFAPasscode(secret, time.Time{})
require.NoError(t, err)
authCtx, _ = getNewAuthorization()
err = service.EnableUserMFA(authCtx, goodCode0, time.Time{})
require.NoError(t, err)
now := time.Now()
goodCode1, err := console.NewMFAPasscode(secret, now)
require.NoError(t, err)
authUser := console.AuthUser{
Email: newUser.Email,
Password: newUser.FullName,
MFAPasscode: goodCode1,
}
// successful login.
token, err := service.Token(ctx, authUser)
require.NoError(t, err)
require.NotEmpty(t, token)
// check if user's account gets locked because of providing wrong password.
authUser.Password = "qweQWE1@"
for i := 1; i <= consoleConfig.LoginAttemptsWithoutPenalty; i++ {
token, err = service.Token(ctx, authUser)
require.Empty(t, token)
if i < consoleConfig.LoginAttemptsWithoutPenalty {
require.True(t, console.ErrLoginPassword.Has(err))
} else {
require.True(t, console.ErrLockedAccount.Has(err))
}
}
lockedUser, err := service.GetUser(authCtx, user.ID)
require.NoError(t, err)
require.True(t, lockedUser.FailedLoginCount == consoleConfig.LoginAttemptsWithoutPenalty)
require.True(t, lockedUser.LoginLockoutExpiration.After(now))
// lock account once again and check if lockout expiration time increased.
expDuration := time.Duration(math.Pow(consoleConfig.FailedLoginPenalty, float64(lockedUser.FailedLoginCount-1))) * time.Minute
lockoutExpDate := now.Add(expDuration)
err = service.UpdateUsersFailedLoginState(authCtx, lockedUser, lockoutExpDate)
require.NoError(t, err)
lockedUser, err = service.GetUser(authCtx, user.ID)
require.NoError(t, err)
require.True(t, lockedUser.FailedLoginCount == consoleConfig.LoginAttemptsWithoutPenalty+1)
diff := lockedUser.LoginLockoutExpiration.Sub(now)
require.Greater(t, diff, time.Duration(consoleConfig.FailedLoginPenalty)*time.Minute)
// unlock account by successful login
lockedUser.LoginLockoutExpiration = now.Add(-time.Second)
err = usersDB.Update(authCtx, lockedUser)
require.NoError(t, err)
authUser.Password = newUser.FullName
token, err = service.Token(ctx, authUser)
require.NoError(t, err)
require.NotEmpty(t, token)
unlockedUser, err := service.GetUser(authCtx, user.ID)
require.NoError(t, err)
require.Zero(t, unlockedUser.FailedLoginCount)
// check if user's account gets locked because of providing wrong mfa passcode.
authUser.MFAPasscode = "000000"
for i := 1; i <= consoleConfig.LoginAttemptsWithoutPenalty; i++ {
token, err = service.Token(ctx, authUser)
require.Empty(t, token)
if i < consoleConfig.LoginAttemptsWithoutPenalty {
require.True(t, console.ErrMFAPasscode.Has(err))
} else {
require.True(t, console.ErrLockedAccount.Has(err))
}
}
lockedUser, err = service.GetUser(authCtx, user.ID)
require.NoError(t, err)
require.True(t, lockedUser.FailedLoginCount == consoleConfig.LoginAttemptsWithoutPenalty)
require.True(t, lockedUser.LoginLockoutExpiration.After(now))
// unlock account
lockedUser.LoginLockoutExpiration = time.Time{}
lockedUser.FailedLoginCount = 0
err = usersDB.Update(authCtx, lockedUser)
require.NoError(t, err)
// check if user's account gets locked because of providing wrong mfa recovery code.
authUser.MFAPasscode = ""
authUser.MFARecoveryCode = "000000"
for i := 1; i <= consoleConfig.LoginAttemptsWithoutPenalty; i++ {
token, err = service.Token(ctx, authUser)
require.Empty(t, token)
if i < consoleConfig.LoginAttemptsWithoutPenalty {
require.True(t, console.ErrMFARecoveryCode.Has(err))
} else {
require.True(t, console.ErrLockedAccount.Has(err))
}
}
lockedUser, err = service.GetUser(authCtx, user.ID)
require.NoError(t, err)
require.True(t, lockedUser.FailedLoginCount == consoleConfig.LoginAttemptsWithoutPenalty)
require.True(t, lockedUser.LoginLockoutExpiration.After(now))
})
}