Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
1 change: 1 addition & 0 deletions api/groups/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func getServiceRoutesConfig() config.ApiRoutesConfig {
{Name: "/sign-multiple-transactions", Open: true},
{Name: "/set-security-mode", Open: true},
{Name: "/unset-security-mode", Open: true},
{Name: "/user-status/:address", Open: true},
{Name: "/debug", Open: true},
{Name: "/verify-code", Open: true},
{Name: "/registered-users", Open: true},
Expand Down
47 changes: 47 additions & 0 deletions api/groups/guardianGroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
signMultipleTransactionsPath = "/sign-multiple-transactions"
setSecurityModeNoExpirePath = "/set-security-mode"
unsetSecurityModeNoExpirePath = "/unset-security-mode"
getUserStatusPath = "/user-status/:address"
registerPath = "/register"
verifyCodePath = "/verify-code"
registeredUsersPath = "/registered-users"
Expand Down Expand Up @@ -103,6 +104,11 @@ func NewGuardianGroup(facade shared.FacadeHandler) (*guardianGroup, error) {
Method: http.MethodGet,
Handler: gg.config,
},
{
Path: getUserStatusPath,
Method: http.MethodGet,
Handler: gg.getUserStatus,
},
}
gg.endpoints = endpoints

Expand Down Expand Up @@ -237,6 +243,47 @@ func (gg *guardianGroup) unsetSecurityModeNoExpire(c *gin.Context) {
returnStatus(c, nil, http.StatusOK, "", chainApiShared.ReturnCodeSuccess)
}

func (gg *guardianGroup) getUserStatus(c *gin.Context) {
var debugErr error

userIp := c.GetString(mfaMiddleware.UserIpKey)
userAgent := c.GetString(mfaMiddleware.UserAgentKey)
userAddr := c.Param("address")

defer func() {
logUserStatusRequest(userIp, userAgent, userAddr, debugErr)
}()

status, err := gg.facade.GetUserStatus(userAddr)
if err != nil {
debugErr = fmt.Errorf("%w while interrogating security status", err)
handleErrorAndReturn(c, status, err.Error())
return

}

returnStatus(c, status, http.StatusOK, "", chainApiShared.ReturnCodeSuccess)
}

func logUserStatusRequest(userIp string, userAgent string, userAddr string, debugErr error) {
logArgs := []interface{}{
"route", getUserStatusPath,
"ip", userIp,
"user agent", userAgent,
"userAddr", userAddr,
}
defer func() {
guardianLog.Info("Request info", logArgs...)
}()

if debugErr == nil {
logArgs = append(logArgs, "result", "success")
return
}

logArgs = append(logArgs, "error", debugErr.Error())
}

// signTransaction returns the transaction signed by the guardian if the verification passed
func (gg *guardianGroup) signTransaction(c *gin.Context) {
var request requests.SignTransaction
Expand Down
64 changes: 64 additions & 0 deletions api/groups/guardianGroup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,70 @@ func TestGuardianGroup_UnsetSecurityModeNoExpire(t *testing.T) {
})
}

func TestGuardianGroup_getUserStatus(t *testing.T) {
t.Parallel()

t.Run("facade returns error", func(t *testing.T) {
t.Parallel()

facade := mockFacade.GuardianFacadeStub{
GetUserStatusCalled: func(userAddress string) (*requests.UserStatusResponse, error) {
return &requests.UserStatusResponse{SecurityStatus: -1}, expectedError
},
}

gg, _ := groups.NewGuardianGroup(&facade)

ws := startWebServer(gg, "guardian", getServiceRoutesConfig(), providedAddr)

req, _ := http.NewRequest("GET", "/guardian/user-status/"+providedAddr, nil)
resp := httptest.NewRecorder()
ws.ServeHTTP(resp, req)

statusRsp := generalResponse{}
loadResponse(resp.Body, &statusRsp)

expectedGenResponse := createExpectedGeneralResponse(&requests.UserStatusResponse{
SecurityStatus: -1,
}, "")

assert.Equal(t, expectedGenResponse.Data, statusRsp.Data)
assert.True(t, strings.Contains(statusRsp.Error, expectedError.Error()))
require.Equal(t, http.StatusInternalServerError, resp.Code)
})

t.Run("should work", func(t *testing.T) {
t.Parallel()

facade := mockFacade.GuardianFacadeStub{
GetUserStatusCalled: func(userAddress string) (*requests.UserStatusResponse, error) {
return &requests.UserStatusResponse{
SecurityStatus: 1,
}, nil
},
}

gg, _ := groups.NewGuardianGroup(&facade)

ws := startWebServer(gg, "guardian", getServiceRoutesConfig(), providedAddr)

req, _ := http.NewRequest("GET", "/guardian/user-status/"+providedAddr, nil)
resp := httptest.NewRecorder()
ws.ServeHTTP(resp, req)

statusRsp := generalResponse{}
loadResponse(resp.Body, &statusRsp)

expectedGenResponse := createExpectedGeneralResponse(&requests.UserStatusResponse{
SecurityStatus: 1,
}, "")

assert.Equal(t, expectedGenResponse.Data, statusRsp.Data)
assert.Equal(t, "", statusRsp.Error)
require.Equal(t, http.StatusOK, resp.Code)
})
}

func TestGuardianGroup_signMultipleTransaction(t *testing.T) {
t.Parallel()

Expand Down
1 change: 1 addition & 0 deletions api/shared/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type FacadeHandler interface {
SignMultipleTransactions(userIp string, request requests.SignMultipleTransactions) ([][]byte, *requests.OTPCodeVerifyData, error)
SetSecurityModeNoExpire(userIp string, request requests.SecurityModeNoExpire) (*requests.OTPCodeVerifyData, error)
UnsetSecurityModeNoExpire(userIp string, request requests.SecurityModeNoExpire) (*requests.OTPCodeVerifyData, error)
GetUserStatus(userAddress string) (*requests.UserStatusResponse, error)
RegisteredUsers() (uint32, error)
TcsConfig() *tcsCore.TcsConfig
GetMetrics() map[string]*requests.EndpointMetricsResponse
Expand Down
1 change: 1 addition & 0 deletions cmd/multi-factor-auth/config/api.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ RestApiInterface = ":8080" # The interface `address and port` to which the REST
{ Name = "/sign-multiple-transactions", Open = true, Auth = false, MaxContentLength = 1500000 },
{ Name = "/set-security-mode", Open = true, Auth = false, MaxContentLength = 200 },
{ Name = "/unset-security-mode", Open = true, Auth = false, MaxContentLength = 200 },
{ Name = "/user-status/:address", Open = true, Auth = false },
{ Name = "/verify-code", Open = true, Auth = true, MaxContentLength = 200 },
{ Name = "/registered-users", Open = true, Auth = false },
{ Name = "/config", Open = true, Auth = false },
Expand Down
20 changes: 20 additions & 0 deletions cmd/multi-factor-auth/swagger/data.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we need to also generate swagger files based on these changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,23 @@ type _ struct {
// required:true
Payload requests.SecurityModeNoExpire
}

// swagger:route GET /user-status Guardian getUsersStatus
// Returns the status of the user.
// This request does not need the Authorization header
//
// responses:
// 200: userStatusResponse

// The status of the operation
// swagger:response userStatusResponse
type _ struct {
// in:body
Body struct {
// UserStatusResponse
// HTTP status code
Status string `json:"status"`
// Internal error
Error string `json:"error"`
}
}
12 changes: 12 additions & 0 deletions core/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,15 @@ const (

// NoExpiryValue is the returned value for a persistent key expiry time
const NoExpiryValue = -1

// Status represents the status of the security mode
type Status int
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing comments on new fields

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is too generic, especially when used for the security mode

maybe EnhancedSecurityModeStatus

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed


const (
// NotSet means that security mode is not activated
NotSet Status = iota
// ManuallySet means that security mode was activated because of failures
ManuallySet
// AutomaticallySet means that security mode was activated by user with SetSecurityModeNoExpire
AutomaticallySet
)
1 change: 1 addition & 0 deletions core/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type ServiceResolver interface {
SignMessage(userIp string, request requests.SignMessage) ([]byte, *requests.OTPCodeVerifyData, error)
SetSecurityModeNoExpire(userIp string, request requests.SecurityModeNoExpire) (*requests.OTPCodeVerifyData, error)
UnsetSecurityModeNoExpire(userIp string, request requests.SecurityModeNoExpire) (*requests.OTPCodeVerifyData, error)
GetUserStatus(userAddr string) (*requests.UserStatusResponse, error)
SignTransaction(userIp string, request requests.SignTransaction) ([]byte, *requests.OTPCodeVerifyData, error)
SignMultipleTransactions(userIp string, request requests.SignMultipleTransactions) ([][]byte, *requests.OTPCodeVerifyData, error)
RegisteredUsers() (uint32, error)
Expand Down
5 changes: 5 additions & 0 deletions core/requests/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ type SecurityModeNoExpire struct {
UserAddr string `json:"user"`
}

// UserStatusResponse is the JSON response for the user status interrogation
type UserStatusResponse struct {
SecurityStatus int `json:"status"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rename to SecurityModeStatus? or EnhancedSecurityModeStatus?

SecurityStatus = NotSet doesn't sound good, as the account has protection via regular 2FA

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

}

// SignMessageResponse is the service response to the sign message request
type SignMessageResponse struct {
Message string `json:"message"`
Expand Down
5 changes: 5 additions & 0 deletions facade/guardianFacade.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ func (gf *guardianFacade) UnsetSecurityModeNoExpire(userIp string, request reque
return gf.serviceResolver.UnsetSecurityModeNoExpire(userIp, request)
}

// GetUserStatus returns the user's security status
func (gf *guardianFacade) GetUserStatus(userAddress string) (*requests.UserStatusResponse, error) {
return gf.serviceResolver.GetUserStatus(userAddress)
}

// SignMultipleTransactions validates user's transactions, then adds guardian signature and returns the transaction
func (gf *guardianFacade) SignMultipleTransactions(userIp string, request requests.SignMultipleTransactions) ([][]byte, *requests.OTPCodeVerifyData, error) {
return gf.serviceResolver.SignMultipleTransactions(userIp, request)
Expand Down
12 changes: 12 additions & 0 deletions facade/guardianFacade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ func TestGuardianFacade_Getters(t *testing.T) {
}
wasUnsetSecurityModeNoExpireCalled := false

providedUserAddr := "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"

expectedUserStatusResponse := requests.UserStatusResponse{SecurityStatus: 1}

args.ServiceResolver = &testscommon.ServiceResolverStub{
VerifyCodeCalled: func(userAddress sdkCore.AddressHandler, userIp string, request requests.VerificationPayload) (*requests.OTPCodeVerifyData, error) {
assert.Equal(t, providedVerifyCodeReq, request)
Expand Down Expand Up @@ -142,6 +146,10 @@ func TestGuardianFacade_Getters(t *testing.T) {
wasUnsetSecurityModeNoExpireCalled = true
return nil, nil
},
GetUserStatusCalled: func(userAddress string) (*requests.UserStatusResponse, error) {
assert.Equal(t, providedUserAddr, providedUserAddr)
return &expectedUserStatusResponse, nil
},
SignTransactionCalled: func(userIp string, request requests.SignTransaction) ([]byte, *requests.OTPCodeVerifyData, error) {
assert.Equal(t, providedIp, userIp)
assert.Equal(t, providedSignTxReq, request)
Expand Down Expand Up @@ -207,6 +215,10 @@ func TestGuardianFacade_Getters(t *testing.T) {
assert.Nil(t, err)
assert.True(t, wasUnsetSecurityModeNoExpireCalled)

userStatus, err := facadeInstance.GetUserStatus(providedUserAddr)
assert.Nil(t, err)
assert.Equal(t, &expectedUserStatusResponse, userStatus)

signedTxs, _, err := facadeInstance.SignMultipleTransactions(providedIp, providedSignMultipleTxsReq)
assert.Nil(t, err)
assert.Equal(t, expectedSignMultipleTxsResponse, signedTxs)
Expand Down
1 change: 1 addition & 0 deletions handlers/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type SecureOtpHandler interface {
SecurityModeMaxFailures() uint64
SetSecurityModeNoExpire(key string) error
UnsetSecurityModeNoExpire(key string) error
GetSecurityStatus(key string) core.Status
IsVerificationAllowedAndIncreaseTrials(account string, ip string) (*requests.OTPCodeVerifyData, error)
Reset(account string, ip string)
DecrementSecurityModeFailedTrials(account string) error
Expand Down
5 changes: 5 additions & 0 deletions handlers/secureOtp/secureOtpHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ func (totp *secureOtpHandler) UnsetSecurityModeNoExpire(key string) error {
return totp.rateLimiter.UnsetSecurityModeNoExpire(key)
}

// GetSecurityStatus returns the status of the security mode
func (totp *secureOtpHandler) GetSecurityStatus(key string) core.Status {
return totp.rateLimiter.GetSecurityStatus(key)
}

// Reset removes the account and ip from local cache
func (totp *secureOtpHandler) Reset(account string, ip string) {
key := computeVerificationKey(account, ip)
Expand Down
15 changes: 15 additions & 0 deletions handlers/secureOtp/secureOtpHandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,21 @@ func TestSecureOtpHandler_UnsetSecurityModeNoExpireShouldErr(t *testing.T) {
require.Equal(t, expectedErr, err)
}

func TestSecureOtpHandler_GetSecurityStatusShouldWork(t *testing.T) {
t.Parallel()

args := createMockArgsSecureOtpHandler()
args.RateLimiter = &testscommon.RateLimiterStub{
GetSecurityStatusCalled: func(key string) core.Status {
return core.NotSet
},
}
totp, _ := secureOtp.NewSecureOtpHandler(args)
require.NotNil(t, totp)

require.Equal(t, core.NotSet, totp.GetSecurityStatus(account))
}

func TestSecureOtpHandler_Getters(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading