KEYCLOAK-953: add allowing user to delete his own account feature
This commit is contained in:
parent
e56bd9d8b8
commit
4f330f4a57
27 changed files with 727 additions and 10 deletions
|
@ -38,6 +38,7 @@ public interface Errors {
|
|||
String USER_TEMPORARILY_DISABLED = "user_temporarily_disabled";
|
||||
String INVALID_USER_CREDENTIALS = "invalid_user_credentials";
|
||||
String DIFFERENT_USER_AUTHENTICATED = "different_user_authenticated";
|
||||
String USER_DELETE_ERROR = "user_delete_error";
|
||||
|
||||
String USERNAME_MISSING = "username_missing";
|
||||
String USERNAME_IN_USE = "username_in_use";
|
||||
|
|
|
@ -132,7 +132,10 @@ public enum EventType {
|
|||
TOKEN_EXCHANGE_ERROR(true),
|
||||
|
||||
PERMISSION_TOKEN(true),
|
||||
PERMISSION_TOKEN_ERROR(false);
|
||||
PERMISSION_TOKEN_ERROR(false),
|
||||
|
||||
DELETE_ACCOUNT(true),
|
||||
DELETE_ACCOUNT_ERROR(true);
|
||||
|
||||
private boolean saveByDefault;
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import java.util.Map;
|
|||
import java.util.regex.Pattern;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.migration.migrators.MigrateTo12_0_0;
|
||||
import org.keycloak.migration.migrators.MigrateTo1_2_0;
|
||||
import org.keycloak.migration.migrators.MigrateTo1_3_0;
|
||||
import org.keycloak.migration.migrators.MigrateTo1_4_0;
|
||||
|
@ -90,7 +91,8 @@ public class MigrationModelManager {
|
|||
new MigrateTo8_0_0(),
|
||||
new MigrateTo8_0_2(),
|
||||
new MigrateTo9_0_0(),
|
||||
new MigrateTo9_0_4()
|
||||
new MigrateTo9_0_4(),
|
||||
new MigrateTo12_0_0()
|
||||
};
|
||||
|
||||
public static void migrate(KeycloakSession session) {
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package org.keycloak.migration.migrators;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.keycloak.migration.ModelVersion;
|
||||
import org.keycloak.models.AccountRoles;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RequiredActionProviderModel;
|
||||
|
||||
public class MigrateTo12_0_0 implements Migration {
|
||||
|
||||
public static final ModelVersion VERSION = new ModelVersion("12.0.0");
|
||||
|
||||
public static final RequiredActionProviderModel deleteAccount = new RequiredActionProviderModel();
|
||||
|
||||
static {
|
||||
deleteAccount.setEnabled(false);
|
||||
deleteAccount.setAlias("delete_account");
|
||||
deleteAccount.setName("Delete Account");
|
||||
deleteAccount.setProviderId("delete_account");
|
||||
deleteAccount.setDefaultAction(false);
|
||||
deleteAccount.setPriority(60);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void migrate(KeycloakSession session) {
|
||||
session.realms()
|
||||
.getRealms()
|
||||
.stream()
|
||||
.map(realm -> realm.getClientByClientId("account"))
|
||||
.filter(client -> Objects.isNull(client.getRole(AccountRoles.DELETE_ACCOUNT)))
|
||||
.forEach(client -> client.addRole(AccountRoles.DELETE_ACCOUNT)
|
||||
.setDescription("${role_"+AccountRoles.DELETE_ACCOUNT+"}"));
|
||||
|
||||
session.realms().getRealms().stream().filter(realm -> Objects.isNull(realm.getRequiredActionProviderByAlias("delete_account"))).forEach(realm -> realm.addRequiredActionProvider(deleteAccount));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelVersion getVersion() {
|
||||
return VERSION;
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ public interface AccountRoles {
|
|||
String VIEW_APPLICATIONS = "view-applications";
|
||||
String VIEW_CONSENT = "view-consent";
|
||||
String MANAGE_CONSENT = "manage-consent";
|
||||
String DELETE_ACCOUNT = "delete-account";
|
||||
|
||||
String[] ALL = {VIEW_PROFILE, MANAGE_ACCOUNT};
|
||||
|
||||
|
|
|
@ -84,6 +84,17 @@ public class DefaultRequiredActions {
|
|||
}
|
||||
|
||||
addUpdateLocaleAction(realm);
|
||||
|
||||
if (realm.getRequiredActionProviderByAlias("delete_account") == null) {
|
||||
RequiredActionProviderModel deleteAccount = new RequiredActionProviderModel();
|
||||
deleteAccount.setEnabled(false);
|
||||
deleteAccount.setAlias("delete_account");
|
||||
deleteAccount.setName("Delete Account");
|
||||
deleteAccount.setProviderId("delete_account");
|
||||
deleteAccount.setDefaultAction(false);
|
||||
deleteAccount.setPriority(60);
|
||||
realm.addRequiredActionProvider(deleteAccount);
|
||||
}
|
||||
}
|
||||
|
||||
public static void addUpdateLocaleAction(RealmModel realm) {
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.authentication.requiredactions;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.authentication.InitiatedActionSupport;
|
||||
import org.keycloak.authentication.RequiredActionContext;
|
||||
import org.keycloak.authentication.RequiredActionFactory;
|
||||
import org.keycloak.authentication.RequiredActionProvider;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.AccountRoles;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserManager;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.ForbiddenException;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
|
||||
public class DeleteAccount implements RequiredActionProvider, RequiredActionFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "delete_account";
|
||||
|
||||
private static final String TRIGGERED_FROM_AIA = "triggered_from_aia";
|
||||
|
||||
private static final Logger logger = Logger.getLogger(DeleteAccount.class);
|
||||
|
||||
@Override
|
||||
public String getDisplayText() {
|
||||
return "Delete Account";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluateTriggers(RequiredActionContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requiredActionChallenge(RequiredActionContext context) {
|
||||
if (!clientHasDeleteAccountRole(context)) {
|
||||
context.challenge(context.form().setError(Messages.DELETE_ACCOUNT_LACK_PRIVILEDGES).createForm("error.ftl"));
|
||||
return;
|
||||
}
|
||||
|
||||
context.challenge(context.form().setAttribute(TRIGGERED_FROM_AIA, isCurrentActionTriggeredFromAIA(context)).createForm("delete-account-confirm.ftl"));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void processAction(RequiredActionContext context) {
|
||||
KeycloakSession session = context.getSession();
|
||||
EventBuilder eventBuilder = context.getEvent();
|
||||
KeycloakContext keycloakContext = session.getContext();
|
||||
RealmModel realm = keycloakContext.getRealm();
|
||||
UserModel user = keycloakContext.getAuthenticationSession().getAuthenticatedUser();
|
||||
|
||||
try {
|
||||
if(!clientHasDeleteAccountRole(context)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
boolean removed = new UserManager(session).removeUser(realm, user);
|
||||
|
||||
if (removed) {
|
||||
eventBuilder.event(EventType.DELETE_ACCOUNT)
|
||||
.client(keycloakContext.getClient())
|
||||
.user(user)
|
||||
.detail(Details.USERNAME, user.getUsername())
|
||||
.success();
|
||||
|
||||
cleanSession(context, RequiredActionContext.KcActionStatus.SUCCESS);
|
||||
|
||||
context.challenge(context.form()
|
||||
.setAttribute("messageHeader", "")
|
||||
.setInfo("userDeletedSuccessfully")
|
||||
.createForm("info.ftl"));
|
||||
} else {
|
||||
eventBuilder.event(EventType.DELETE_ACCOUNT)
|
||||
.client(keycloakContext.getClient())
|
||||
.user(user)
|
||||
.detail(Details.USERNAME, user.getUsername())
|
||||
.error("User could not be deleted");
|
||||
|
||||
cleanSession(context, RequiredActionContext.KcActionStatus.ERROR);
|
||||
context.failure();
|
||||
}
|
||||
|
||||
} catch (ForbiddenException forbidden) {
|
||||
logger.error("account client does not have the required roles for user deletion");
|
||||
eventBuilder.event(EventType.DELETE_ACCOUNT_ERROR)
|
||||
.client(keycloakContext.getClient())
|
||||
.user(keycloakContext.getAuthenticationSession().getAuthenticatedUser())
|
||||
.detail(Details.REASON, "does not have the required roles for user deletion")
|
||||
.error(Errors.USER_DELETE_ERROR);
|
||||
//deletingAccountForbidden
|
||||
context.challenge(context.form().setAttribute(TRIGGERED_FROM_AIA, isCurrentActionTriggeredFromAIA(context)).setError(Messages.DELETE_ACCOUNT_LACK_PRIVILEDGES).createForm("delete-account-confirm.ftl"));
|
||||
} catch (Exception exception) {
|
||||
logger.error("unexpected error happened during account deletion", exception);
|
||||
eventBuilder.event(EventType.DELETE_ACCOUNT_ERROR)
|
||||
.client(keycloakContext.getClient())
|
||||
.user(keycloakContext.getAuthenticationSession().getAuthenticatedUser())
|
||||
.detail(Details.REASON, exception.getMessage())
|
||||
.error(Errors.USER_DELETE_ERROR);
|
||||
context.challenge(context.form().setError(Messages.DELETE_ACCOUNT_ERROR).createForm("delete-account-confirm.ftl"));
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanSession(RequiredActionContext context, RequiredActionContext.KcActionStatus status) {
|
||||
context.getAuthenticationSession().removeRequiredAction(PROVIDER_ID);
|
||||
context.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
|
||||
AuthenticationManager.setKcActionStatus(PROVIDER_ID, status, context.getAuthenticationSession());
|
||||
}
|
||||
|
||||
private boolean clientHasDeleteAccountRole(RequiredActionContext context) {
|
||||
RoleModel deleteAccountRole = context.getRealm().getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getRole(AccountRoles.DELETE_ACCOUNT);
|
||||
return deleteAccountRole != null && context.getUser().hasRole(deleteAccountRole);
|
||||
}
|
||||
|
||||
private boolean isCurrentActionTriggeredFromAIA(RequiredActionContext context) {
|
||||
return Objects.equals(context.getAuthenticationSession().getClientNote(Constants.KC_ACTION), PROVIDER_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequiredActionProvider create(KeycloakSession session) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InitiatedActionSupport initiatedActionSupport() {
|
||||
return InitiatedActionSupport.SUPPORTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOneTimeAction() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxAuthAge() {
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -443,6 +443,9 @@ public class RealmManager {
|
|||
manageConsentRole.setDescription("${role_" + AccountRoles.MANAGE_CONSENT + "}");
|
||||
manageConsentRole.addCompositeRole(viewConsentRole);
|
||||
|
||||
RoleModel deleteOwnAccount = accountClient.addRole(AccountRoles.DELETE_ACCOUNT);
|
||||
deleteOwnAccount.setDescription("${role_"+AccountRoles.DELETE_ACCOUNT+"}");
|
||||
|
||||
ClientModel accountConsoleClient = realm.getClientByClientId(Constants.ACCOUNT_CONSOLE_CLIENT_ID);
|
||||
if (accountConsoleClient == null) {
|
||||
accountConsoleClient = KeycloakModelUtils.createClient(realm, Constants.ACCOUNT_CONSOLE_CLIENT_ID);
|
||||
|
|
|
@ -254,4 +254,7 @@ public class Messages {
|
|||
public static final String WEBAUTHN_ERROR_AUTH_VERIFICATION = "webauthn-error-auth-verification";
|
||||
public static final String WEBAUTHN_ERROR_REGISTER_VERIFICATION = "webauthn-error-register-verification";
|
||||
public static final String WEBAUTHN_ERROR_USER_NOT_FOUND = "webauthn-error-user-not-found";
|
||||
|
||||
public static final String DELETE_ACCOUNT_LACK_PRIVILEDGES = "deletingAccountForbidden";
|
||||
public static final String DELETE_ACCOUNT_ERROR = "errorDeletingAccount";
|
||||
}
|
||||
|
|
|
@ -2,12 +2,15 @@ package org.keycloak.services.resources.account;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.keycloak.authentication.requiredactions.DeleteAccount;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.events.EventStoreProvider;
|
||||
import org.keycloak.models.AccountRoles;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||
import org.keycloak.services.Urls;
|
||||
|
@ -124,11 +127,17 @@ public class AccountConsole {
|
|||
map.put("isAuthorizationEnabled", true);
|
||||
|
||||
boolean isTotpConfigured = false;
|
||||
boolean deleteAccountAllowed = false;
|
||||
if (user != null) {
|
||||
isTotpConfigured = session.userCredentialManager().isConfiguredFor(realm, user, realm.getOTPPolicy().getType());
|
||||
RoleModel deleteAccountRole = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getRole(AccountRoles.DELETE_ACCOUNT);
|
||||
deleteAccountAllowed = deleteAccountRole != null && user.hasRole(deleteAccountRole) && realm.getRequiredActionProviderByAlias(DeleteAccount.PROVIDER_ID).isEnabled();
|
||||
}
|
||||
|
||||
map.put("isTotpConfigured", isTotpConfigured);
|
||||
|
||||
map.put("deleteAccountAllowed", deleteAccountAllowed);
|
||||
|
||||
FreeMarkerUtil freeMarkerUtil = new FreeMarkerUtil();
|
||||
String result = freeMarkerUtil.processTemplate(map, "index.ftl", theme);
|
||||
Response.ResponseBuilder builder = Response.status(Response.Status.OK).type(MediaType.TEXT_HTML_UTF_8).language(Locale.ENGLISH).entity(result);
|
||||
|
|
|
@ -22,4 +22,5 @@ org.keycloak.authentication.requiredactions.VerifyEmail
|
|||
org.keycloak.authentication.requiredactions.TermsAndConditions
|
||||
org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory
|
||||
org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory
|
||||
org.keycloak.authentication.requiredactions.UpdateUserLocaleAction
|
||||
org.keycloak.authentication.requiredactions.UpdateUserLocaleAction
|
||||
org.keycloak.authentication.requiredactions.DeleteAccount
|
|
@ -0,0 +1,45 @@
|
|||
package org.keycloak.testsuite.auth.page.login;
|
||||
|
||||
import org.keycloak.authentication.requiredactions.DeleteAccount;
|
||||
import org.keycloak.testsuite.auth.page.AuthRealm;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
import static org.keycloak.testsuite.util.UIUtils.clickLink;
|
||||
|
||||
public class DeleteAccountActionConfirmPage extends RequiredActions {
|
||||
|
||||
@FindBy(css = "button[name='cancel-aia']")
|
||||
WebElement cancelActionButton;
|
||||
|
||||
@FindBy(css = "input[type='submit']")
|
||||
WebElement confirmActionButton;
|
||||
|
||||
@Override
|
||||
public String getActionId() {
|
||||
return DeleteAccount.PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCurrent() {
|
||||
return driver.getCurrentUrl().contains("login-actions/required-action") && driver.getCurrentUrl().contains("execution=delete_account");
|
||||
}
|
||||
|
||||
|
||||
public void clickCancelAIA() {
|
||||
clickLink(cancelActionButton);
|
||||
}
|
||||
|
||||
public void clickConfirmAction() {
|
||||
clickLink(confirmActionButton);
|
||||
}
|
||||
|
||||
public boolean isErrorMessageDisplayed() {
|
||||
return driver.findElements(By.cssSelector(".alert-error")).size() == 1;
|
||||
}
|
||||
|
||||
public String getErrorMessageText() {
|
||||
return driver.findElement(By.cssSelector("#kc-content-wrapper > div > span.kc-feedback-text")).getText();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package org.keycloak.testsuite.actions;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.authentication.requiredactions.DeleteAccount;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.AccountRoles;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.auth.page.login.DeleteAccountActionConfirmPage;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
|
||||
public class DeleteAccountActionTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
@Page
|
||||
public DeleteAccountActionConfirmPage deleteAccountPage;
|
||||
|
||||
@Page
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Page
|
||||
protected ErrorPage errorPage;
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUpAction() {
|
||||
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
|
||||
UserBuilder.edit(user).requiredAction(DeleteAccount.PROVIDER_ID);
|
||||
testRealm().users().get(user.getId()).update(user);
|
||||
addDeleteAccountRoleToUserClientRoles();
|
||||
|
||||
RequiredActionProviderRepresentation rep = testRealm().flows().getRequiredAction(DeleteAccount.PROVIDER_ID);
|
||||
rep.setEnabled(true);
|
||||
adminClient.realm("test").flows().updateRequiredAction(DeleteAccount.PROVIDER_ID, rep);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteAccountActionSucceeds() {
|
||||
loginPage.open();
|
||||
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
Assert.assertTrue(deleteAccountPage.isCurrent());
|
||||
|
||||
deleteAccountPage.clickConfirmAction();
|
||||
|
||||
events.expect(EventType.DELETE_ACCOUNT);
|
||||
|
||||
List<UserRepresentation> users = testRealm().users().search("test-user@localhost");
|
||||
|
||||
Assert.assertEquals(users.size(), 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteAccountFailsWithoutRoleFails() {
|
||||
removeDeleteAccountRoleFromUserClientRoles();
|
||||
loginPage.open();
|
||||
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
Assert.assertTrue(errorPage.isCurrent());
|
||||
|
||||
Assert.assertEquals(errorPage.getError(), "You do not have enough permissions to delete your own account, contact admin.");
|
||||
}
|
||||
|
||||
|
||||
private void addDeleteAccountRoleToUserClientRoles() {
|
||||
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
|
||||
ApiUtil.assignClientRoles(adminClient.realm("test"), user.getId(), "account", AccountRoles.DELETE_ACCOUNT);
|
||||
}
|
||||
|
||||
private void removeDeleteAccountRoleFromUserClientRoles() {
|
||||
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
|
||||
UserResource userResource = testRealm().users().get(user.getId());
|
||||
ClientRepresentation clientRepresentation = testRealm().clients().findByClientId("account").get(0);
|
||||
String deleteRoleId = userResource.roles().clientLevel(clientRepresentation.getId()).listAll().stream().filter(role -> Objects
|
||||
.equals(role.getName(), "delete-account")).findFirst().get().getId();
|
||||
RoleRepresentation deleteRole = new RoleRepresentation();
|
||||
deleteRole.setName("delete-account");
|
||||
deleteRole.setId(deleteRoleId);
|
||||
userResource.roles().clientLevel(clientRepresentation.getId()).remove(Arrays.asList(deleteRole));
|
||||
}
|
||||
}
|
|
@ -560,8 +560,7 @@ public class ClientTest extends AbstractAdminTest {
|
|||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll(), AccountRoles.VIEW_PROFILE);
|
||||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective(), AccountRoles.VIEW_PROFILE);
|
||||
|
||||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS,
|
||||
AccountRoles.VIEW_APPLICATIONS, AccountRoles.VIEW_CONSENT, AccountRoles.MANAGE_CONSENT);
|
||||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS, AccountRoles.VIEW_APPLICATIONS, AccountRoles.VIEW_CONSENT, AccountRoles.MANAGE_CONSENT, AccountRoles.DELETE_ACCOUNT);
|
||||
|
||||
Assert.assertNames(scopesResource.getAll().getRealmMappings(), "role1");
|
||||
Assert.assertNames(scopesResource.getAll().getClientMappings().get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getMappings(), AccountRoles.VIEW_PROFILE);
|
||||
|
@ -576,8 +575,9 @@ public class ClientTest extends AbstractAdminTest {
|
|||
Assert.assertNames(scopesResource.realmLevel().listEffective());
|
||||
Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "role1", "role2");
|
||||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll());
|
||||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.VIEW_PROFILE, AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS,
|
||||
AccountRoles.VIEW_APPLICATIONS, AccountRoles.VIEW_CONSENT, AccountRoles.MANAGE_CONSENT);
|
||||
|
||||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.VIEW_PROFILE, AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS, AccountRoles.VIEW_APPLICATIONS, AccountRoles.VIEW_CONSENT, AccountRoles.MANAGE_CONSENT, AccountRoles.DELETE_ACCOUNT);
|
||||
|
||||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective());
|
||||
}
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
|
|||
addRequiredAction(expected, "UPDATE_PASSWORD", "Update Password", true, false, null);
|
||||
addRequiredAction(expected, "UPDATE_PROFILE", "Update Profile", true, false, null);
|
||||
addRequiredAction(expected, "VERIFY_EMAIL", "Verify Email", true, false, null);
|
||||
addRequiredAction(expected, "delete_account", "Delete Account", false, false, null);
|
||||
addRequiredAction(expected, "terms_and_conditions", "Terms and Conditions", false, false, null);
|
||||
addRequiredAction(expected, "update_user_locale", "Update User Locale", true, false, null);
|
||||
|
||||
|
|
|
@ -285,6 +285,19 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
|
|||
testUserLocaleActionAdded(migrationRealm);
|
||||
}
|
||||
|
||||
protected void testMigrationTo12_0_0() {
|
||||
testAccountConsoleClientHasDeleteUserRole(masterRealm);
|
||||
testAccountConsoleClientHasDeleteUserRole(migrationRealm);
|
||||
}
|
||||
|
||||
private void testAccountConsoleClientHasDeleteUserRole(RealmResource realm) {
|
||||
ClientRepresentation accountClient = realm.clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0);
|
||||
|
||||
ClientResource accountResource = realm.clients().get(accountClient.getId());
|
||||
RoleRepresentation deleteUserRole = accountResource.roles().get(AccountRoles.DELETE_ACCOUNT).toRepresentation();
|
||||
assertNotNull(deleteUserRole);
|
||||
}
|
||||
|
||||
private void testAccountClient(RealmResource realm) {
|
||||
ClientRepresentation accountClient = realm.clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0);
|
||||
|
||||
|
@ -890,6 +903,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
|
|||
testMigrationTo9_0_0();
|
||||
}
|
||||
|
||||
protected void testMigrationTo12_x() {
|
||||
testMigrationTo12_0_0();
|
||||
}
|
||||
|
||||
protected void testMigrationTo7_x(boolean supportedAuthzServices) {
|
||||
if (supportedAuthzServices) {
|
||||
testDecisionStrategySetOnResourceServer();
|
||||
|
|
|
@ -57,6 +57,12 @@ public class MigrationTest extends AbstractMigrationTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Migration(versionFrom = "12.")
|
||||
public void migration12_xTest() {
|
||||
testMigrationTo12_x();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Migration(versionFrom = "9.")
|
||||
public void migration9_xTest() throws Exception {
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.ui.account2;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.AccountRoles;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.ui.account2.page.AbstractLoggedInPage;
|
||||
import org.keycloak.testsuite.auth.page.login.DeleteAccountActionConfirmPage;
|
||||
import org.keycloak.testsuite.ui.account2.page.PersonalInfoPage;
|
||||
|
||||
import static org.keycloak.testsuite.util.UIUtils.refreshPageAndWaitForLoad;
|
||||
|
||||
/**
|
||||
* @author Zakaria Amine <zakaria.amine88@gmail.com>
|
||||
*/
|
||||
public class DeleteAccountTest extends BaseAccountPageTest {
|
||||
|
||||
@Page
|
||||
private PersonalInfoPage personalInfoPage;
|
||||
|
||||
@Page
|
||||
private DeleteAccountActionConfirmPage deleteAccountActionConfirmPage;
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
@Override
|
||||
protected AbstractLoggedInPage getAccountPage() {
|
||||
return personalInfoPage;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
enableDeleteAccountRequiredAction();
|
||||
addDeleteAccountRoleToUserClientRoles();
|
||||
}
|
||||
|
||||
@After
|
||||
public void clean() {
|
||||
disableDeleteAccountRequiredAction();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteOwnAccountSectionNotVisibleWithoutClientRole() {
|
||||
removeDeleteAccountRoleFromUserClientRoles();
|
||||
refreshPageAndWaitForLoad();
|
||||
personalInfoPage.assertDeleteAccountSectionVisible(false);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void deleteOwnAccountSectionNotVisibleWithoutDeleteAccountActionEnabled() {
|
||||
disableDeleteAccountRequiredAction();
|
||||
refreshPageAndWaitForLoad();
|
||||
personalInfoPage.assertDeleteAccountSectionVisible(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteOwnAccountAIACancellationSucceeds() {
|
||||
refreshPageAndWaitForLoad();
|
||||
personalInfoPage.assertDeleteAccountSectionVisible(true);
|
||||
personalInfoPage.clickOpenDeleteExapandable();
|
||||
personalInfoPage.clickDeleteAccountButton();
|
||||
loginPage.form().login(testUser);
|
||||
Assert.assertTrue(deleteAccountActionConfirmPage.isCurrent());
|
||||
deleteAccountActionConfirmPage.clickCancelAIA();
|
||||
Assert.assertTrue(personalInfoPage.isCurrent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteOwnAccountForbiddenWithoutClientRole() {
|
||||
refreshPageAndWaitForLoad();
|
||||
personalInfoPage.assertDeleteAccountSectionVisible(true);
|
||||
personalInfoPage.clickOpenDeleteExapandable();
|
||||
personalInfoPage.clickDeleteAccountButton();
|
||||
loginPage.form().login(testUser);
|
||||
Assert.assertTrue(deleteAccountActionConfirmPage.isCurrent());
|
||||
removeDeleteAccountRoleFromUserClientRoles();
|
||||
deleteAccountActionConfirmPage.clickConfirmAction();
|
||||
Assert.assertTrue(deleteAccountActionConfirmPage.isErrorMessageDisplayed());
|
||||
Assert.assertEquals(deleteAccountActionConfirmPage.getErrorMessageText(), "You do not have enough permissions to delete your own account, contact admin.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteOwnAccountSucceeds() {
|
||||
personalInfoPage.navigateTo();
|
||||
personalInfoPage.assertDeleteAccountSectionVisible(true);
|
||||
personalInfoPage.clickOpenDeleteExapandable();
|
||||
personalInfoPage.clickDeleteAccountButton();
|
||||
loginPage.form().login(testUser);
|
||||
deleteAccountActionConfirmPage.isCurrent();
|
||||
deleteAccountActionConfirmPage.clickConfirmAction();
|
||||
events.expectAccount(EventType.DELETE_ACCOUNT);
|
||||
Assert.assertTrue(testRealmResource().users().search(testUser.getUsername()).isEmpty());
|
||||
}
|
||||
|
||||
private void addDeleteAccountRoleToUserClientRoles() {
|
||||
ApiUtil.assignClientRoles(testRealmResource(), testUser.getId(), "account",AccountRoles.DELETE_ACCOUNT);
|
||||
}
|
||||
|
||||
private void disableDeleteAccountRequiredAction() {
|
||||
RequiredActionProviderRepresentation deleteAccount = testRealmResource().flows().getRequiredAction("delete_account");
|
||||
deleteAccount.setEnabled(false);
|
||||
testRealmResource().flows().updateRequiredAction("delete_account", deleteAccount);
|
||||
}
|
||||
|
||||
private void enableDeleteAccountRequiredAction() {
|
||||
RequiredActionProviderRepresentation deleteAccount = testRealmResource().flows().getRequiredAction("delete_account");
|
||||
deleteAccount.setEnabled(true);
|
||||
testRealmResource().flows().updateRequiredAction("delete_account", deleteAccount);
|
||||
}
|
||||
|
||||
private void removeDeleteAccountRoleFromUserClientRoles() {
|
||||
ClientRepresentation clientRepresentation = testRealmResource().clients().findByClientId("account").get(0);
|
||||
String deleteRoleId = testUserResource().roles().clientLevel(clientRepresentation.getId()).listAll().stream().filter(role -> Objects.equals(role.getName(), "delete-account")).findFirst().get().getId();
|
||||
RoleRepresentation deleteRole = new RoleRepresentation();
|
||||
deleteRole.setName("delete-account");
|
||||
deleteRole.setId(deleteRoleId);
|
||||
testUserResource().roles().clientLevel(clientRepresentation.getId()).remove(Arrays.asList(deleteRole));
|
||||
}
|
||||
}
|
|
@ -18,13 +18,18 @@
|
|||
package org.keycloak.testsuite.ui.account2.page;
|
||||
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
import org.openqa.selenium.support.ui.Select;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.keycloak.testsuite.util.UIAssert.assertElementDisabled;
|
||||
import static org.keycloak.testsuite.util.UIAssert.assertInputElementValid;
|
||||
import static org.keycloak.testsuite.util.UIUtils.clickLink;
|
||||
import static org.keycloak.testsuite.util.UIUtils.getTextInputValue;
|
||||
import static org.keycloak.testsuite.util.UIUtils.isElementVisible;
|
||||
import static org.keycloak.testsuite.util.UIUtils.setTextInputValue;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
@ -47,6 +52,8 @@ public class PersonalInfoPage extends AbstractLoggedInPage {
|
|||
private WebElement saveBtn;
|
||||
@FindBy(id = "cancel-btn")
|
||||
private WebElement cancelBtn;
|
||||
@FindBy(id = "delete-account")
|
||||
private WebElement deleteAccountSection;
|
||||
|
||||
@Override
|
||||
public String getPageId() {
|
||||
|
@ -109,6 +116,14 @@ public class PersonalInfoPage extends AbstractLoggedInPage {
|
|||
assertElementDisabled(expected, saveBtn);
|
||||
}
|
||||
|
||||
public void assertDeleteAccountSectionVisible(boolean expected) {
|
||||
if (deleteAccountSection == null) {
|
||||
assertFalse(expected);
|
||||
return;
|
||||
}
|
||||
assertEquals(expected, isElementVisible(deleteAccountSection));
|
||||
}
|
||||
|
||||
public void clickSave() {
|
||||
clickSave(true);
|
||||
}
|
||||
|
@ -124,6 +139,14 @@ public class PersonalInfoPage extends AbstractLoggedInPage {
|
|||
cancelBtn.click();
|
||||
}
|
||||
|
||||
public void clickOpenDeleteExapandable() {
|
||||
clickLink(driver.findElement(By.cssSelector(".pf-c-expandable__toggle")));
|
||||
}
|
||||
|
||||
public void clickDeleteAccountButton() {
|
||||
clickLink(driver.findElement(By.id("delete-account-btn")));
|
||||
}
|
||||
|
||||
public void setValues(UserRepresentation user, boolean includeUsername) {
|
||||
if (includeUsername) {setUsername(user.getUsername());}
|
||||
setEmail(user.getEmail());
|
||||
|
|
|
@ -6,6 +6,7 @@ doAdd=Add
|
|||
doSignOut=Sign Out
|
||||
doLogIn=Log In
|
||||
doLink=Link
|
||||
noAccessMessage=Access not allowed
|
||||
|
||||
|
||||
editAccountHtmlTitle=Edit Account
|
||||
|
@ -150,6 +151,12 @@ totpInterval=Interval
|
|||
totpCounter=Counter
|
||||
totpDeviceName=Device Name
|
||||
|
||||
irreversibleAction=This action is irreversible
|
||||
deletingImplies=Deleting your account implies:
|
||||
errasingData=Erasing all your data
|
||||
loggingOutImmediately=Logging you out immediately
|
||||
accountUnusable=Any subsequent use of the application will not be possible with this account
|
||||
|
||||
missingUsernameMessage=Please specify username.
|
||||
missingFirstNameMessage=Please specify first name.
|
||||
invalidEmailMessage=Invalid email address.
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout; section>
|
||||
|
||||
<#if section = "header">
|
||||
${msg("deleteAccountConfirm")}
|
||||
|
||||
<#elseif section = "form">
|
||||
|
||||
<form action="${url.loginAction}" class="form-vertical" method="post">
|
||||
|
||||
<div class="alert alert-warning" style="margin-top:0 !important;margin-bottom:30px !important">
|
||||
<span class="pficon pficon-warning-triangle-o"></span>
|
||||
${msg("irreversibleAction")}
|
||||
</div>
|
||||
|
||||
<p>${msg("deletingImplies")}</p>
|
||||
<ul style="color: #72767b;list-style: disc;list-style-position: inside;">
|
||||
<li>${msg("loggingOutImmediately")}</li>
|
||||
<li>${msg("errasingData")}</li>
|
||||
</ul>
|
||||
|
||||
<p class="delete-account-text">${msg("finalDeletionConfirmation")}</p>
|
||||
|
||||
<div id="kc-form-buttons">
|
||||
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doConfirmDelete")}" />
|
||||
<#if triggered_from_aia>
|
||||
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" style="margin-left: calc(100% - 220px)" type="submit" name="cancel-aia" value="true" />${msg("doCancel")}</button>
|
||||
</#if>
|
||||
</div>
|
||||
</form>
|
||||
</#if>
|
||||
|
||||
</@layout.registrationLayout>
|
|
@ -14,6 +14,9 @@ doClickHere=Click here
|
|||
doImpersonate=Impersonate
|
||||
doTryAgain=Try again
|
||||
doTryAnotherWay=Try Another Way
|
||||
doConfirmDelete=Confirm deletion
|
||||
errorDeletingAccount=Error happened while deleting account
|
||||
deletingAccountForbidden=You do not have enough permissions to delete your own account, contact admin.
|
||||
kerberosNotConfigured=Kerberos Not Configured
|
||||
kerberosNotConfiguredTitle=Kerberos Not Configured
|
||||
bypassKerberosDetail=Either you are not logged in by Kerberos or your browser is not set up for Kerberos login. Please click continue to login in through other means
|
||||
|
@ -379,3 +382,13 @@ webauthn-error-user-not-found=Unknown user authenticated by the Security key.
|
|||
identity-provider-redirector=Connect with another Identity Provider
|
||||
identity-provider-login-label=Or sign in with
|
||||
|
||||
finalDeletionConfirmation=If you delete your account, it cannot be restored. To keep your account, click Cancel.
|
||||
irreversibleAction=This action is irreversible
|
||||
deleteAccountConfirm=Delete account confirmation
|
||||
|
||||
deletingImplies=Deleting your account implies:
|
||||
errasingData=Erasing all your data
|
||||
loggingOutImmediately=Logging you out immediately
|
||||
accountUnusable=Any subsequent use of the application will not be possible with this account
|
||||
userDeletedSuccessfully=User deleted successfully
|
||||
|
||||
|
|
|
@ -45,7 +45,8 @@
|
|||
isLinkedAccountsEnabled : ${realm.identityFederationEnabled?c},
|
||||
isEventsEnabled : ${isEventsEnabled?c},
|
||||
isMyResourcesEnabled : ${(realm.userManagedAccessAllowed && isAuthorizationEnabled)?c},
|
||||
isTotpConfigured : ${isTotpConfigured?c}
|
||||
isTotpConfigured : ${isTotpConfigured?c},
|
||||
deleteAccountAllowed : ${deleteAccountAllowed?c}
|
||||
}
|
||||
|
||||
var availableLocales = [];
|
||||
|
|
|
@ -5,6 +5,7 @@ forbidden=Forbidden
|
|||
needAccessRights=You do not have access rights to this request. Contact your administrator.
|
||||
invalidRoute={0} is not a valid route.
|
||||
actionRequiresIDP=This action requires redirection to your identity provider.
|
||||
actionNotDefined=No Action defined
|
||||
continue=Continue
|
||||
refreshPage=Refresh the page
|
||||
done=Done
|
||||
|
@ -112,3 +113,9 @@ removeModalTitle=Remove Access
|
|||
removeModalMessage=This will remove the currently granted access permission for {0}. You will need to grant access again if you want to use this app.
|
||||
confirmButton=Confirm
|
||||
infoMessage=By clicking 'Remove Access', you will remove granted permissions of this application. This application will no longer use your information.
|
||||
|
||||
#Delete Account page
|
||||
doDelete=Delete
|
||||
deleteAccountSummary=Deleting your account will erase all your data and log you out immediately.
|
||||
deleteAccount=Delete Account
|
||||
deleteAccountWarning=This is irreversible. All your data will be permanently destroyed, and irretrievable.
|
|
@ -1,3 +1,16 @@
|
|||
.brand {
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
width: 150px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
@media (max-width: 320px) {
|
||||
.delete-button {
|
||||
width: 120px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
}
|
|
@ -14,15 +14,18 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { ActionGroup, Button, Form, FormGroup, TextInput } from '@patternfly/react-core';
|
||||
import { ActionGroup, Button, Form, FormGroup, TextInput, Grid, GridItem, Expandable} from '@patternfly/react-core';
|
||||
|
||||
import { HttpResponse, AccountServiceClient } from '../../account-service/account.service';
|
||||
import { HttpResponse } from '../../account-service/account.service';
|
||||
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
|
||||
import { Features } from '../../widgets/features';
|
||||
import { Msg } from '../../widgets/Msg';
|
||||
import { ContentPage } from '../ContentPage';
|
||||
import { ContentAlert } from '../ContentAlert';
|
||||
import { LocaleSelector } from '../../widgets/LocaleSelectors';
|
||||
import { KeycloakContext } from '../../keycloak-service/KeycloakContext';
|
||||
import { KeycloakService } from '../../keycloak-service/keycloak.service';
|
||||
import { AIACommand } from '../../util/AIACommand';
|
||||
|
||||
declare const features: Features;
|
||||
declare const locale: string;
|
||||
|
@ -51,6 +54,7 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
|
|||
context: React.ContextType<typeof AccountServiceContext>;
|
||||
private isRegistrationEmailAsUsername: boolean = features.isRegistrationEmailAsUsername;
|
||||
private isEditUserNameAllowed: boolean = features.isEditUserNameAllowed;
|
||||
private isDeleteAccountAllowed: boolean = features.deleteAccountAllowed;
|
||||
private readonly DEFAULT_STATE: AccountPageState = {
|
||||
errors: {
|
||||
username: '',
|
||||
|
@ -130,6 +134,10 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
|
|||
|
||||
}
|
||||
|
||||
private handleDelete = (keycloak: KeycloakService): void => {
|
||||
new AIACommand(keycloak, "delete_account").execute();
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const fields: FormFields = this.state.formFields;
|
||||
return (
|
||||
|
@ -236,6 +244,29 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
|
|||
</Button>
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
|
||||
{ this.isDeleteAccountAllowed &&
|
||||
<div id="delete-account" style={{marginTop:"30px"}}>
|
||||
<Expandable toggleText="Delete Account">
|
||||
<Grid gutter={"sm"}>
|
||||
<GridItem span={6}>
|
||||
<p>
|
||||
<Msg msgKey="deleteAccountWarning" />
|
||||
</p>
|
||||
</GridItem>
|
||||
<GridItem span={4}>
|
||||
<KeycloakContext.Consumer>
|
||||
{ (keycloak: KeycloakService) => (
|
||||
<Button id="delete-account-btn" variant="danger" onClick={() => this.handleDelete(keycloak)} className="delete-button"><Msg msgKey="doDelete" /></Button>
|
||||
)}
|
||||
</KeycloakContext.Consumer>
|
||||
</GridItem>
|
||||
<GridItem span={2}>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
</Expandable>
|
||||
</div>}
|
||||
</ContentPage>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
isEventsEnabled: boolean;
|
||||
isMyResourcesEnabled: boolean;
|
||||
isTotpConfigured: boolean;
|
||||
deleteAccountAllowed: boolean;
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue