From 40f220db394565dfbc7761001d3ac70c72eaa64e Mon Sep 17 00:00:00 2001 From: Francis PEROT Date: Mon, 9 Mar 2020 15:58:14 +0100 Subject: [PATCH] [CLOUDTRUST-2357] Add a way to solve temporary locked user --- api/management/swagger-api_management.yaml | 53 +++++++++++++++------- cmd/keycloakb/keycloak_bridge.go | 4 ++ pkg/management/authorization.go | 12 +++++ pkg/management/authorization_test.go | 7 +++ pkg/management/component.go | 15 ++++++ pkg/management/component_test.go | 29 ++++++++++++ pkg/management/endpoint.go | 11 +++++ pkg/management/endpoint_test.go | 22 +++++++++ 8 files changed, 137 insertions(+), 16 deletions(-) diff --git a/api/management/swagger-api_management.yaml b/api/management/swagger-api_management.yaml index 7c9ff1f1..7f981d46 100644 --- a/api/management/swagger-api_management.yaml +++ b/api/management/swagger-api_management.yaml @@ -655,11 +655,11 @@ paths: type: array items: $ref: '#/components/schemas/Credential' - /realms/{realm}/users/{userID}/recovery-code: - post: + /realms/{realm}/users/{userID}/credentials/{credentialID}: + delete: tags: - Credentials - summary: Set a recovery code for the user + summary: Delete the credentials for the user parameters: - name: realm in: path @@ -673,20 +673,20 @@ paths: required: true schema: type: string + - name: credentialID + in: path + description: Credential id + required: true + schema: + type: string responses: - 201: + 200: description: successful operation - content: - text/plain: - schema: - type: string - 409: - description: recovery code already set - /realms/{realm}/users/{userID}/credentials/{credentialID}: + /realms/{realm}/users/{userID}/clear-login-failures: delete: tags: - - Credentials - summary: Delete the credentials for the user + - login-failures + summary: Delete the login failures for the user parameters: - name: realm in: path @@ -700,15 +700,36 @@ paths: required: true schema: type: string - - name: credentialID + responses: + 200: + description: successful operation + /realms/{realm}/users/{userID}/recovery-code: + post: + tags: + - Credentials + summary: Set a recovery code for the user + parameters: + - name: realm in: path - description: Credential id + description: realm name (not id!) + required: true + schema: + type: string + - name: userID + in: path + description: User id required: true schema: type: string responses: - 200: + 201: description: successful operation + content: + text/plain: + schema: + type: string + 409: + description: recovery code already set /realms/{realm}/roles: get: tags: diff --git a/cmd/keycloakb/keycloak_bridge.go b/cmd/keycloakb/keycloak_bridge.go index 44481a90..0be1f39c 100644 --- a/cmd/keycloakb/keycloak_bridge.go +++ b/cmd/keycloakb/keycloak_bridge.go @@ -581,6 +581,7 @@ func main() { CreateRecoveryCode: prepareEndpoint(management.MakeCreateRecoveryCodeEndpoint(keycloakComponent), "create_recovery_code_endpoint", influxMetrics, managementLogger, tracer, rateLimit["management"]), GetCredentialsForUser: prepareEndpoint(management.MakeGetCredentialsForUserEndpoint(keycloakComponent), "get_credentials_for_user_endpoint", influxMetrics, managementLogger, tracer, rateLimit["management"]), DeleteCredentialsForUser: prepareEndpoint(management.MakeDeleteCredentialsForUserEndpoint(keycloakComponent), "delete_credentials_for_user_endpoint", influxMetrics, managementLogger, tracer, rateLimit["management"]), + ClearUserLoginFailures: prepareEndpoint(management.MakeClearUserLoginFailures(keycloakComponent), "clear_user_login_failures_endpoint", influxMetrics, managementLogger, tracer, rateLimit["management"]), GetRealmCustomConfiguration: prepareEndpoint(management.MakeGetRealmCustomConfigurationEndpoint(keycloakComponent), "get_realm_custom_config_endpoint", influxMetrics, managementLogger, tracer, rateLimit["management"]), UpdateRealmCustomConfiguration: prepareEndpoint(management.MakeUpdateRealmCustomConfigurationEndpoint(keycloakComponent), "update_realm_custom_config_endpoint", influxMetrics, managementLogger, tracer, rateLimit["management"]), @@ -841,6 +842,7 @@ func main() { var getCredentialsForUserHandler = configureManagementHandler(keycloakb.ComponentName, ComponentID, idGenerator, keycloakClient, audienceRequired, tracer, logger)(managementEndpoints.GetCredentialsForUser) var deleteCredentialsForUserHandler = configureManagementHandler(keycloakb.ComponentName, ComponentID, idGenerator, keycloakClient, audienceRequired, tracer, logger)(managementEndpoints.DeleteCredentialsForUser) + var clearUserLoginFailuresHandler = configureManagementHandler(keycloakb.ComponentName, ComponentID, idGenerator, keycloakClient, audienceRequired, tracer, logger)(managementEndpoints.ClearUserLoginFailures) var getRealmCustomConfigurationHandler = configureManagementHandler(keycloakb.ComponentName, ComponentID, idGenerator, keycloakClient, audienceRequired, tracer, logger)(managementEndpoints.GetRealmCustomConfiguration) var updateRealmCustomConfigurationHandler = configureManagementHandler(keycloakb.ComponentName, ComponentID, idGenerator, keycloakClient, audienceRequired, tracer, logger)(managementEndpoints.UpdateRealmCustomConfiguration) @@ -899,6 +901,8 @@ func main() { managementSubroute.Path("/realms/{realm}/users/{userID}/credentials").Methods("GET").Handler(getCredentialsForUserHandler) managementSubroute.Path("/realms/{realm}/users/{userID}/credentials/{credentialID}").Methods("DELETE").Handler(deleteCredentialsForUserHandler) + managementSubroute.Path("/realms/{realm}/users/{userID}/clear-login-failures").Methods("DELETE").Handler(clearUserLoginFailuresHandler) + // roles managementSubroute.Path("/realms/{realm}/roles").Methods("GET").Handler(getRolesHandler) managementSubroute.Path("/realms/{realm}/roles-by-id/{roleID}").Methods("GET").Handler(getRoleHandler) diff --git a/pkg/management/authorization.go b/pkg/management/authorization.go index 08e55577..9aa5d1fe 100644 --- a/pkg/management/authorization.go +++ b/pkg/management/authorization.go @@ -48,6 +48,7 @@ var ( MGMTCreateRecoveryCode = newAction("MGMT_CreateRecoveryCode", security.ScopeGroup) MGMTGetCredentialsForUser = newAction("MGMT_GetCredentialsForUser", security.ScopeGroup) MGMTDeleteCredentialsForUser = newAction("MGMT_DeleteCredentialsForUser", security.ScopeGroup) + MGMTClearUserLoginFailures = newAction("MGMT_ClearUserLoginFailures", security.ScopeGroup) MGMTGetRoles = newAction("MGMT_GetRoles", security.ScopeRealm) MGMTGetRole = newAction("MGMT_GetRole", security.ScopeRealm) MGMTGetGroups = newAction("MGMT_GetGroups", security.ScopeRealm) @@ -371,6 +372,17 @@ func (c *authorizationComponentMW) DeleteCredentialsForUser(ctx context.Context, return c.next.DeleteCredentialsForUser(ctx, realmName, userID, credentialID) } +func (c *authorizationComponentMW) ClearUserLoginFailures(ctx context.Context, realmName, userID string) error { + var action = MGMTClearUserLoginFailures.String() + var targetRealm = realmName + + if err := c.authManager.CheckAuthorizationOnTargetUser(ctx, action, targetRealm, userID); err != nil { + return err + } + + return c.next.ClearUserLoginFailures(ctx, realmName, userID) +} + func (c *authorizationComponentMW) GetRoles(ctx context.Context, realmName string) ([]api.RoleRepresentation, error) { var action = MGMTGetRoles.String() var targetRealm = realmName diff --git a/pkg/management/authorization_test.go b/pkg/management/authorization_test.go index ff3e67fa..901b4fec 100644 --- a/pkg/management/authorization_test.go +++ b/pkg/management/authorization_test.go @@ -179,6 +179,9 @@ func TestDeny(t *testing.T) { err = authorizationMW.DeleteCredentialsForUser(ctx, realmName, userID, credentialID) assert.Equal(t, security.ForbiddenError{}, err) + err = authorizationMW.ClearUserLoginFailures(ctx, realmName, userID) + assert.Equal(t, security.ForbiddenError{}, err) + _, err = authorizationMW.GetRoles(ctx, realmName) assert.Equal(t, security.ForbiddenError{}, err) @@ -431,6 +434,10 @@ func TestAllowed(t *testing.T) { err = authorizationMW.DeleteCredentialsForUser(ctx, realmName, userID, credentialID) assert.Nil(t, err) + mockManagementComponent.EXPECT().ClearUserLoginFailures(ctx, realmName, userID).Return(nil).Times(1) + err = authorizationMW.ClearUserLoginFailures(ctx, realmName, userID) + assert.Nil(t, err) + mockManagementComponent.EXPECT().GetRoles(ctx, realmName).Return([]api.RoleRepresentation{}, nil).Times(1) _, err = authorizationMW.GetRoles(ctx, realmName) assert.Nil(t, err) diff --git a/pkg/management/component.go b/pkg/management/component.go index b2062e3d..2176acaa 100644 --- a/pkg/management/component.go +++ b/pkg/management/component.go @@ -59,6 +59,7 @@ type KeycloakClient interface { UpdateLabelCredential(accessToken string, realmName string, userID string, credentialID string, label string) error DeleteCredential(accessToken string, realmName string, userID string, credentialID string) error CreateShadowUser(accessToken string, realmName string, userID string, provider string, fedID kc.FederatedIdentityRepresentation) error + ClearUserLoginFailures(accessToken string, realmName, userID string) error } // Component is the management component interface. @@ -91,6 +92,7 @@ type Component interface { CreateRecoveryCode(ctx context.Context, realmName string, userID string) (string, error) GetCredentialsForUser(ctx context.Context, realmName string, userID string) ([]api.CredentialRepresentation, error) DeleteCredentialsForUser(ctx context.Context, realmName string, userID string, credentialID string) error + ClearUserLoginFailures(ctx context.Context, realmName, userID string) error GetRoles(ctx context.Context, realmName string) ([]api.RoleRepresentation, error) GetRole(ctx context.Context, realmName string, roleID string) (api.RoleRepresentation, error) GetClientRoles(ctx context.Context, realmName, idClient string) ([]api.RoleRepresentation, error) @@ -790,6 +792,19 @@ func (c *component) DeleteCredentialsForUser(ctx context.Context, realmName stri return err } +func (c *component) ClearUserLoginFailures(ctx context.Context, realmName, userID string) error { + var accessToken = ctx.Value(cs.CtContextAccessToken).(string) + var err = c.keycloakClient.ClearUserLoginFailures(accessToken, realmName, userID) + if err != nil { + c.logger.Warn(ctx, "err", err.Error()) + return err + } + + c.reportEvent(ctx, "LOGIN_FAILURE_CLEARED", database.CtEventRealmName, realmName, database.CtEventUserID, userID) + + return nil +} + func (c *component) GetRoles(ctx context.Context, realmName string) ([]api.RoleRepresentation, error) { var accessToken = ctx.Value(cs.CtContextAccessToken).(string) diff --git a/pkg/management/component_test.go b/pkg/management/component_test.go index 5e13e1b5..b08c974c 100644 --- a/pkg/management/component_test.go +++ b/pkg/management/component_test.go @@ -2110,6 +2110,35 @@ func TestDeleteCredentialsForUser(t *testing.T) { }) } +func TestClearUserLoginFailures(t *testing.T) { + var mockCtrl = gomock.NewController(t) + defer mockCtrl.Finish() + var mockKeycloakClient = mock.NewKeycloakClient(mockCtrl) + var mockEventDBModule = mock.NewEventDBModule(mockCtrl) + var mockConfigurationDBModule = mock.NewConfigurationDBModule(mockCtrl) + var logger = log.NewNopLogger() + + var accessToken = "TOKEN==" + var realm = "master" + var userID = "1245-7854-8963" + var allowedTrustIDGroups = []string{"grp1", "grp2"} + var ctx = context.WithValue(context.TODO(), cs.CtContextAccessToken, accessToken) + var component = NewComponent(mockKeycloakClient, mockEventDBModule, mockConfigurationDBModule, allowedTrustIDGroups, logger) + + t.Run("Error occured", func(t *testing.T) { + var expectedError = errors.New("kc error") + mockKeycloakClient.EXPECT().ClearUserLoginFailures(accessToken, realm, userID).Return(expectedError) + var err = component.ClearUserLoginFailures(ctx, realm, userID) + assert.Equal(t, expectedError, err) + }) + t.Run("Success", func(t *testing.T) { + mockKeycloakClient.EXPECT().ClearUserLoginFailures(accessToken, realm, userID).Return(nil) + mockEventDBModule.EXPECT().ReportEvent(ctx, "LOGIN_FAILURE_CLEARED", gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + var err = component.ClearUserLoginFailures(ctx, realm, userID) + assert.Nil(t, err) + }) +} + func TestGetRoles(t *testing.T) { var mockCtrl = gomock.NewController(t) defer mockCtrl.Finish() diff --git a/pkg/management/endpoint.go b/pkg/management/endpoint.go index deb01453..7a40f1c7 100644 --- a/pkg/management/endpoint.go +++ b/pkg/management/endpoint.go @@ -42,6 +42,7 @@ type Endpoints struct { CreateRecoveryCode endpoint.Endpoint GetCredentialsForUser endpoint.Endpoint DeleteCredentialsForUser endpoint.Endpoint + ClearUserLoginFailures endpoint.Endpoint GetRoles endpoint.Endpoint GetRole endpoint.Endpoint @@ -94,6 +95,7 @@ type ManagementComponent interface { CreateRecoveryCode(ctx context.Context, realmName string, userID string) (string, error) GetCredentialsForUser(ctx context.Context, realmName string, userID string) ([]api.CredentialRepresentation, error) DeleteCredentialsForUser(ctx context.Context, realmName string, userID string, credentialID string) error + ClearUserLoginFailures(ctx context.Context, realmName, userID string) error GetRoles(ctx context.Context, realmName string) ([]api.RoleRepresentation, error) GetRole(ctx context.Context, realmName string, roleID string) (api.RoleRepresentation, error) GetClientRoles(ctx context.Context, realmName, idClient string) ([]api.RoleRepresentation, error) @@ -445,6 +447,15 @@ func MakeDeleteCredentialsForUserEndpoint(managementComponent ManagementComponen } } +// MakeClearUserLoginFailures creates an endpoint for ClearUserLoginFailures +func MakeClearUserLoginFailures(managementComponent ManagementComponent) cs.Endpoint { + return func(ctx context.Context, req interface{}) (interface{}, error) { + var m = req.(map[string]string) + + return nil, managementComponent.ClearUserLoginFailures(ctx, m["realm"], m["userID"]) + } +} + // MakeGetRolesEndpoint creates an endpoint for GetRoles func MakeGetRolesEndpoint(managementComponent ManagementComponent) cs.Endpoint { return func(ctx context.Context, req interface{}) (interface{}, error) { diff --git a/pkg/management/endpoint_test.go b/pkg/management/endpoint_test.go index 32321a6a..647c0268 100644 --- a/pkg/management/endpoint_test.go +++ b/pkg/management/endpoint_test.go @@ -771,6 +771,28 @@ func TestDeleteCredentialsForUserEndpoint(t *testing.T) { } } +func TestClearUserLoginFailuresEndpoint(t *testing.T) { + var mockCtrl = gomock.NewController(t) + defer mockCtrl.Finish() + + var mockManagementComponent = mock.NewManagementComponent(mockCtrl) + var e = MakeClearUserLoginFailures(mockManagementComponent) + + // No error - Without param + { + var realm = "master" + var userID = "123-456-789" + var ctx = context.Background() + var req = make(map[string]string) + req["realm"] = realm + req["userID"] = userID + + mockManagementComponent.EXPECT().ClearUserLoginFailures(ctx, realm, userID).Return(nil).Times(1) + var _, err = e(ctx, req) + assert.Nil(t, err) + } +} + func TestGetRolesEndpoint(t *testing.T) { var mockCtrl = gomock.NewController(t) defer mockCtrl.Finish()