Skip to content

Commit

Permalink
[CLOUDTRUST-2295] Bridge endpoint to self-ask for an email validation
Browse files Browse the repository at this point in the history
[CLOUDTRUST-2296] Bridge endpoint to self-ask for a phone number validation
[CLOUDTRUST-2299] Send email when user updates his email/phone. Unvalidate his account validation
  • Loading branch information
fperot74 committed Feb 25, 2020
1 parent 0e44c68 commit 0a67a1f
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 110 deletions.
40 changes: 28 additions & 12 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 16 additions & 1 deletion api/account/swagger-api_account.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,22 @@ paths:
responses:
200:
description: Successful operation.

/account/verify-email:
put:
tags:
- Account
summary: Request an email verification
responses:
200:
description: successful operation
/account/verify-phone-number:
put:
tags:
- Account
summary: Request a phone number verification
responses:
200:
description: successful operation
components:
schemas:
UpdatePassword:
Expand Down
7 changes: 7 additions & 0 deletions cmd/keycloakb/keycloak_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,8 @@ func main() {
UpdateLabelCredential: prepareEndpoint(account.MakeUpdateLabelCredentialEndpoint(accountComponent), "update_label_credential", influxMetrics, accountLogger, tracer, rateLimit["account"]),
MoveCredential: prepareEndpoint(account.MakeMoveCredentialEndpoint(accountComponent), "move_credential", influxMetrics, accountLogger, tracer, rateLimit["account"]),
GetConfiguration: prepareEndpoint(account.MakeGetConfigurationEndpoint(accountComponent), "get_configuration", influxMetrics, accountLogger, tracer, rateLimit["account"]),
SendVerifyEmail: prepareEndpoint(account.MakeSendVerifyEmailEndpoint(accountComponent), "send_verify_email", influxMetrics, accountLogger, tracer, rateLimit["account"]),
SendVerifyPhoneNumber: prepareEndpoint(account.MakeSendVerifyPhoneNumberEndpoint(accountComponent), "send_verify_phone_number", influxMetrics, accountLogger, tracer, rateLimit["account"]),
}
}

Expand Down Expand Up @@ -939,6 +941,8 @@ func main() {
var updateAccountHandler = configureAccountHandler(keycloakb.ComponentName, ComponentID, idGenerator, keycloakClient, audienceRequired, tracer, logger)(accountEndpoints.UpdateAccount)
var deleteAccountHandler = configureAccountHandler(keycloakb.ComponentName, ComponentID, idGenerator, keycloakClient, audienceRequired, tracer, logger)(accountEndpoints.DeleteAccount)
var getConfigurationHandler = configureAccountHandler(keycloakb.ComponentName, ComponentID, idGenerator, keycloakClient, audienceRequired, tracer, logger)(accountEndpoints.GetConfiguration)
var sendVerifyEmailHandler = configureAccountHandler(keycloakb.ComponentName, ComponentID, idGenerator, keycloakClient, audienceRequired, tracer, logger)(accountEndpoints.SendVerifyEmail)
var sendVerifyPhoneNumberHandler = configureAccountHandler(keycloakb.ComponentName, ComponentID, idGenerator, keycloakClient, audienceRequired, tracer, logger)(accountEndpoints.SendVerifyPhoneNumber)

route.Path("/account").Methods("GET").Handler(getAccountHandler)
route.Path("/account").Methods("POST").Handler(updateAccountHandler)
Expand All @@ -953,6 +957,9 @@ func main() {
route.Path("/account/credentials/{credentialID}").Methods("PUT").Handler(updateLabelCredentialHandler)
route.Path("/account/credentials/{credentialID}/after/{previousCredentialID}").Methods("POST").Handler(moveCredentialHandler)

route.Path("/account/verify-email").Methods("PUT").Handler(sendVerifyEmailHandler)
route.Path("/account/verify-phone-number").Methods("PUT").Handler(sendVerifyPhoneNumberHandler)

var handler http.Handler = route

if accessLogsEnabled {
Expand Down
10 changes: 10 additions & 0 deletions pkg/account/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,16 @@ func (c *authorizationComponentMW) GetConfiguration(ctx context.Context, realmID
return c.next.GetConfiguration(ctx, realmIDOverride)
}

func (c *authorizationComponentMW) SendVerifyEmail(ctx context.Context) error {
// No restriction for this call
return c.next.SendVerifyEmail(ctx)
}

func (c *authorizationComponentMW) SendVerifyPhoneNumber(ctx context.Context) error {
// No restriction for this call
return c.next.SendVerifyPhoneNumber(ctx)
}

func isEnabled(booleanPtr *bool) bool {
return booleanPtr != nil && *booleanPtr
}
78 changes: 51 additions & 27 deletions pkg/account/authorization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestNoRestrictions(t *testing.T) {

var mockLogger = log.NewNopLogger()
var mockConfigurationDBModule = mock.NewConfigurationDBModule(mockCtrl)
var mockAccountComponent = mock.NewAccountComponent(mockCtrl)
var mockAccountComponent = mock.NewComponent(mockCtrl)

var accessToken = "TOKEN=="
var realmName = "master"
Expand All @@ -37,29 +37,53 @@ func TestNoRestrictions(t *testing.T) {
var ctx = context.WithValue(context.Background(), cs.CtContextAccessToken, accessToken)
ctx = context.WithValue(ctx, cs.CtContextRealm, realmName)

mockAccountComponent.EXPECT().GetCredentials(ctx).Return([]api.CredentialRepresentation{}, nil).Times(1)
_, err = authorizationMW.GetCredentials(ctx)
assert.Nil(t, err)

mockAccountComponent.EXPECT().GetCredentialRegistrators(ctx).Return([]string{}, nil).Times(1)
_, err = authorizationMW.GetCredentialRegistrators(ctx)
assert.Nil(t, err)

mockAccountComponent.EXPECT().UpdateLabelCredential(ctx, credentialID, "newLabel").Return(nil).Times(1)
err = authorizationMW.UpdateLabelCredential(ctx, credentialID, "newLabel")
assert.Nil(t, err)

mockAccountComponent.EXPECT().MoveCredential(ctx, credentialID, credentialID).Return(nil).Times(1)
err = authorizationMW.MoveCredential(ctx, credentialID, credentialID)
assert.Nil(t, err)

mockAccountComponent.EXPECT().GetAccount(ctx).Return(api.AccountRepresentation{}, nil).Times(1)
_, err = authorizationMW.GetAccount(ctx)
assert.Nil(t, err)

mockAccountComponent.EXPECT().GetConfiguration(ctx, "").Return(api.Configuration{}, nil).Times(1)
_, err = authorizationMW.GetConfiguration(ctx, "")
assert.Nil(t, err)
t.Run("GetCredentials", func(t *testing.T) {
mockAccountComponent.EXPECT().GetCredentials(ctx).Return([]api.CredentialRepresentation{}, nil).Times(1)
_, err = authorizationMW.GetCredentials(ctx)
assert.Nil(t, err)
})

t.Run("GetCredentialRegistrators", func(t *testing.T) {
mockAccountComponent.EXPECT().GetCredentialRegistrators(ctx).Return([]string{}, nil).Times(1)
_, err = authorizationMW.GetCredentialRegistrators(ctx)
assert.Nil(t, err)
})

t.Run("UpdateLabelCredential", func(t *testing.T) {
mockAccountComponent.EXPECT().UpdateLabelCredential(ctx, credentialID, "newLabel").Return(nil).Times(1)
err = authorizationMW.UpdateLabelCredential(ctx, credentialID, "newLabel")
assert.Nil(t, err)
})

t.Run("MoveCredential", func(t *testing.T) {
mockAccountComponent.EXPECT().MoveCredential(ctx, credentialID, credentialID).Return(nil).Times(1)
err = authorizationMW.MoveCredential(ctx, credentialID, credentialID)
assert.Nil(t, err)
})

t.Run("GetAccount", func(t *testing.T) {
mockAccountComponent.EXPECT().GetAccount(ctx).Return(api.AccountRepresentation{}, nil).Times(1)
_, err = authorizationMW.GetAccount(ctx)
assert.Nil(t, err)
})

t.Run("GetConfiguration", func(t *testing.T) {
mockAccountComponent.EXPECT().GetConfiguration(ctx, "").Return(api.Configuration{}, nil).Times(1)
_, err = authorizationMW.GetConfiguration(ctx, "")
assert.Nil(t, err)
})

t.Run("SendVerifyEmail", func(t *testing.T) {
mockAccountComponent.EXPECT().SendVerifyEmail(ctx).Return(nil).Times(1)
err = authorizationMW.SendVerifyEmail(ctx)
assert.Nil(t, err)
})

t.Run("SendVerifyPhoneNumber", func(t *testing.T) {
mockAccountComponent.EXPECT().SendVerifyPhoneNumber(ctx).Return(nil).Times(1)
err = authorizationMW.SendVerifyPhoneNumber(ctx)
assert.Nil(t, err)
})
}
}

Expand All @@ -69,7 +93,7 @@ func TestDeny(t *testing.T) {

var mockLogger = log.NewNopLogger()
var mockConfigurationDBModule = mock.NewConfigurationDBModule(mockCtrl)
var mockAccountComponent = mock.NewAccountComponent(mockCtrl)
var mockAccountComponent = mock.NewComponent(mockCtrl)

var accessToken = "TOKEN=="
var realmName = "master"
Expand Down Expand Up @@ -117,7 +141,7 @@ func TestAllowed(t *testing.T) {

var mockLogger = log.NewNopLogger()
var mockConfigurationDBModule = mock.NewConfigurationDBModule(mockCtrl)
var mockAccountComponent = mock.NewAccountComponent(mockCtrl)
var mockAccountComponent = mock.NewComponent(mockCtrl)

var accessToken = "TOKEN=="
var realmName = "master"
Expand Down Expand Up @@ -170,7 +194,7 @@ func TestError(t *testing.T) {

var mockLogger = log.NewNopLogger()
var mockConfigurationDBModule = mock.NewConfigurationDBModule(mockCtrl)
var mockAccountComponent = mock.NewAccountComponent(mockCtrl)
var mockAccountComponent = mock.NewComponent(mockCtrl)

var accessToken = "TOKEN=="
var realmName = "master"
Expand Down
46 changes: 45 additions & 1 deletion pkg/account/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"net/http"
"strconv"
"strings"

cs "github.com/cloudtrust/common-service"
"github.com/cloudtrust/common-service/configuration"
Expand All @@ -16,6 +17,12 @@ import (
kc "github.com/cloudtrust/keycloak-client"
)

// Constants
const (
ActionVerifyEmail = "VERIFY_EMAIL"
ActionVerifyPhoneNumber = "mobilephone-validation"
)

// KeycloakAccountClient interface exposes methods we need to call to send requests to Keycloak API of Account
type KeycloakAccountClient interface {
UpdatePassword(accessToken, realm, currentPassword, newPassword, confirmPassword string) (string, error)
Expand All @@ -28,6 +35,7 @@ type KeycloakAccountClient interface {
UpdateAccount(accessToken, realm string, user kc.UserRepresentation) error
GetAccount(accessToken, realm string) (kc.UserRepresentation, error)
DeleteAccount(accessToken, realm string) error
ExecuteActionsEmail(accessToken string, realmName string, actions []string) error
}

// Component interface exposes methods used by the bridge API
Expand All @@ -42,6 +50,8 @@ type Component interface {
UpdateAccount(context.Context, api.AccountRepresentation) error
DeleteAccount(context.Context) error
GetConfiguration(context.Context, string) (api.Configuration, error)
SendVerifyEmail(ctx context.Context) error
SendVerifyPhoneNumber(ctx context.Context) error
}

// ConfigurationDBModule is the interface of the configuration module.
Expand Down Expand Up @@ -159,11 +169,13 @@ func (c *component) UpdateAccount(ctx context.Context, user api.AccountRepresent
}

var emailVerified, phoneNumberVerified *bool
var actions []string

// when the email changes, set the EmailVerified to false
if user.Email != nil && oldUserKc.Email != nil && *oldUserKc.Email != *user.Email {
var verified = false
emailVerified = &verified
actions = append(actions, ActionVerifyEmail)
}

// when the phone number changes, set the PhoneNumberVerified to false
Expand All @@ -173,6 +185,7 @@ func (c *component) UpdateAccount(ctx context.Context, user api.AccountRepresent
if _, ok := m["phoneNumber"]; !ok || m["phoneNumber"][0] != *user.PhoneNumber {
var verified = false
phoneNumberVerified = &verified
actions = append(actions, ActionVerifyPhoneNumber)
}
} else { // the user has no attributes until now, i.e. he has not set yet his phone number
var verified = false
Expand Down Expand Up @@ -220,7 +233,11 @@ func (c *component) UpdateAccount(ctx context.Context, user api.AccountRepresent
//store the API call into the DB
c.reportEvent(ctx, "UPDATE_ACCOUNT", database.CtEventRealmName, realm, database.CtEventUserID, userID, database.CtEventUsername, username)

return nil
if len(actions) > 0 {
err = c.executeActions(ctx, actions)
}

return err
}

func (c *component) DeleteAccount(ctx context.Context) error {
Expand Down Expand Up @@ -370,3 +387,30 @@ func (c *component) GetConfiguration(ctx context.Context, realmIDOverride string

return apiConfig, nil
}

func (c *component) SendVerifyEmail(ctx context.Context) error {
return c.executeActions(ctx, []string{ActionVerifyEmail})
}

func (c *component) SendVerifyPhoneNumber(ctx context.Context) error {
return c.executeActions(ctx, []string{ActionVerifyPhoneNumber})
}

func (c *component) executeActions(ctx context.Context, actions []string) error {
var accessToken = ctx.Value(cs.CtContextAccessToken).(string)
var currentRealm = ctx.Value(cs.CtContextRealm).(string)
var userID = ctx.Value(cs.CtContextUserID).(string)
var err = c.keycloakAccountClient.ExecuteActionsEmail(accessToken, currentRealm, actions)

if err != nil {
c.logger.Warn(ctx, "err", err.Error())
return err
}

var additionalInfos = map[string]string{"actions": strings.Join(actions, ",")}
var additionalBytes, _ = json.Marshal(additionalInfos)
var additionalString = string(additionalBytes)
c.reportEvent(ctx, "ACTION_EMAIL", database.CtEventRealmName, currentRealm, database.CtEventUserID, userID, database.CtEventAdditionalInfo, additionalString)

return err
}
Loading

0 comments on commit 0a67a1f

Please sign in to comment.