Map storage: Single-use objects (action token)
This commit is contained in:
parent
6dda69a634
commit
0cb3c95ed5
29 changed files with 1049 additions and 15 deletions
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2022 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.models.map.singleUseObject;
|
||||
|
||||
import org.keycloak.models.ActionTokenKeyModel;
|
||||
import org.keycloak.models.ActionTokenValueModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.map.common.AbstractEntity;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
|
||||
*/
|
||||
public abstract class AbstractSingleUseObjectModel<E extends AbstractEntity> implements ActionTokenKeyModel, ActionTokenValueModel {
|
||||
|
||||
protected final KeycloakSession session;
|
||||
protected final E entity;
|
||||
|
||||
public AbstractSingleUseObjectModel(KeycloakSession session, E entity) {
|
||||
Objects.requireNonNull(entity, "entity");
|
||||
|
||||
this.session = session;
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof ActionTokenValueModel)) return false;
|
||||
|
||||
MapSingleUseObjectAdapter that = (MapSingleUseObjectAdapter) o;
|
||||
return Objects.equals(that.entity.getId(), entity.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return entity.getId().hashCode();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2022 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.models.map.singleUseObject;
|
||||
|
||||
import org.keycloak.models.ActionTokenStoreProviderFactory;
|
||||
import org.keycloak.models.ActionTokenValueModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.map.common.AbstractMapProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
|
||||
*/
|
||||
public class MapActionTokenProviderFactory extends AbstractMapProviderFactory<MapSingleUseObjectProvider, MapSingleUseObjectEntity, ActionTokenValueModel>
|
||||
implements ActionTokenStoreProviderFactory<MapSingleUseObjectProvider> {
|
||||
|
||||
public MapActionTokenProviderFactory() {
|
||||
super(ActionTokenValueModel.class, MapSingleUseObjectProvider.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapSingleUseObjectProvider createNew(KeycloakSession session) {
|
||||
return new MapSingleUseObjectProvider(session, getStorage(session));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Action token provider";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2022 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.models.map.singleUseObject;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.map.common.TimeAdapter;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
|
||||
*/
|
||||
public class MapSingleUseObjectAdapter extends AbstractSingleUseObjectModel<MapSingleUseObjectEntity> {
|
||||
|
||||
public MapSingleUseObjectAdapter(KeycloakSession session, MapSingleUseObjectEntity entity) {
|
||||
super(session, entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserId() {
|
||||
return entity.getUserId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActionId() {
|
||||
return entity.getActionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getExpiration() {
|
||||
Long expiration = entity.getExpiration();
|
||||
return expiration != null ? TimeAdapter.fromLongWithTimeInSecondsToIntegerWithTimeInSeconds(expiration) : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getActionVerificationNonce() {
|
||||
String actionVerificationNonce = entity.getActionVerificationNonce();
|
||||
return actionVerificationNonce != null ? UUID.fromString(actionVerificationNonce) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getNotes() {
|
||||
Map<String, String> notes = entity.getNotes();
|
||||
return notes == null ? Collections.emptyMap() : Collections.unmodifiableMap(notes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNote(String name) {
|
||||
Map<String, String> notes = entity.getNotes();
|
||||
return notes == null ? null : notes.get(name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright 2022 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.models.map.singleUseObject;
|
||||
|
||||
import org.keycloak.models.map.annotations.GenerateEntityImplementations;
|
||||
import org.keycloak.models.map.common.AbstractEntity;
|
||||
import org.keycloak.models.map.common.DeepCloner;
|
||||
import org.keycloak.models.map.common.ExpirableEntity;
|
||||
import org.keycloak.models.map.common.UpdatableEntity;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
|
||||
*/
|
||||
@GenerateEntityImplementations(
|
||||
inherits = "org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity.AbstractSingleUseObjectEntity"
|
||||
)
|
||||
@DeepCloner.Root
|
||||
public interface MapSingleUseObjectEntity extends AbstractEntity, UpdatableEntity, ExpirableEntity {
|
||||
|
||||
public abstract class AbstractSingleUseObjectEntity extends UpdatableEntity.Impl implements MapSingleUseObjectEntity {
|
||||
|
||||
private String id;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setId(String id) {
|
||||
if (this.id != null) throw new IllegalStateException("Id cannot be changed");
|
||||
this.id = id;
|
||||
this.updated |= id != null;
|
||||
}
|
||||
}
|
||||
|
||||
String getUserId();
|
||||
void setUserId(String userId);
|
||||
|
||||
String getActionId();
|
||||
void setActionId(String actionId);
|
||||
|
||||
String getActionVerificationNonce();
|
||||
void setActionVerificationNonce(String actionVerificationNonce);
|
||||
|
||||
Map<String, String> getNotes();
|
||||
void setNotes(Map<String, String> notes);
|
||||
String getNote(String name);
|
||||
void setNote(String key, String value);
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* Copyright 2022 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.models.map.singleUseObject;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.ActionTokenKeyModel;
|
||||
import org.keycloak.models.ActionTokenStoreProvider;
|
||||
import org.keycloak.models.ActionTokenValueModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.SingleUseObjectProvider;
|
||||
import org.keycloak.models.map.common.TimeAdapter;
|
||||
import org.keycloak.models.map.storage.MapKeycloakTransaction;
|
||||
import org.keycloak.models.map.storage.MapStorage;
|
||||
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
|
||||
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
|
||||
import static org.keycloak.models.map.storage.QueryParameters.withCriteria;
|
||||
import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
|
||||
*/
|
||||
public class MapSingleUseObjectProvider implements ActionTokenStoreProvider, SingleUseObjectProvider {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(MapSingleUseObjectProvider.class);
|
||||
private final KeycloakSession session;
|
||||
protected final MapKeycloakTransaction<MapSingleUseObjectEntity, ActionTokenValueModel> actionTokenStoreTx;
|
||||
|
||||
public MapSingleUseObjectProvider(KeycloakSession session, MapStorage<MapSingleUseObjectEntity, ActionTokenValueModel> storage) {
|
||||
this.session = session;
|
||||
actionTokenStoreTx = storage.createTransaction(session);
|
||||
|
||||
session.getTransactionManager().enlistAfterCompletion(actionTokenStoreTx);
|
||||
}
|
||||
|
||||
private ActionTokenValueModel singleUseEntityToAdapter(MapSingleUseObjectEntity origEntity) {
|
||||
long expiration = origEntity.getExpiration() != null ? origEntity.getExpiration() : 0L;
|
||||
if (Time.currentTime() < expiration) {
|
||||
return new MapSingleUseObjectAdapter(session, origEntity);
|
||||
} else {
|
||||
actionTokenStoreTx.delete(origEntity.getId());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(ActionTokenKeyModel actionTokenKey, Map<String, String> notes) {
|
||||
if (actionTokenKey == null || actionTokenKey.getUserId() == null || actionTokenKey.getActionId() == null || actionTokenKey.getActionVerificationNonce() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.tracef("put(%s, %s, %s)%s", actionTokenKey.getUserId(), actionTokenKey.getActionId(), actionTokenKey.getActionVerificationNonce(), getShortStackTrace());
|
||||
|
||||
DefaultModelCriteria<ActionTokenValueModel> mcb = criteria();
|
||||
mcb = mcb.compare(ActionTokenValueModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, actionTokenKey.getUserId())
|
||||
.compare(ActionTokenValueModel.SearchableFields.ACTION_ID, ModelCriteriaBuilder.Operator.EQ, actionTokenKey.getActionId())
|
||||
.compare(ActionTokenValueModel.SearchableFields.ACTION_VERIFICATION_NONCE, ModelCriteriaBuilder.Operator.EQ, actionTokenKey.getActionVerificationNonce().toString());
|
||||
|
||||
ActionTokenValueModel existing = actionTokenStoreTx.read(withCriteria(mcb))
|
||||
.findFirst().map(this::singleUseEntityToAdapter).orElse(null);
|
||||
|
||||
if (existing == null) {
|
||||
MapSingleUseObjectEntity actionTokenStoreEntity = new MapSingleUseObjectEntityImpl();
|
||||
actionTokenStoreEntity.setUserId(actionTokenKey.getUserId());
|
||||
actionTokenStoreEntity.setActionId(actionTokenKey.getActionId());
|
||||
actionTokenStoreEntity.setActionVerificationNonce(actionTokenKey.getActionVerificationNonce().toString());
|
||||
actionTokenStoreEntity.setExpiration(TimeAdapter.fromIntegerWithTimeInSecondsToLongWithTimeAsInSeconds(actionTokenKey.getExpiration()));
|
||||
actionTokenStoreEntity.setNotes(notes);
|
||||
|
||||
LOG.debugf("Adding used action token to actionTokens cache: %s", actionTokenKey.toString());
|
||||
|
||||
actionTokenStoreTx.create(actionTokenStoreEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionTokenValueModel get(ActionTokenKeyModel key) {
|
||||
if (key == null || key.getUserId() == null || key.getActionId() == null || key.getActionVerificationNonce() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LOG.tracef("get(%s, %s, %s)%s", key.getUserId(), key.getActionId(), key.getActionVerificationNonce(), getShortStackTrace());
|
||||
|
||||
DefaultModelCriteria<ActionTokenValueModel> mcb = criteria();
|
||||
mcb = mcb.compare(ActionTokenValueModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, key.getUserId())
|
||||
.compare(ActionTokenValueModel.SearchableFields.ACTION_ID, ModelCriteriaBuilder.Operator.EQ, key.getActionId())
|
||||
.compare(ActionTokenValueModel.SearchableFields.ACTION_VERIFICATION_NONCE, ModelCriteriaBuilder.Operator.EQ, key.getActionVerificationNonce().toString());
|
||||
|
||||
return actionTokenStoreTx.read(withCriteria(mcb))
|
||||
.findFirst().map(this::singleUseEntityToAdapter).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionTokenValueModel remove(ActionTokenKeyModel key) {
|
||||
if (key == null || key.getUserId() == null || key.getActionId() == null || key.getActionVerificationNonce() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LOG.tracef("remove(%s, %s, %s)%s", key.getUserId(), key.getActionId(), key.getActionVerificationNonce(), getShortStackTrace());
|
||||
|
||||
DefaultModelCriteria<ActionTokenValueModel> mcb = criteria();
|
||||
mcb = mcb.compare(ActionTokenValueModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, key.getUserId())
|
||||
.compare(ActionTokenValueModel.SearchableFields.ACTION_ID, ModelCriteriaBuilder.Operator.EQ, key.getActionId())
|
||||
.compare(ActionTokenValueModel.SearchableFields.ACTION_VERIFICATION_NONCE, ModelCriteriaBuilder.Operator.EQ, key.getActionVerificationNonce().toString());
|
||||
|
||||
MapSingleUseObjectEntity mapSingleUseObjectEntity = actionTokenStoreTx.read(withCriteria(mcb)).findFirst().orElse(null);
|
||||
if (mapSingleUseObjectEntity != null) {
|
||||
ActionTokenValueModel actionToken = singleUseEntityToAdapter(mapSingleUseObjectEntity);
|
||||
if (actionToken != null) {
|
||||
actionTokenStoreTx.delete(mapSingleUseObjectEntity.getId());
|
||||
return actionToken;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String key, long lifespanSeconds, Map<String, String> notes) {
|
||||
LOG.tracef("put(%s)%s", key, getShortStackTrace());
|
||||
|
||||
MapSingleUseObjectEntity singleUseEntity = getWithExpiration(key);
|
||||
|
||||
if (singleUseEntity != null) {
|
||||
throw new ModelDuplicateException("Single-use object entity exists: " + singleUseEntity.getId());
|
||||
}
|
||||
|
||||
singleUseEntity = new MapSingleUseObjectEntityImpl();
|
||||
singleUseEntity.setId(key);
|
||||
singleUseEntity.setExpiration((long) Time.currentTime() + lifespanSeconds);
|
||||
singleUseEntity.setNotes(notes);
|
||||
|
||||
actionTokenStoreTx.create(singleUseEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> get(String key) {
|
||||
LOG.tracef("get(%s)%s", key, getShortStackTrace());
|
||||
|
||||
MapSingleUseObjectEntity actionToken = getWithExpiration(key);
|
||||
if (actionToken != null) {
|
||||
Map<String, String> notes = actionToken.getNotes();
|
||||
return notes == null ? Collections.emptyMap() : Collections.unmodifiableMap(notes);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> remove(String key) {
|
||||
LOG.tracef("remove(%s)%s", key, getShortStackTrace());
|
||||
|
||||
MapSingleUseObjectEntity singleUseEntity = getWithExpiration(key);
|
||||
|
||||
if (singleUseEntity != null) {
|
||||
Map<String, String> notes = singleUseEntity.getNotes();
|
||||
if (actionTokenStoreTx.delete(key)) {
|
||||
return notes == null ? Collections.emptyMap() : Collections.unmodifiableMap(notes);
|
||||
}
|
||||
}
|
||||
// the single-use entity expired or someone else already used and deleted it
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean replace(String key, Map<String, String> notes) {
|
||||
LOG.tracef("replace(%s)%s", key, getShortStackTrace());
|
||||
|
||||
MapSingleUseObjectEntity singleUseEntity = getWithExpiration(key);
|
||||
if (singleUseEntity != null) {
|
||||
singleUseEntity.setNotes(notes);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean putIfAbsent(String key, long lifespanInSeconds) {
|
||||
LOG.tracef("putIfAbsent(%s)%s", key, getShortStackTrace());
|
||||
|
||||
MapSingleUseObjectEntity singleUseEntity = getWithExpiration(key);
|
||||
if (singleUseEntity != null) {
|
||||
return false;
|
||||
} else {
|
||||
singleUseEntity = new MapSingleUseObjectEntityImpl();
|
||||
singleUseEntity.setId(key);
|
||||
singleUseEntity.setExpiration((long) Time.currentTime() + lifespanInSeconds);
|
||||
|
||||
actionTokenStoreTx.create(singleUseEntity);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String key) {
|
||||
LOG.tracef("contains(%s)%s", key, getShortStackTrace());
|
||||
|
||||
MapSingleUseObjectEntity actionToken = getWithExpiration(key);
|
||||
|
||||
return actionToken != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
private MapSingleUseObjectEntity getWithExpiration(String key) {
|
||||
MapSingleUseObjectEntity singleUseEntity = actionTokenStoreTx.read(key);
|
||||
if (singleUseEntity != null) {
|
||||
long expiration = singleUseEntity.getExpiration() != null ? singleUseEntity.getExpiration() : 0L;
|
||||
if (Time.currentTime() < expiration) {
|
||||
return singleUseEntity;
|
||||
}
|
||||
actionTokenStoreTx.delete(key);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2022 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.models.map.singleUseObject;
|
||||
|
||||
import org.keycloak.models.ActionTokenValueModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.SingleUseObjectProviderFactory;
|
||||
import org.keycloak.models.map.common.AbstractMapProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
|
||||
*/
|
||||
public class MapSingleUseObjectProviderFactory extends AbstractMapProviderFactory<MapSingleUseObjectProvider, MapSingleUseObjectEntity, ActionTokenValueModel>
|
||||
implements SingleUseObjectProviderFactory<MapSingleUseObjectProvider> {
|
||||
|
||||
public MapSingleUseObjectProviderFactory() {
|
||||
super(ActionTokenValueModel.class, MapSingleUseObjectProvider.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapSingleUseObjectProvider createNew(KeycloakSession session) {
|
||||
return new MapSingleUseObjectProvider(session, getStorage(session));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Single use object provider";
|
||||
}
|
||||
|
||||
}
|
|
@ -22,6 +22,7 @@ import org.keycloak.authorization.model.Resource;
|
|||
import org.keycloak.authorization.model.ResourceServer;
|
||||
import org.keycloak.events.Event;
|
||||
import org.keycloak.events.admin.AdminEvent;
|
||||
import org.keycloak.models.ActionTokenValueModel;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
|
@ -31,6 +32,7 @@ import org.keycloak.models.RoleModel;
|
|||
import org.keycloak.models.UserLoginFailureModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity;
|
||||
import org.keycloak.models.map.authSession.MapRootAuthenticationSessionEntity;
|
||||
import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity;
|
||||
import org.keycloak.models.map.authorization.entity.MapPolicyEntity;
|
||||
|
@ -64,6 +66,7 @@ public class ModelEntityUtil {
|
|||
|
||||
private static final Map<Class<?>, String> MODEL_TO_NAME = new HashMap<>();
|
||||
static {
|
||||
MODEL_TO_NAME.put(ActionTokenValueModel.class, "single-use-objects");
|
||||
MODEL_TO_NAME.put(AuthenticatedClientSessionModel.class, "client-sessions");
|
||||
MODEL_TO_NAME.put(ClientScopeModel.class, "client-scopes");
|
||||
MODEL_TO_NAME.put(ClientModel.class, "clients");
|
||||
|
@ -90,6 +93,7 @@ public class ModelEntityUtil {
|
|||
|
||||
private static final Map<Class<?>, Class<? extends AbstractEntity>> MODEL_TO_ENTITY_TYPE = new HashMap<>();
|
||||
static {
|
||||
MODEL_TO_ENTITY_TYPE.put(ActionTokenValueModel.class, MapSingleUseObjectEntity.class);
|
||||
MODEL_TO_ENTITY_TYPE.put(AuthenticatedClientSessionModel.class, MapAuthenticatedClientSessionEntity.class);
|
||||
MODEL_TO_ENTITY_TYPE.put(ClientScopeModel.class, MapClientScopeEntity.class);
|
||||
MODEL_TO_ENTITY_TYPE.put(ClientModel.class, MapClientEntity.class);
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.keycloak.models.map.storage.chm;
|
||||
|
||||
import org.keycloak.models.ActionTokenValueModel;
|
||||
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity;
|
||||
import org.keycloak.models.map.authSession.MapAuthenticationSessionEntity;
|
||||
import org.keycloak.models.map.authSession.MapAuthenticationSessionEntityImpl;
|
||||
import org.keycloak.models.map.authSession.MapRootAuthenticationSessionEntity;
|
||||
|
@ -64,6 +66,7 @@ import java.util.EnumSet;
|
|||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntityImpl;
|
||||
import org.keycloak.models.map.storage.MapStorageProvider;
|
||||
import org.keycloak.models.map.storage.MapStorageProviderFactory;
|
||||
import org.keycloak.models.map.user.MapUserConsentEntityImpl;
|
||||
|
@ -143,6 +146,7 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide
|
|||
.constructor(MapAuthenticatedClientSessionEntity.class, MapAuthenticatedClientSessionEntityImpl::new)
|
||||
.constructor(MapAuthEventEntity.class, MapAuthEventEntityImpl::new)
|
||||
.constructor(MapAdminEventEntity.class, MapAdminEventEntityImpl::new)
|
||||
.constructor(MapSingleUseObjectEntity.class, MapSingleUseObjectEntityImpl::new)
|
||||
.build();
|
||||
|
||||
private static final Map<String, StringKeyConverter> KEY_CONVERTERS = new HashMap<>();
|
||||
|
@ -244,6 +248,13 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide
|
|||
return "ConcurrentHashMapStorage(" + mapName + suffix + ")";
|
||||
}
|
||||
};
|
||||
} else if(modelType == ActionTokenValueModel.class) {
|
||||
store = new SingleUseObjectConcurrentHashMapStorage(kc, CLONER) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ConcurrentHashMapStorage(" + mapName + suffix + ")";
|
||||
}
|
||||
};
|
||||
} else {
|
||||
store = new ConcurrentHashMapStorage(modelType, kc, CLONER) {
|
||||
@Override
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.keycloak.authorization.model.ResourceServer;
|
|||
import org.keycloak.authorization.model.Scope;
|
||||
import org.keycloak.events.Event;
|
||||
import org.keycloak.events.admin.AdminEvent;
|
||||
import org.keycloak.models.ActionTokenValueModel;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
|
@ -47,6 +48,7 @@ import org.keycloak.models.map.group.MapGroupEntity;
|
|||
import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntity;
|
||||
import org.keycloak.models.map.realm.MapRealmEntity;
|
||||
import org.keycloak.models.map.role.MapRoleEntity;
|
||||
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity;
|
||||
import org.keycloak.models.map.storage.QueryParameters;
|
||||
import org.keycloak.models.map.user.MapUserConsentEntity;
|
||||
import org.keycloak.storage.SearchableModelField;
|
||||
|
@ -98,6 +100,7 @@ public class MapFieldPredicates {
|
|||
public static final Map<SearchableModelField<UserSessionModel>, UpdatePredicatesFunc<Object, MapUserSessionEntity, UserSessionModel>> USER_SESSION_PREDICATES = basePredicates(UserSessionModel.SearchableFields.ID);
|
||||
public static final Map<SearchableModelField<Event>, UpdatePredicatesFunc<Object, MapAuthEventEntity, Event>> AUTH_EVENTS_PREDICATES = basePredicates(Event.SearchableFields.ID);
|
||||
public static final Map<SearchableModelField<AdminEvent>, UpdatePredicatesFunc<Object, MapAdminEventEntity, AdminEvent>> ADMIN_EVENTS_PREDICATES = basePredicates(AdminEvent.SearchableFields.ID);
|
||||
public static final Map<SearchableModelField<ActionTokenValueModel>, UpdatePredicatesFunc<Object, MapSingleUseObjectEntity, ActionTokenValueModel>> ACTION_TOKEN_PREDICATES = basePredicates(ActionTokenValueModel.SearchableFields.ID);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final Map<Class<?>, Map> PREDICATES = new HashMap<>();
|
||||
|
@ -226,6 +229,10 @@ public class MapFieldPredicates {
|
|||
put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.OPERATION_TYPE, MapAdminEventEntity::getOperationType);
|
||||
put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.RESOURCE_TYPE, MapAdminEventEntity::getResourceType);
|
||||
put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.RESOURCE_PATH, MapAdminEventEntity::getResourcePath);
|
||||
|
||||
put(ACTION_TOKEN_PREDICATES, ActionTokenValueModel.SearchableFields.USER_ID, MapSingleUseObjectEntity::getUserId);
|
||||
put(ACTION_TOKEN_PREDICATES, ActionTokenValueModel.SearchableFields.ACTION_ID, MapSingleUseObjectEntity::getActionId);
|
||||
put(ACTION_TOKEN_PREDICATES, ActionTokenValueModel.SearchableFields.ACTION_VERIFICATION_NONCE, MapSingleUseObjectEntity::getActionVerificationNonce);
|
||||
}
|
||||
|
||||
static {
|
||||
|
@ -246,6 +253,7 @@ public class MapFieldPredicates {
|
|||
PREDICATES.put(UserLoginFailureModel.class, USER_LOGIN_FAILURE_PREDICATES);
|
||||
PREDICATES.put(Event.class, AUTH_EVENTS_PREDICATES);
|
||||
PREDICATES.put(AdminEvent.class, ADMIN_EVENTS_PREDICATES);
|
||||
PREDICATES.put(ActionTokenValueModel.class, ACTION_TOKEN_PREDICATES);
|
||||
}
|
||||
|
||||
private static <K, V extends AbstractEntity, M, L extends Comparable<L>> void put(
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright 2022 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.models.map.storage.chm;
|
||||
|
||||
import org.keycloak.models.ActionTokenValueModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.map.common.AbstractEntity;
|
||||
import org.keycloak.models.map.common.DeepCloner;
|
||||
import org.keycloak.models.map.common.StringKeyConverter;
|
||||
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity;
|
||||
import org.keycloak.models.map.storage.MapKeycloakTransaction;
|
||||
import org.keycloak.models.map.storage.QueryParameters;
|
||||
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
|
||||
import org.keycloak.storage.SearchableModelField;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
|
||||
*/
|
||||
public class SingleUseObjectConcurrentHashMapStorage<K, V extends AbstractEntity, M> extends ConcurrentHashMapStorage<K, MapSingleUseObjectEntity, ActionTokenValueModel> {
|
||||
|
||||
public SingleUseObjectConcurrentHashMapStorage(StringKeyConverter<K> keyConverter, DeepCloner cloner) {
|
||||
super(ActionTokenValueModel.class, keyConverter, cloner);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public MapKeycloakTransaction<MapSingleUseObjectEntity, ActionTokenValueModel> createTransaction(KeycloakSession session) {
|
||||
MapKeycloakTransaction<MapSingleUseObjectEntity, ActionTokenValueModel> actionTokenTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class);
|
||||
return actionTokenTransaction == null ? new SingleUseObjectConcurrentHashMapStorage.Transaction(getKeyConverter(), cloner, fieldPredicates) : actionTokenTransaction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapSingleUseObjectEntity create(MapSingleUseObjectEntity value) {
|
||||
if (value.getId() == null) {
|
||||
if (value.getUserId() != null && value.getActionId() != null && value.getActionVerificationNonce() != null) {
|
||||
value.setId(value.getUserId() + ":" + value.getActionId() + ":" + value.getActionVerificationNonce());
|
||||
}
|
||||
}
|
||||
return super.create(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<MapSingleUseObjectEntity> read(QueryParameters<ActionTokenValueModel> queryParameters) {
|
||||
DefaultModelCriteria<ActionTokenValueModel> criteria = queryParameters.getModelCriteriaBuilder();
|
||||
|
||||
if (criteria == null) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
SingleUseObjectModelCriteriaBuilder mcb = criteria.flashToModelCriteriaBuilder(createSingleUseObjectCriteriaBuilder());
|
||||
if (mcb.isValid()) {
|
||||
MapSingleUseObjectEntity value = read(mcb.getKey());
|
||||
return value != null ? Stream.of(value) : Stream.empty();
|
||||
}
|
||||
|
||||
return super.read(queryParameters);
|
||||
}
|
||||
|
||||
private SingleUseObjectModelCriteriaBuilder createSingleUseObjectCriteriaBuilder() {
|
||||
return new SingleUseObjectModelCriteriaBuilder();
|
||||
}
|
||||
|
||||
private class Transaction extends ConcurrentHashMapKeycloakTransaction<K, MapSingleUseObjectEntity, ActionTokenValueModel> {
|
||||
|
||||
public Transaction(StringKeyConverter<K> keyConverter, DeepCloner cloner,
|
||||
Map<SearchableModelField<? super ActionTokenValueModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, MapSingleUseObjectEntity, ActionTokenValueModel>> fieldPredicates) {
|
||||
super(SingleUseObjectConcurrentHashMapStorage.this, keyConverter, cloner, fieldPredicates);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapSingleUseObjectEntity create(MapSingleUseObjectEntity value) {
|
||||
if (value.getId() == null) {
|
||||
if (value.getUserId() != null && value.getActionId() != null && value.getActionVerificationNonce() != null) {
|
||||
value.setId(value.getUserId() + ":" + value.getActionId() + ":" + value.getActionVerificationNonce());
|
||||
}
|
||||
}
|
||||
return super.create(value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright 2022 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.models.map.storage.chm;
|
||||
|
||||
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
|
||||
import org.keycloak.storage.SearchableModelField;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
|
||||
*/
|
||||
public class SingleUseObjectModelCriteriaBuilder implements ModelCriteriaBuilder {
|
||||
|
||||
private String userId;
|
||||
|
||||
private String actionId;
|
||||
|
||||
private String actionVerificationNonce;
|
||||
|
||||
public SingleUseObjectModelCriteriaBuilder() {
|
||||
}
|
||||
|
||||
public SingleUseObjectModelCriteriaBuilder(String userId, String actionId, String actionVerificationNonce) {
|
||||
this.userId = userId;
|
||||
this.actionId = actionId;
|
||||
this.actionVerificationNonce = actionVerificationNonce;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelCriteriaBuilder compare(SearchableModelField modelField, Operator op, Object... value) {
|
||||
if (modelField == org.keycloak.models.ActionTokenValueModel.SearchableFields.USER_ID) {
|
||||
userId = value[0].toString();
|
||||
} else if (modelField == org.keycloak.models.ActionTokenValueModel.SearchableFields.ACTION_ID) {
|
||||
actionId = value[0].toString();
|
||||
} else if (modelField == org.keycloak.models.ActionTokenValueModel.SearchableFields.ACTION_VERIFICATION_NONCE) {
|
||||
actionVerificationNonce = value[0].toString();
|
||||
}
|
||||
return new SingleUseObjectModelCriteriaBuilder(userId, actionId, actionVerificationNonce);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelCriteriaBuilder and(ModelCriteriaBuilder[] builders) {
|
||||
String userId = null;
|
||||
String actionId = null;
|
||||
String actionVerificationNonce = null;
|
||||
|
||||
for (ModelCriteriaBuilder builder: builders) {
|
||||
SingleUseObjectModelCriteriaBuilder suoMcb = (SingleUseObjectModelCriteriaBuilder) builder;
|
||||
if (suoMcb.userId != null) {
|
||||
userId = suoMcb.userId;
|
||||
}
|
||||
if (suoMcb.actionId != null) {
|
||||
actionId = suoMcb.actionId;
|
||||
}
|
||||
if (suoMcb.actionVerificationNonce != null) {
|
||||
actionVerificationNonce = suoMcb.actionVerificationNonce;
|
||||
}
|
||||
}
|
||||
return new SingleUseObjectModelCriteriaBuilder(userId, actionId, actionVerificationNonce);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelCriteriaBuilder or(ModelCriteriaBuilder[] builders) {
|
||||
throw new IllegalStateException("SingleUseObjectModelCriteriaBuilder does not support OR operation.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelCriteriaBuilder not(ModelCriteriaBuilder builder) {
|
||||
throw new IllegalStateException("SingleUseObjectModelCriteriaBuilder does not support NOT operation.");
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return userId != null && actionId != null && actionVerificationNonce != null;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return userId + ":" + actionId + ":" + actionVerificationNonce;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
#
|
||||
# Copyright 2022 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.
|
||||
#
|
||||
|
||||
org.keycloak.models.map.singleUseObject.MapActionTokenProviderFactory
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#
|
||||
# Copyright 2022 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.
|
||||
#
|
||||
|
||||
org.keycloak.models.map.singleUseObject.MapSingleUseObjectProviderFactory
|
||||
|
|
@ -22,6 +22,6 @@ import org.keycloak.provider.ProviderFactory;
|
|||
*
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public interface ActionTokenStoreProviderFactory extends ProviderFactory<ActionTokenStoreProvider> {
|
||||
public interface ActionTokenStoreProviderFactory<T extends ActionTokenStoreProvider> extends ProviderFactory<T> {
|
||||
|
||||
}
|
||||
|
|
|
@ -22,5 +22,5 @@ import org.keycloak.provider.ProviderFactory;
|
|||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface SingleUseObjectProviderFactory extends ProviderFactory<SingleUseObjectProvider> {
|
||||
public interface SingleUseObjectProviderFactory<T extends SingleUseObjectProvider> extends ProviderFactory<T> {
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.keycloak.models;
|
||||
|
||||
import org.keycloak.storage.SearchableModelField;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
@ -24,6 +26,13 @@ import java.util.Map;
|
|||
*/
|
||||
public interface ActionTokenValueModel {
|
||||
|
||||
class SearchableFields {
|
||||
public static final SearchableModelField<ActionTokenValueModel> ID = new SearchableModelField<>("id", String.class);
|
||||
public static final SearchableModelField<ActionTokenValueModel> USER_ID = new SearchableModelField<>("userId", String.class);
|
||||
public static final SearchableModelField<ActionTokenValueModel> ACTION_ID = new SearchableModelField<>("actionId", String.class);
|
||||
public static final SearchableModelField<ActionTokenValueModel> ACTION_VERIFICATION_NONCE = new SearchableModelField<>("actionVerificationNonce", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns unmodifiable map of all notes.
|
||||
* @return see description. Returns empty map if no note is set, never returns {@code null}.
|
||||
|
|
|
@ -843,6 +843,8 @@
|
|||
<keycloak.loginFailure.provider>map</keycloak.loginFailure.provider>
|
||||
<keycloak.authorization.provider>map</keycloak.authorization.provider>
|
||||
<keycloak.eventsStore.provider>map</keycloak.eventsStore.provider>
|
||||
<keycloak.actionToken.provider>map</keycloak.actionToken.provider>
|
||||
<keycloak.singleUseObject.provider>map</keycloak.singleUseObject.provider>
|
||||
<keycloak.authorizationCache.enabled>false</keycloak.authorizationCache.enabled>
|
||||
<keycloak.realmCache.enabled>false</keycloak.realmCache.enabled>
|
||||
<keycloak.userCache.enabled>false</keycloak.userCache.enabled>
|
||||
|
|
|
@ -387,6 +387,7 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
|||
|
||||
@Test
|
||||
public void accessTokenCodeExpired() {
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
RealmManager.realm(adminClient.realm("test")).accessCodeLifeSpan(1);
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
|
||||
|
@ -397,15 +398,18 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
|||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
|
||||
try {
|
||||
setTimeOffset(2);
|
||||
|
||||
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
|
||||
Assert.assertEquals(400, response.getStatusCode());
|
||||
|
||||
setTimeOffset(0);
|
||||
} finally {
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
resetTimeOffset();
|
||||
}
|
||||
|
||||
AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, codeId);
|
||||
expectedEvent.error("expired_code")
|
||||
expectedEvent.error("invalid_code")
|
||||
.removeDetail(Details.TOKEN_ID)
|
||||
.removeDetail(Details.REFRESH_TOKEN_ID)
|
||||
.removeDetail(Details.REFRESH_TOKEN_TYPE)
|
||||
|
|
|
@ -469,6 +469,7 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
|
|||
|
||||
@Test
|
||||
public void testExpiredUserCodeTest() throws Exception {
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
// Device Authorization Request from device
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(DEVICE_APP);
|
||||
|
@ -486,10 +487,12 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
|
|||
setTimeOffset(610);
|
||||
openVerificationPage(response.getVerificationUriComplete());
|
||||
} finally {
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
resetTimeOffset();
|
||||
}
|
||||
|
||||
verificationPage.assertExpiredUserCodePage();
|
||||
// device code not found in the cache because of expiration => invalid_grant error and redirection to the login page
|
||||
loginPage.assertCurrent();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -561,6 +564,7 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
|
|||
|
||||
@Test
|
||||
public void testExpiredDeviceCode() throws Exception {
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
// Device Authorization Request from device
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(DEVICE_APP);
|
||||
|
@ -581,8 +585,9 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
|
|||
response.getDeviceCode());
|
||||
|
||||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("expired_token", tokenResponse.getError());
|
||||
Assert.assertEquals("invalid_grant", tokenResponse.getError());
|
||||
} finally {
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
resetTimeOffset();
|
||||
}
|
||||
}
|
||||
|
@ -600,6 +605,7 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
|
|||
|
||||
@Test
|
||||
public void testDeviceCodeLifespanPerClient() throws Exception {
|
||||
getTestingClient().testing().setTestingInfinispanTimeService();
|
||||
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), DEVICE_APP);
|
||||
ClientRepresentation clientRepresentation = client.toRepresentation();
|
||||
// Device Authorization Request from device
|
||||
|
@ -638,6 +644,7 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
|
|||
Assert.assertEquals(400, tokenResponse.getStatusCode());
|
||||
Assert.assertEquals("expired_token", tokenResponse.getError());
|
||||
} finally {
|
||||
getTestingClient().testing().revertTestingInfinispanTimeService();
|
||||
resetTimeOffset();
|
||||
}
|
||||
|
||||
|
|
|
@ -142,9 +142,28 @@
|
|||
}
|
||||
},
|
||||
|
||||
"actionToken": {
|
||||
"provider": "${keycloak.actionToken.provider:infinispan}",
|
||||
"map": {
|
||||
"storage": {
|
||||
"provider": "${keycloak.actionToken.map.storage.provider:concurrenthashmap}"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"singleUseObject": {
|
||||
"provider": "${keycloak.singleUseObject.provider:infinispan}",
|
||||
"map": {
|
||||
"storage": {
|
||||
"provider": "${keycloak.singleUseObject.map.storage.provider:concurrenthashmap}"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"mapStorage": {
|
||||
"concurrenthashmap": {
|
||||
"dir": "${project.build.directory:target}",
|
||||
"keyType.single-use-objects": "string",
|
||||
"keyType.realms": "string",
|
||||
"keyType.authz-resource-servers": "string"
|
||||
},
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.infinispan.server.hotrod.configuration.HotRodServerConfiguration;
|
|||
import org.infinispan.server.hotrod.configuration.HotRodServerConfigurationBuilder;
|
||||
import org.junit.rules.ExternalResource;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.connections.infinispan.InfinispanUtil;
|
||||
import org.keycloak.models.map.storage.hotRod.common.HotRodUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -77,6 +78,10 @@ public class HotRodServerRule extends ExternalResource {
|
|||
|
||||
getCaches(USER_SESSION_CACHE_NAME, OFFLINE_USER_SESSION_CACHE_NAME, CLIENT_SESSION_CACHE_NAME, OFFLINE_CLIENT_SESSION_CACHE_NAME,
|
||||
LOGIN_FAILURE_CACHE_NAME, WORK_CACHE_NAME, ACTION_TOKEN_CACHE);
|
||||
|
||||
// Use Keycloak time service in remote caches
|
||||
InfinispanUtil.setTimeServiceToKeycloakTime(hotRodCacheManager);
|
||||
InfinispanUtil.setTimeServiceToKeycloakTime(hotRodCacheManager2);
|
||||
}
|
||||
|
||||
public void createHotRodMapStoreServer() {
|
||||
|
|
|
@ -21,7 +21,9 @@ import org.junit.runner.Description;
|
|||
import org.junit.runners.model.Statement;
|
||||
import org.keycloak.authorization.store.StoreFactorySpi;
|
||||
import org.keycloak.events.EventStoreSpi;
|
||||
import org.keycloak.models.ActionTokenStoreSpi;
|
||||
import org.keycloak.models.DeploymentStateSpi;
|
||||
import org.keycloak.models.SingleUseObjectSpi;
|
||||
import org.keycloak.models.UserLoginFailureSpi;
|
||||
import org.keycloak.models.UserSessionSpi;
|
||||
import org.keycloak.models.dblock.NoLockingDBLockProviderFactory;
|
||||
|
@ -30,6 +32,7 @@ import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory;
|
|||
import org.keycloak.models.map.client.MapClientProviderFactory;
|
||||
import org.keycloak.models.map.clientscope.MapClientScopeProviderFactory;
|
||||
import org.keycloak.models.map.events.MapEventStoreProviderFactory;
|
||||
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectProviderFactory;
|
||||
import org.keycloak.models.map.storage.hotRod.connections.DefaultHotRodConnectionProviderFactory;
|
||||
import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProviderFactory;
|
||||
import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionSpi;
|
||||
|
@ -73,6 +76,8 @@ public class HotRodMapStorage extends KeycloakModelParameters {
|
|||
@Override
|
||||
public void updateConfig(Config cf) {
|
||||
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(ActionTokenStoreSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(SingleUseObjectSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("group").provider(MapGroupProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
|
||||
|
@ -90,7 +95,8 @@ public class HotRodMapStorage extends KeycloakModelParameters {
|
|||
|
||||
cf.spi(MapStorageSpi.NAME)
|
||||
.provider(ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.config("dir", "${project.build.directory:target}");
|
||||
.config("dir", "${project.build.directory:target}")
|
||||
.config("keyType.single-use-objects", "string");
|
||||
|
||||
cf.spi(HotRodConnectionSpi.NAME).provider(DefaultHotRodConnectionProviderFactory.PROVIDER_ID)
|
||||
.config("enableSecurity", "false")
|
||||
|
|
|
@ -19,8 +19,12 @@ package org.keycloak.testsuite.model.parameters;
|
|||
import org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionSpi;
|
||||
import org.keycloak.models.ActionTokenStoreSpi;
|
||||
import org.keycloak.models.SingleUseObjectSpi;
|
||||
import org.keycloak.models.session.UserSessionPersisterSpi;
|
||||
import org.keycloak.models.sessions.infinispan.InfinispanActionTokenStoreProviderFactory;
|
||||
import org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory;
|
||||
import org.keycloak.models.sessions.infinispan.InfinispanSingleUseObjectProviderFactory;
|
||||
import org.keycloak.models.sessions.infinispan.InfinispanUserLoginFailureProviderFactory;
|
||||
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory;
|
||||
import org.keycloak.sessions.AuthenticationSessionSpi;
|
||||
|
@ -55,6 +59,8 @@ public class Infinispan extends KeycloakModelParameters {
|
|||
.add(InfinispanConnectionSpi.class)
|
||||
.add(StickySessionEncoderSpi.class)
|
||||
.add(UserSessionPersisterSpi.class)
|
||||
.add(ActionTokenStoreSpi.class)
|
||||
.add(SingleUseObjectSpi.class)
|
||||
|
||||
.build();
|
||||
|
||||
|
@ -66,6 +72,8 @@ public class Infinispan extends KeycloakModelParameters {
|
|||
.add(InfinispanUserCacheProviderFactory.class)
|
||||
.add(InfinispanUserSessionProviderFactory.class)
|
||||
.add(InfinispanUserLoginFailureProviderFactory.class)
|
||||
.add(InfinispanActionTokenStoreProviderFactory.class)
|
||||
.add(InfinispanSingleUseObjectProviderFactory.class)
|
||||
.add(StickySessionEncoderProviderFactory.class)
|
||||
.add(TimerProviderFactory.class)
|
||||
.build();
|
||||
|
|
|
@ -20,8 +20,10 @@ import com.google.common.collect.ImmutableSet;
|
|||
import java.util.Set;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authorization.store.StoreFactorySpi;
|
||||
import org.keycloak.models.ActionTokenStoreSpi;
|
||||
import org.keycloak.events.EventStoreSpi;
|
||||
import org.keycloak.models.DeploymentStateSpi;
|
||||
import org.keycloak.models.SingleUseObjectSpi;
|
||||
import org.keycloak.models.UserLoginFailureSpi;
|
||||
import org.keycloak.models.UserSessionSpi;
|
||||
import org.keycloak.models.dblock.NoLockingDBLockProviderFactory;
|
||||
|
@ -34,6 +36,7 @@ import org.keycloak.models.map.group.MapGroupProviderFactory;
|
|||
import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory;
|
||||
import org.keycloak.models.map.realm.MapRealmProviderFactory;
|
||||
import org.keycloak.models.map.role.MapRoleProviderFactory;
|
||||
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectProviderFactory;
|
||||
import org.keycloak.models.map.storage.MapStorageSpi;
|
||||
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
|
||||
import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory;
|
||||
|
@ -94,6 +97,8 @@ public class JpaMapStorage extends KeycloakModelParameters {
|
|||
.spi("user").provider(MapUserProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(UserLoginFailureSpi.NAME).provider(MapUserLoginFailureProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("dblock").provider(NoLockingDBLockProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(ActionTokenStoreSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(SingleUseObjectSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(UserSessionSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID) .config("storage-user-sessions.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.config("storage-client-sessions.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(EventStoreSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID) .config("storage-admin-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
|
|
|
@ -22,10 +22,10 @@ import org.junit.runner.Description;
|
|||
import org.junit.runners.model.Statement;
|
||||
import org.keycloak.authorization.store.StoreFactorySpi;
|
||||
import org.keycloak.events.EventStoreSpi;
|
||||
import org.keycloak.models.ActionTokenStoreSpi;
|
||||
import org.keycloak.models.DeploymentStateSpi;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.SingleUseObjectSpi;
|
||||
import org.keycloak.models.UserLoginFailureSpi;
|
||||
import org.keycloak.models.UserSessionSpi;
|
||||
import org.keycloak.models.map.storage.MapStorageSpi;
|
||||
|
@ -38,7 +38,6 @@ import org.keycloak.testsuite.model.KeycloakModelParameters;
|
|||
import org.keycloak.testsuite.util.LDAPRule;
|
||||
import org.keycloak.util.ldap.LDAPEmbeddedServer;
|
||||
|
||||
import javax.naming.NamingException;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
|
@ -104,7 +103,9 @@ public class LdapMapStorage extends KeycloakModelParameters {
|
|||
.spi("authorizationPersister").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("authenticationSessions").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(EventStoreSpi.NAME).config("map.storage-admin-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(EventStoreSpi.NAME).config("map.storage-auth-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID);
|
||||
.spi(EventStoreSpi.NAME).config("map.storage-auth-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(ActionTokenStoreSpi.NAME).config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi(SingleUseObjectSpi.NAME).config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,11 @@ package org.keycloak.testsuite.model.parameters;
|
|||
|
||||
import org.keycloak.authorization.store.StoreFactorySpi;
|
||||
import org.keycloak.events.EventStoreSpi;
|
||||
import org.keycloak.models.ActionTokenStoreProviderFactory;
|
||||
import org.keycloak.models.ActionTokenStoreSpi;
|
||||
import org.keycloak.models.DeploymentStateSpi;
|
||||
import org.keycloak.models.SingleUseObjectProviderFactory;
|
||||
import org.keycloak.models.SingleUseObjectSpi;
|
||||
import org.keycloak.models.UserLoginFailureSpi;
|
||||
import org.keycloak.models.UserSessionSpi;
|
||||
import org.keycloak.models.dblock.NoLockingDBLockProviderFactory;
|
||||
|
@ -26,6 +30,8 @@ import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderF
|
|||
import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory;
|
||||
import org.keycloak.models.map.events.MapEventStoreProviderFactory;
|
||||
import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory;
|
||||
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectProviderFactory;
|
||||
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
|
||||
import org.keycloak.models.map.userSession.MapUserSessionProviderFactory;
|
||||
import org.keycloak.sessions.AuthenticationSessionSpi;
|
||||
import org.keycloak.testsuite.model.KeycloakModelParameters;
|
||||
|
@ -51,6 +57,8 @@ public class Map extends KeycloakModelParameters {
|
|||
|
||||
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
|
||||
.add(AuthenticationSessionSpi.class)
|
||||
.add(ActionTokenStoreSpi.class)
|
||||
.add(SingleUseObjectSpi.class)
|
||||
.add(MapStorageSpi.class)
|
||||
|
||||
.build();
|
||||
|
@ -69,6 +77,8 @@ public class Map extends KeycloakModelParameters {
|
|||
.add(MapUserLoginFailureProviderFactory.class)
|
||||
.add(NoLockingDBLockProviderFactory.class)
|
||||
.add(MapEventStoreProviderFactory.class)
|
||||
.add(ActionTokenStoreProviderFactory.class)
|
||||
.add(SingleUseObjectProviderFactory.class)
|
||||
.build();
|
||||
|
||||
public Map() {
|
||||
|
@ -78,6 +88,8 @@ public class Map extends KeycloakModelParameters {
|
|||
@Override
|
||||
public void updateConfig(Config cf) {
|
||||
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).defaultProvider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID)
|
||||
.spi(ActionTokenStoreSpi.NAME).defaultProvider(MapSingleUseObjectProviderFactory.PROVIDER_ID)
|
||||
.spi(SingleUseObjectSpi.NAME).defaultProvider(MapSingleUseObjectProviderFactory.PROVIDER_ID)
|
||||
.spi("client").defaultProvider(MapClientProviderFactory.PROVIDER_ID)
|
||||
.spi("clientScope").defaultProvider(MapClientScopeProviderFactory.PROVIDER_ID)
|
||||
.spi("group").defaultProvider(MapGroupProviderFactory.PROVIDER_ID)
|
||||
|
@ -91,5 +103,6 @@ public class Map extends KeycloakModelParameters {
|
|||
.spi("dblock").defaultProvider(NoLockingDBLockProviderFactory.PROVIDER_ID)
|
||||
.spi(EventStoreSpi.NAME).defaultProvider(MapEventStoreProviderFactory.PROVIDER_ID)
|
||||
;
|
||||
cf.spi(MapStorageSpi.NAME).provider(ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).config("keyType.single-use-objects", "string");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Copyright 2022 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.model.singleUseObject;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.ActionTokenKeyModel;
|
||||
import org.keycloak.models.ActionTokenStoreProvider;
|
||||
import org.keycloak.models.ActionTokenValueModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.SingleUseObjectProvider;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.testsuite.model.KeycloakModelTest;
|
||||
import org.keycloak.testsuite.model.RequireProvider;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
@RequireProvider(ActionTokenStoreProvider.class)
|
||||
@RequireProvider(SingleUseObjectProvider.class)
|
||||
public class SingleUseObjectModelTest extends KeycloakModelTest {
|
||||
|
||||
private String realmId;
|
||||
|
||||
private String userId;
|
||||
|
||||
@Override
|
||||
public void createEnvironment(KeycloakSession s) {
|
||||
RealmModel realm = s.realms().createRealm("realm");
|
||||
realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName()));
|
||||
realmId = realm.getId();
|
||||
UserModel user = s.users().addUser(realm, "user");
|
||||
userId = user.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanEnvironment(KeycloakSession s) {
|
||||
Time.setOffset(0);
|
||||
s.realms().removeRealm(realmId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActionTokens() {
|
||||
ActionTokenKeyModel key = withRealm(realmId, (session, realm) -> {
|
||||
ActionTokenStoreProvider actionTokenStore = session.getProvider(ActionTokenStoreProvider.class);
|
||||
DefaultActionTokenKey actionTokenKey = new DefaultActionTokenKey(userId, UUID.randomUUID().toString(), Time.currentTime() + 60, null);
|
||||
Map<String, String> notes = new HashMap<>();
|
||||
notes.put("foo", "bar");
|
||||
actionTokenStore.put(actionTokenKey, notes);
|
||||
return actionTokenKey;
|
||||
});
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
ActionTokenStoreProvider actionTokenStore = session.getProvider(ActionTokenStoreProvider.class);
|
||||
ActionTokenValueModel valueModel = actionTokenStore.get(key);
|
||||
Assert.assertNotNull(valueModel);
|
||||
Assert.assertEquals("bar", valueModel.getNote("foo"));
|
||||
|
||||
valueModel = actionTokenStore.remove(key);
|
||||
Assert.assertNotNull(valueModel);
|
||||
Assert.assertEquals("bar", valueModel.getNote("foo"));
|
||||
});
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
ActionTokenStoreProvider actionTokenStore = session.getProvider(ActionTokenStoreProvider.class);
|
||||
ActionTokenValueModel valueModel = actionTokenStore.get(key);
|
||||
Assert.assertNull(valueModel);
|
||||
|
||||
Map<String, String> notes = new HashMap<>();
|
||||
notes.put("foo", "bar");
|
||||
actionTokenStore.put(key, notes);
|
||||
});
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
ActionTokenStoreProvider actionTokenStore = session.getProvider(ActionTokenStoreProvider.class);
|
||||
ActionTokenValueModel valueModel = actionTokenStore.get(key);
|
||||
Assert.assertNotNull(valueModel);
|
||||
Assert.assertEquals("bar", valueModel.getNote("foo"));
|
||||
|
||||
Time.setOffset(70);
|
||||
|
||||
valueModel = actionTokenStore.get(key);
|
||||
Assert.assertNull(valueModel);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleUseStore() {
|
||||
String key = UUID.randomUUID().toString();
|
||||
Map<String, String> notes = new HashMap<>();
|
||||
notes.put("foo", "bar");
|
||||
|
||||
Map<String, String> notes2 = new HashMap<>();
|
||||
notes2.put("baf", "meow");
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
|
||||
Assert.assertFalse(singleUseStore.replace(key, notes2));
|
||||
|
||||
singleUseStore.put(key, 60, notes);
|
||||
});
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
|
||||
Map<String, String> actualNotes = singleUseStore.get(key);
|
||||
Assert.assertEquals(notes, actualNotes);
|
||||
|
||||
Assert.assertTrue(singleUseStore.replace(key, notes2));
|
||||
});
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
|
||||
Map<String, String> actualNotes = singleUseStore.get(key);
|
||||
Assert.assertEquals(notes2, actualNotes);
|
||||
|
||||
Assert.assertFalse(singleUseStore.putIfAbsent(key, 60));
|
||||
|
||||
Assert.assertEquals(notes2, singleUseStore.remove(key));
|
||||
});
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
|
||||
Assert.assertTrue(singleUseStore.putIfAbsent(key, 60));
|
||||
});
|
||||
|
||||
inComittedTransaction(session -> {
|
||||
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
|
||||
Map<String, String> actualNotes = singleUseStore.get(key);
|
||||
assertThat(actualNotes, Matchers.anEmptyMap());
|
||||
|
||||
Time.setOffset(70);
|
||||
|
||||
Assert.assertNull(singleUseStore.get(key));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -297,6 +297,8 @@
|
|||
<systemProperty><key>keycloak.userSession.provider</key><value>map</value></systemProperty>
|
||||
<systemProperty><key>keycloak.loginFailure.provider</key><value>map</value></systemProperty>
|
||||
<systemProperty><key>keycloak.authorization.provider</key><value>map</value></systemProperty>
|
||||
<systemProperty><key>keycloak.actionToken.provider</key><value>map</value></systemProperty>
|
||||
<systemProperty><key>keycloak.singleUseObject.provider</key><value>map</value></systemProperty>
|
||||
<systemProperty><key>keycloak.authorizationCache.enabled</key><value>false</value></systemProperty>
|
||||
<systemProperty><key>keycloak.realmCache.enabled</key><value>false</value></systemProperty>
|
||||
<systemProperty><key>keycloak.userCache.enabled</key><value>false</value></systemProperty>
|
||||
|
|
|
@ -103,10 +103,29 @@
|
|||
}
|
||||
},
|
||||
|
||||
"actionToken": {
|
||||
"provider": "${keycloak.actionToken.provider:infinispan}",
|
||||
"map": {
|
||||
"storage": {
|
||||
"provider": "${keycloak.actionToken.map.storage.provider:concurrenthashmap}"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"singleUseObject": {
|
||||
"provider": "${keycloak.singleUseObject.provider:infinispan}",
|
||||
"map": {
|
||||
"storage": {
|
||||
"provider": "${keycloak.singleUseObject.map.storage.provider:concurrenthashmap}"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"mapStorage": {
|
||||
"provider": "${keycloak.mapStorage.provider:}",
|
||||
"concurrenthashmap": {
|
||||
"dir": "${project.build.directory:target/map}",
|
||||
"keyType.single-use-objects": "string",
|
||||
"keyType.realms": "string",
|
||||
"keyType.authz-resource-servers": "string"
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue