KEYCLOAK-13259
This commit is contained in:
parent
9474dd6208
commit
5ddd605ee9
9 changed files with 226 additions and 11 deletions
|
@ -57,7 +57,7 @@ public class JpaUserCredentialStore implements UserCredentialStore {
|
||||||
@Override
|
@Override
|
||||||
public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) {
|
public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) {
|
||||||
CredentialEntity entity = em.find(CredentialEntity.class, cred.getId());
|
CredentialEntity entity = em.find(CredentialEntity.class, cred.getId());
|
||||||
if (entity == null) return;
|
if (!checkCredentialEntity(entity, user)) return;
|
||||||
entity.setCreatedDate(cred.getCreatedDate());
|
entity.setCreatedDate(cred.getCreatedDate());
|
||||||
entity.setUserLabel(cred.getUserLabel());
|
entity.setUserLabel(cred.getUserLabel());
|
||||||
entity.setType(cred.getType());
|
entity.setType(cred.getType());
|
||||||
|
@ -80,7 +80,7 @@ public class JpaUserCredentialStore implements UserCredentialStore {
|
||||||
@Override
|
@Override
|
||||||
public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) {
|
public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) {
|
||||||
CredentialEntity entity = em.find(CredentialEntity.class, id);
|
CredentialEntity entity = em.find(CredentialEntity.class, id);
|
||||||
if (entity == null) return null;
|
if (!checkCredentialEntity(entity, user)) return null;
|
||||||
CredentialModel model = toModel(entity);
|
CredentialModel model = toModel(entity);
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
@ -161,7 +161,7 @@ public class JpaUserCredentialStore implements UserCredentialStore {
|
||||||
|
|
||||||
CredentialEntity removeCredentialEntity(RealmModel realm, UserModel user, String id) {
|
CredentialEntity removeCredentialEntity(RealmModel realm, UserModel user, String id) {
|
||||||
CredentialEntity entity = em.find(CredentialEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
|
CredentialEntity entity = em.find(CredentialEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
|
||||||
if (entity == null) return null;
|
if (!checkCredentialEntity(entity, user)) return null;
|
||||||
|
|
||||||
int currentPriority = entity.getPriority();
|
int currentPriority = entity.getPriority();
|
||||||
|
|
||||||
|
@ -234,4 +234,8 @@ public class JpaUserCredentialStore implements UserCredentialStore {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean checkCredentialEntity(CredentialEntity entity, UserModel user) {
|
||||||
|
return entity != null && entity.getUser() != null && entity.getUser().getId().equals(user.getId());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -568,7 +568,7 @@ public class JpaUserFederatedStorageProvider implements
|
||||||
@Override
|
@Override
|
||||||
public void updateCredential(RealmModel realm, String userId, CredentialModel cred) {
|
public void updateCredential(RealmModel realm, String userId, CredentialModel cred) {
|
||||||
FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, cred.getId());
|
FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, cred.getId());
|
||||||
if (entity == null) return;
|
if (!checkCredentialEntity(entity, userId)) return;
|
||||||
createIndex(realm, userId);
|
createIndex(realm, userId);
|
||||||
entity.setCreatedDate(cred.getCreatedDate());
|
entity.setCreatedDate(cred.getCreatedDate());
|
||||||
entity.setType(cred.getType());
|
entity.setType(cred.getType());
|
||||||
|
@ -605,7 +605,7 @@ public class JpaUserFederatedStorageProvider implements
|
||||||
@Override
|
@Override
|
||||||
public boolean removeStoredCredential(RealmModel realm, String userId, String id) {
|
public boolean removeStoredCredential(RealmModel realm, String userId, String id) {
|
||||||
FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
|
FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
|
||||||
if (entity == null) return false;
|
if (!checkCredentialEntity(entity, userId)) return false;
|
||||||
|
|
||||||
int currentPriority = entity.getPriority();
|
int currentPriority = entity.getPriority();
|
||||||
|
|
||||||
|
@ -625,11 +625,15 @@ public class JpaUserFederatedStorageProvider implements
|
||||||
@Override
|
@Override
|
||||||
public CredentialModel getStoredCredentialById(RealmModel realm, String userId, String id) {
|
public CredentialModel getStoredCredentialById(RealmModel realm, String userId, String id) {
|
||||||
FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, id);
|
FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, id);
|
||||||
if (entity == null) return null;
|
if (!checkCredentialEntity(entity, userId)) return null;
|
||||||
CredentialModel model = toModel(entity);
|
CredentialModel model = toModel(entity);
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean checkCredentialEntity(FederatedUserCredentialEntity entity, String userId) {
|
||||||
|
return entity != null && entity.getUserId() != null && entity.getUserId().equals(userId);
|
||||||
|
}
|
||||||
|
|
||||||
protected CredentialModel toModel(FederatedUserCredentialEntity entity) {
|
protected CredentialModel toModel(FederatedUserCredentialEntity entity) {
|
||||||
CredentialModel model = new CredentialModel();
|
CredentialModel model = new CredentialModel();
|
||||||
model.setId(entity.getId());
|
model.setId(entity.getId());
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.keycloak.services.resources.account;
|
package org.keycloak.services.resources.account;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.authentication.AuthenticatorFactory;
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
|
@ -17,13 +18,16 @@ import org.keycloak.models.*;
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
|
import org.keycloak.services.ErrorResponseException;
|
||||||
import org.keycloak.services.managers.Auth;
|
import org.keycloak.services.managers.Auth;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
import org.keycloak.utils.MediaType;
|
import org.keycloak.utils.MediaType;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.DELETE;
|
import javax.ws.rs.DELETE;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.NotFoundException;
|
||||||
import javax.ws.rs.POST;
|
import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.PUT;
|
import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
|
@ -45,6 +49,8 @@ import static org.keycloak.utils.CredentialHelper.createUserStorageCredentialRep
|
||||||
|
|
||||||
public class AccountCredentialResource {
|
public class AccountCredentialResource {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(AccountCredentialResource.class);
|
||||||
|
|
||||||
public static final String TYPE = "type";
|
public static final String TYPE = "type";
|
||||||
public static final String ENABLED_ONLY = "enabled-only";
|
public static final String ENABLED_ONLY = "enabled-only";
|
||||||
public static final String USER_CREDENTIALS = "user-credentials";
|
public static final String USER_CREDENTIALS = "user-credentials";
|
||||||
|
@ -279,6 +285,10 @@ public class AccountCredentialResource {
|
||||||
@NoCache
|
@NoCache
|
||||||
public void removeCredential(final @PathParam("credentialId") String credentialId) {
|
public void removeCredential(final @PathParam("credentialId") String credentialId) {
|
||||||
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
||||||
|
CredentialModel credential = session.userCredentialManager().getStoredCredentialById(realm, user, credentialId);
|
||||||
|
if (credential == null) {
|
||||||
|
throw new NotFoundException("Credential not found");
|
||||||
|
}
|
||||||
session.userCredentialManager().removeStoredCredential(realm, user, credentialId);
|
session.userCredentialManager().removeStoredCredential(realm, user, credentialId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,14 +297,25 @@ public class AccountCredentialResource {
|
||||||
* Update a user label of specified credential of current user
|
* Update a user label of specified credential of current user
|
||||||
*
|
*
|
||||||
* @param credentialId ID of the credential, which will be updated
|
* @param credentialId ID of the credential, which will be updated
|
||||||
* @param userLabel new user label
|
* @param userLabel new user label as JSON string
|
||||||
*/
|
*/
|
||||||
@PUT
|
@PUT
|
||||||
@Consumes(javax.ws.rs.core.MediaType.TEXT_PLAIN)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@Path("{credentialId}/label")
|
@Path("{credentialId}/label")
|
||||||
|
@NoCache
|
||||||
public void setLabel(final @PathParam("credentialId") String credentialId, String userLabel) {
|
public void setLabel(final @PathParam("credentialId") String credentialId, String userLabel) {
|
||||||
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
||||||
session.userCredentialManager().updateCredentialLabel(realm, user, credentialId, userLabel);
|
CredentialModel credential = session.userCredentialManager().getStoredCredentialById(realm, user, credentialId);
|
||||||
|
if (credential == null) {
|
||||||
|
throw new NotFoundException("Credential not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String label = JsonSerialization.readValue(userLabel, String.class);
|
||||||
|
session.userCredentialManager().updateCredentialLabel(realm, user, credentialId, label);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new ErrorResponseException(ErrorResponse.error(Messages.INVALID_REQUEST, Response.Status.BAD_REQUEST));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is kept here for now and commented.
|
// TODO: This is kept here for now and commented.
|
||||||
|
|
|
@ -662,6 +662,12 @@ public class UserResource {
|
||||||
@NoCache
|
@NoCache
|
||||||
public void removeCredential(final @PathParam("credentialId") String credentialId) {
|
public void removeCredential(final @PathParam("credentialId") String credentialId) {
|
||||||
auth.users().requireManage(user);
|
auth.users().requireManage(user);
|
||||||
|
CredentialModel credential = session.userCredentialManager().getStoredCredentialById(realm, user, credentialId);
|
||||||
|
if (credential == null) {
|
||||||
|
// we do this to make sure somebody can't phish ids
|
||||||
|
if (auth.users().canQuery()) throw new NotFoundException("Credential not found");
|
||||||
|
else throw new ForbiddenException();
|
||||||
|
}
|
||||||
session.userCredentialManager().removeStoredCredential(realm, user, credentialId);
|
session.userCredentialManager().removeStoredCredential(realm, user, credentialId);
|
||||||
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).success();
|
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).success();
|
||||||
}
|
}
|
||||||
|
@ -677,7 +683,7 @@ public class UserResource {
|
||||||
CredentialModel credential = session.userCredentialManager().getStoredCredentialById(realm, user, credentialId);
|
CredentialModel credential = session.userCredentialManager().getStoredCredentialById(realm, user, credentialId);
|
||||||
if (credential == null) {
|
if (credential == null) {
|
||||||
// we do this to make sure somebody can't phish ids
|
// we do this to make sure somebody can't phish ids
|
||||||
if (auth.users().canQuery()) throw new NotFoundException("User not found");
|
if (auth.users().canQuery()) throw new NotFoundException("Credential not found");
|
||||||
else throw new ForbiddenException();
|
else throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
session.userCredentialManager().updateCredentialLabel(realm, user, credentialId, userLabel);
|
session.userCredentialManager().updateCredentialLabel(realm, user, credentialId, userLabel);
|
||||||
|
@ -705,7 +711,7 @@ public class UserResource {
|
||||||
CredentialModel credential = session.userCredentialManager().getStoredCredentialById(realm, user, credentialId);
|
CredentialModel credential = session.userCredentialManager().getStoredCredentialById(realm, user, credentialId);
|
||||||
if (credential == null) {
|
if (credential == null) {
|
||||||
// we do this to make sure somebody can't phish ids
|
// we do this to make sure somebody can't phish ids
|
||||||
if (auth.users().canQuery()) throw new NotFoundException("User not found");
|
if (auth.users().canQuery()) throw new NotFoundException("Credential not found");
|
||||||
else throw new ForbiddenException();
|
else throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
session.userCredentialManager().moveCredentialTo(realm, user, credentialId, newPreviousCredentialId);
|
session.userCredentialManager().moveCredentialTo(realm, user, credentialId, newPreviousCredentialId);
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.admin.client.resource.RealmResource;
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
import org.keycloak.credential.CredentialModel;
|
import org.keycloak.credential.CredentialModel;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
|
@ -33,8 +34,10 @@ import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.credential.OTPCredentialModel;
|
||||||
import org.keycloak.models.utils.TimeBasedOTP;
|
import org.keycloak.models.utils.TimeBasedOTP;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.representations.idm.EventRepresentation;
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
@ -63,6 +66,7 @@ import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.keycloak.testsuite.util.RealmBuilder;
|
import org.keycloak.testsuite.util.RealmBuilder;
|
||||||
import org.keycloak.testsuite.util.UIUtils;
|
import org.keycloak.testsuite.util.UIUtils;
|
||||||
|
import org.keycloak.testsuite.util.URLUtils;
|
||||||
import org.keycloak.testsuite.util.UserBuilder;
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
|
@ -992,6 +996,47 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertFalse(errorPage.isCurrent());
|
assertFalse(errorPage.isCurrent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void removeTotpAsDifferentUser() {
|
||||||
|
UserResource user1 = ApiUtil.findUserByUsernameId(testRealm(), "user-with-one-configured-otp");
|
||||||
|
CredentialRepresentation otpCredential = user1.credentials().stream()
|
||||||
|
.filter(credentialRep -> OTPCredentialModel.TYPE.equals(credentialRep.getType()))
|
||||||
|
.findFirst()
|
||||||
|
.get();
|
||||||
|
|
||||||
|
// Login as evil user (test-user@localhost) and setup TOTP
|
||||||
|
totpPage.open();
|
||||||
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
Assert.assertTrue(totpPage.isCurrent());
|
||||||
|
|
||||||
|
totpPageSetup();
|
||||||
|
|
||||||
|
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()));
|
||||||
|
|
||||||
|
Assert.assertEquals("Mobile authenticator configured.", profilePage.getSuccess());
|
||||||
|
|
||||||
|
String currentStateChecker = driver.findElement(By.id("stateChecker")).getAttribute("value");
|
||||||
|
|
||||||
|
|
||||||
|
// Try to delete TOTP of "user-with-one-configured-otp" by replace ID of the TOTP credential in the request
|
||||||
|
String currentURL = driver.getCurrentUrl();
|
||||||
|
|
||||||
|
String formParameters = "stateChecker=" + currentStateChecker
|
||||||
|
+ "&submitAction=Delete"
|
||||||
|
+ "&credentialId=" + otpCredential.getId();
|
||||||
|
|
||||||
|
URLUtils.sendPOSTRequestWithWebDriver(currentURL, formParameters);
|
||||||
|
|
||||||
|
// Assert credential of "user-with-one-configured-otp" was NOT deleted and is still present for the user
|
||||||
|
Assert.assertTrue(user1.credentials().stream()
|
||||||
|
.anyMatch(credentialRepresentation -> credentialRepresentation.getType().equals(OTPCredentialModel.TYPE)));
|
||||||
|
|
||||||
|
// Remove TOTP for "test-user" and logout
|
||||||
|
totpPage.removeTotp();
|
||||||
|
totpPage.logout();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void changeProfileNoAccess() throws Exception {
|
public void changeProfileNoAccess() throws Exception {
|
||||||
profilePage.open();
|
profilePage.open();
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterF
|
||||||
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
import org.keycloak.credential.CredentialTypeMetadata;
|
import org.keycloak.credential.CredentialTypeMetadata;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
|
@ -394,6 +395,40 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
Assert.assertNull(password.getUserCredentials());
|
Assert.assertNull(password.getUserCredentials());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCRUDCredentialOfDifferentUser() throws IOException {
|
||||||
|
// Get credential ID of the OTP credential of the different user thant currently logged user
|
||||||
|
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "user-with-one-configured-otp");
|
||||||
|
CredentialRepresentation otpCredential = user.credentials().stream()
|
||||||
|
.filter(credentialRep -> OTPCredentialModel.TYPE.equals(credentialRep.getType()))
|
||||||
|
.findFirst()
|
||||||
|
.get();
|
||||||
|
|
||||||
|
// Test that current user can't update the credential, which belongs to the different user
|
||||||
|
SimpleHttp.Response response = SimpleHttp
|
||||||
|
.doPut(getAccountUrl("credentials/" + otpCredential.getId() + "/label"), httpClient)
|
||||||
|
.auth(tokenUtil.getToken())
|
||||||
|
.json("new-label")
|
||||||
|
.asResponse();
|
||||||
|
assertEquals(404, response.getStatus());
|
||||||
|
|
||||||
|
// Test that current user can't delete the credential, which belongs to the different user
|
||||||
|
response = SimpleHttp
|
||||||
|
.doDelete(getAccountUrl("credentials/" + otpCredential.getId()), httpClient)
|
||||||
|
.acceptJson()
|
||||||
|
.auth(tokenUtil.getToken())
|
||||||
|
.asResponse();
|
||||||
|
assertEquals(404, response.getStatus());
|
||||||
|
|
||||||
|
// Assert credential was not updated or removed
|
||||||
|
CredentialRepresentation otpCredentialLoaded = user.credentials().stream()
|
||||||
|
.filter(credentialRep -> OTPCredentialModel.TYPE.equals(credentialRep.getType()))
|
||||||
|
.findFirst()
|
||||||
|
.get();
|
||||||
|
Assert.assertTrue(ObjectUtil.isEqualOrBothNull(otpCredential.getUserLabel(), otpCredentialLoaded.getUserLabel()));
|
||||||
|
}
|
||||||
|
|
||||||
// Send REST request to get all credential containers and credentials of current user
|
// Send REST request to get all credential containers and credentials of current user
|
||||||
private List<AccountCredentialResource.CredentialContainer> getCredentials() throws IOException {
|
private List<AccountCredentialResource.CredentialContainer> getCredentials() throws IOException {
|
||||||
return SimpleHttp.doGet(getAccountUrl("credentials"), httpClient)
|
return SimpleHttp.doGet(getAccountUrl("credentials"), httpClient)
|
||||||
|
|
|
@ -32,14 +32,17 @@ import org.keycloak.admin.client.resource.RealmResource;
|
||||||
import org.keycloak.admin.client.resource.RoleMappingResource;
|
import org.keycloak.admin.client.resource.RoleMappingResource;
|
||||||
import org.keycloak.admin.client.resource.UserResource;
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
import org.keycloak.admin.client.resource.UsersResource;
|
import org.keycloak.admin.client.resource.UsersResource;
|
||||||
|
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.Base64;
|
import org.keycloak.common.util.Base64;
|
||||||
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
import org.keycloak.credential.CredentialModel;
|
import org.keycloak.credential.CredentialModel;
|
||||||
import org.keycloak.events.admin.OperationType;
|
import org.keycloak.events.admin.OperationType;
|
||||||
import org.keycloak.events.admin.ResourceType;
|
import org.keycloak.events.admin.ResourceType;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
import org.keycloak.models.PasswordPolicy;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.credential.OTPCredentialModel;
|
||||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
|
@ -1958,6 +1961,47 @@ public class UserTest extends AbstractAdminTest {
|
||||||
user.resetPassword(credPasswd);
|
user.resetPassword(credPasswd);
|
||||||
Assert.assertEquals(1, user.credentials().size());
|
Assert.assertEquals(1, user.credentials().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCRUDCredentialsOfDifferentUser() {
|
||||||
|
// Get credential ID of the OTP credential of the user1
|
||||||
|
UserResource user1 = ApiUtil.findUserByUsernameId(testRealm(), "user-with-one-configured-otp");
|
||||||
|
CredentialRepresentation otpCredential = user1.credentials().stream()
|
||||||
|
.filter(credentialRep -> OTPCredentialModel.TYPE.equals(credentialRep.getType()))
|
||||||
|
.findFirst()
|
||||||
|
.get();
|
||||||
|
|
||||||
|
// Test that when admin operates on user "user2", he can't update, move or remove credentials of different user "user1"
|
||||||
|
UserResource user2 = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost");
|
||||||
|
try {
|
||||||
|
user2.setCredentialUserLabel(otpCredential.getId(), "new-label");
|
||||||
|
Assert.fail("Not expected to successfully update user label");
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
user2.moveCredentialToFirst(otpCredential.getId());
|
||||||
|
Assert.fail("Not expected to successfully move credential");
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
user2.removeCredential(otpCredential.getId());
|
||||||
|
Assert.fail("Not expected to successfully remove credential");
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert credential was not removed or updated
|
||||||
|
CredentialRepresentation otpCredentialLoaded = user1.credentials().stream()
|
||||||
|
.filter(credentialRep -> OTPCredentialModel.TYPE.equals(credentialRep.getType()))
|
||||||
|
.findFirst()
|
||||||
|
.get();
|
||||||
|
Assert.assertTrue(ObjectUtil.isEqualOrBothNull(otpCredential.getUserLabel(), otpCredentialLoaded.getUserLabel()));
|
||||||
|
Assert.assertTrue(ObjectUtil.isEqualOrBothNull(otpCredential.getPriority(), otpCredentialLoaded.getPriority()));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetGroupsForUserFullRepresentation() {
|
public void testGetGroupsForUserFullRepresentation() {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import org.junit.Test;
|
||||||
import org.keycloak.admin.client.resource.RealmResource;
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
import org.keycloak.admin.client.resource.UserResource;
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
import org.keycloak.common.util.MultivaluedHashMap;
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
import org.keycloak.credential.CredentialAuthentication;
|
import org.keycloak.credential.CredentialAuthentication;
|
||||||
import org.keycloak.credential.CredentialModel;
|
import org.keycloak.credential.CredentialModel;
|
||||||
import org.keycloak.credential.UserCredentialStoreManager;
|
import org.keycloak.credential.UserCredentialStoreManager;
|
||||||
|
@ -22,6 +23,7 @@ import org.keycloak.models.credential.OTPCredentialModel;
|
||||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||||
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.representations.idm.GroupRepresentation;
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.RoleRepresentation;
|
import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
|
@ -76,6 +78,8 @@ import static org.keycloak.storage.UserStorageProviderModel.IMPORT_ENABLED;
|
||||||
import static org.keycloak.storage.UserStorageProviderModel.MAX_LIFESPAN;
|
import static org.keycloak.storage.UserStorageProviderModel.MAX_LIFESPAN;
|
||||||
import static org.keycloak.testsuite.actions.RequiredActionEmailVerificationTest.getPasswordResetEmailLink;
|
import static org.keycloak.testsuite.actions.RequiredActionEmailVerificationTest.getPasswordResetEmailLink;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||||
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
|
|
||||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
|
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
|
||||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
|
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
|
||||||
|
|
||||||
|
@ -960,6 +964,57 @@ public class UserStorageTest extends AbstractAuthTest {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCRUDCredentialsOfDifferentUser() {
|
||||||
|
// Create OTP credential for user1 in the federated storage
|
||||||
|
testingClient.server().run(session -> {
|
||||||
|
RealmModel realm = session.realms().getRealmByName("test");
|
||||||
|
|
||||||
|
UserModel user = session.users().getUserByUsername("thor", realm);
|
||||||
|
Assert.assertFalse(StorageId.isLocalStorage(user));
|
||||||
|
|
||||||
|
CredentialModel otp1 = OTPCredentialModel.createFromPolicy(realm, "secret1");
|
||||||
|
session.userCredentialManager().createCredential(realm, user, otp1);
|
||||||
|
});
|
||||||
|
|
||||||
|
UserResource user1 = ApiUtil.findUserByUsernameId(testRealmResource(), "thor");
|
||||||
|
CredentialRepresentation otpCredential = user1.credentials().stream()
|
||||||
|
.filter(credentialRep -> OTPCredentialModel.TYPE.equals(credentialRep.getType()))
|
||||||
|
.findFirst()
|
||||||
|
.get();
|
||||||
|
|
||||||
|
// Test that when admin operates on user "user2", who is saved in user-storage, he can't update, move or remove credentials of different user "user1"
|
||||||
|
UserResource user2 = ApiUtil.findUserByUsernameId(testRealmResource(), "tbrady");
|
||||||
|
try {
|
||||||
|
user2.setCredentialUserLabel(otpCredential.getId(), "new-label");
|
||||||
|
Assert.fail("Not expected to successfully update user label");
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
user2.moveCredentialToFirst(otpCredential.getId());
|
||||||
|
Assert.fail("Not expected to successfully move credential");
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
user2.removeCredential(otpCredential.getId());
|
||||||
|
Assert.fail("Not expected to successfully remove credential");
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert credential was not removed or updated
|
||||||
|
CredentialRepresentation otpCredentialLoaded = user1.credentials().stream()
|
||||||
|
.filter(credentialRep -> OTPCredentialModel.TYPE.equals(credentialRep.getType()))
|
||||||
|
.findFirst()
|
||||||
|
.get();
|
||||||
|
Assert.assertTrue(ObjectUtil.isEqualOrBothNull(otpCredential.getUserLabel(), otpCredentialLoaded.getUserLabel()));
|
||||||
|
Assert.assertTrue(ObjectUtil.isEqualOrBothNull(otpCredential.getPriority(), otpCredentialLoaded.getPriority()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void assertOrder(List<CredentialModel> creds, String... expectedIds) {
|
private void assertOrder(List<CredentialModel> creds, String... expectedIds) {
|
||||||
org.keycloak.testsuite.Assert.assertEquals(expectedIds.length, creds.size());
|
org.keycloak.testsuite.Assert.assertEquals(expectedIds.length, creds.size());
|
||||||
|
|
|
@ -78,6 +78,7 @@ webauthn-help-text=Use your security key to log in.
|
||||||
webauthn-passwordless-display-name=Security Key
|
webauthn-passwordless-display-name=Security Key
|
||||||
webauthn-passwordless-help-text=Use your security key for passwordless log in.
|
webauthn-passwordless-help-text=Use your security key for passwordless log in.
|
||||||
basic-authentication=Basic Authentication
|
basic-authentication=Basic Authentication
|
||||||
|
invalidRequestMessage=Invalid Request
|
||||||
|
|
||||||
# Applications page
|
# Applications page
|
||||||
applicationsPageTitle=Applications
|
applicationsPageTitle=Applications
|
||||||
|
|
Loading…
Reference in a new issue