Merge pull request #4248 from mposolda/client-initial-access-db
KEYCLOAK-4631 Move ClientInitialAccessModel from userSession model to…
This commit is contained in:
commit
ab7a0c2252
20 changed files with 399 additions and 413 deletions
|
@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.cluster.ClusterProvider;
|
||||
import org.keycloak.models.ClientInitialAccessModel;
|
||||
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
|
||||
import org.keycloak.migration.MigrationModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
|
@ -1059,4 +1060,34 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
return adapter;
|
||||
}
|
||||
|
||||
// Don't cache ClientInitialAccessModel for now
|
||||
@Override
|
||||
public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
|
||||
return getDelegate().createClientInitialAccessModel(realm, expiration, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
|
||||
return getDelegate().getClientInitialAccessModel(realm, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeClientInitialAccessModel(RealmModel realm, String id) {
|
||||
getDelegate().removeClientInitialAccessModel(realm, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
|
||||
return getDelegate().listClientInitialAccess(realm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeExpiredClientInitialAccess() {
|
||||
getDelegate().removeExpiredClientInitialAccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess) {
|
||||
getDelegate().decreaseRemainingCount(realm, clientInitialAccess);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.sessions.infinispan;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.keycloak.models.ClientInitialAccessModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class ClientInitialAccessAdapter implements ClientInitialAccessModel {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final InfinispanUserSessionProvider provider;
|
||||
private final Cache<String, SessionEntity> cache;
|
||||
private final RealmModel realm;
|
||||
private final ClientInitialAccessEntity entity;
|
||||
|
||||
public ClientInitialAccessAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache, RealmModel realm, ClientInitialAccessEntity entity) {
|
||||
this.session = session;
|
||||
this.provider = provider;
|
||||
this.cache = cache;
|
||||
this.realm = realm;
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return realm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTimestamp() {
|
||||
return entity.getTimestamp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getExpiration() {
|
||||
return entity.getExpiration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return entity.getCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRemainingCount() {
|
||||
return entity.getRemainingCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decreaseRemainingCount() {
|
||||
entity.setRemainingCount(entity.getRemainingCount() - 1);
|
||||
update();
|
||||
}
|
||||
|
||||
void update() {
|
||||
provider.getTx().replace(cache, entity.getId(), entity);
|
||||
}
|
||||
|
||||
}
|
|
@ -22,7 +22,6 @@ import org.infinispan.CacheStream;
|
|||
import org.infinispan.context.Flag;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.ClientInitialAccessModel;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -32,23 +31,19 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.UserSessionProvider;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.stream.ClientInitialAccessPredicate;
|
||||
import org.keycloak.models.sessions.infinispan.stream.Comparators;
|
||||
import org.keycloak.models.sessions.infinispan.stream.Mappers;
|
||||
import org.keycloak.models.sessions.infinispan.stream.SessionPredicate;
|
||||
import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate;
|
||||
import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -271,7 +266,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
log.debugf("Removing expired sessions");
|
||||
removeExpiredUserSessions(realm);
|
||||
removeExpiredOfflineUserSessions(realm);
|
||||
removeExpiredClientInitialAccess(realm);
|
||||
}
|
||||
|
||||
private void removeExpiredUserSessions(RealmModel realm) {
|
||||
|
@ -317,14 +311,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
log.debugf("Removed %d expired offline user sessions for realm '%s'", counter, realm.getName());
|
||||
}
|
||||
|
||||
private void removeExpiredClientInitialAccess(RealmModel realm) {
|
||||
Iterator<String> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
|
||||
.entrySet().stream().filter(ClientInitialAccessPredicate.create(realm.getId()).expired(Time.currentTime())).map(Mappers.sessionId()).iterator();
|
||||
while (itr.hasNext()) {
|
||||
tx.remove(sessionCache, itr.next());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUserSessions(RealmModel realm) {
|
||||
removeUserSessions(realm, false);
|
||||
|
@ -417,19 +403,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
return models;
|
||||
}
|
||||
|
||||
List<ClientInitialAccessModel> wrapClientInitialAccess(RealmModel realm, Collection<ClientInitialAccessEntity> entities) {
|
||||
List<ClientInitialAccessModel> models = new LinkedList<>();
|
||||
for (ClientInitialAccessEntity e : entities) {
|
||||
models.add(wrap(realm, e));
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
ClientInitialAccessAdapter wrap(RealmModel realm, ClientInitialAccessEntity entity) {
|
||||
Cache<String, SessionEntity> cache = getCache(false);
|
||||
return entity != null ? new ClientInitialAccessAdapter(session, this, cache, realm, entity) : null;
|
||||
}
|
||||
|
||||
UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
|
||||
return entity != null ? new UserLoginFailureAdapter(this, loginFailureCache, key, entity) : null;
|
||||
}
|
||||
|
@ -565,48 +538,4 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, importedUserSession.getCache());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
|
||||
String id = KeycloakModelUtils.generateId();
|
||||
|
||||
ClientInitialAccessEntity entity = new ClientInitialAccessEntity();
|
||||
entity.setId(id);
|
||||
entity.setRealm(realm.getId());
|
||||
entity.setTimestamp(Time.currentTime());
|
||||
entity.setExpiration(expiration);
|
||||
entity.setCount(count);
|
||||
entity.setRemainingCount(count);
|
||||
|
||||
tx.put(sessionCache, id, entity);
|
||||
|
||||
return wrap(realm, entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
|
||||
Cache<String, SessionEntity> cache = getCache(false);
|
||||
ClientInitialAccessEntity entity = (ClientInitialAccessEntity) tx.get(cache, id); // Chance created in this transaction
|
||||
|
||||
if (entity == null) {
|
||||
entity = (ClientInitialAccessEntity) cache.get(id);
|
||||
}
|
||||
|
||||
return wrap(realm, entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeClientInitialAccessModel(RealmModel realm, String id) {
|
||||
tx.remove(getCache(false), id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
|
||||
Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.entrySet().stream().filter(ClientInitialAccessPredicate.create(realm.getId())).iterator();
|
||||
List<ClientInitialAccessModel> list = new LinkedList<>();
|
||||
while (itr.hasNext()) {
|
||||
list.add(wrap(realm, (ClientInitialAccessEntity) itr.next().getValue()));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.sessions.infinispan.entities;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class ClientInitialAccessEntity extends SessionEntity {
|
||||
|
||||
private int timestamp;
|
||||
|
||||
private int expires;
|
||||
|
||||
private int count;
|
||||
|
||||
private int remainingCount;
|
||||
|
||||
public int getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(int timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public int getExpiration() {
|
||||
return expires;
|
||||
}
|
||||
|
||||
public void setExpiration(int expires) {
|
||||
this.expires = expires;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setCount(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public int getRemainingCount() {
|
||||
return remainingCount;
|
||||
}
|
||||
|
||||
public void setRemainingCount(int remainingCount) {
|
||||
this.remainingCount = remainingCount;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.sessions.infinispan.mapreduce;
|
||||
|
||||
import org.infinispan.distexec.mapreduce.Collector;
|
||||
import org.infinispan.distexec.mapreduce.Mapper;
|
||||
import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class ClientInitialAccessMapper implements Mapper<String, SessionEntity, String, Object>, Serializable {
|
||||
|
||||
public ClientInitialAccessMapper(String realm) {
|
||||
this.realm = realm;
|
||||
}
|
||||
|
||||
private enum EmitValue {
|
||||
KEY, ENTITY
|
||||
}
|
||||
|
||||
private String realm;
|
||||
|
||||
private EmitValue emit = EmitValue.ENTITY;
|
||||
|
||||
private Integer expired;
|
||||
|
||||
public static ClientInitialAccessMapper create(String realm) {
|
||||
return new ClientInitialAccessMapper(realm);
|
||||
}
|
||||
|
||||
public ClientInitialAccessMapper emitKey() {
|
||||
emit = EmitValue.KEY;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientInitialAccessMapper expired(int time) {
|
||||
this.expired = time;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void map(String key, SessionEntity e, Collector collector) {
|
||||
if (!realm.equals(e.getRealm())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(e instanceof ClientInitialAccessEntity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClientInitialAccessEntity entity = (ClientInitialAccessEntity) e;
|
||||
|
||||
boolean include = false;
|
||||
|
||||
if (expired != null) {
|
||||
if (entity.getRemainingCount() <= 0) {
|
||||
include = true;
|
||||
} else if (entity.getExpiration() > 0 && (entity.getTimestamp() + entity.getExpiration()) < expired) {
|
||||
include = true;
|
||||
}
|
||||
} else {
|
||||
include = true;
|
||||
}
|
||||
|
||||
if (include) {
|
||||
switch (emit) {
|
||||
case KEY:
|
||||
collector.emit(key, key);
|
||||
break;
|
||||
case ENTITY:
|
||||
collector.emit(key, entity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.sessions.infinispan.stream;
|
||||
|
||||
import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class ClientInitialAccessPredicate implements Predicate<Map.Entry<String, SessionEntity>>, Serializable {
|
||||
|
||||
public ClientInitialAccessPredicate(String realm) {
|
||||
this.realm = realm;
|
||||
}
|
||||
|
||||
private String realm;
|
||||
|
||||
private Integer expired;
|
||||
|
||||
public static ClientInitialAccessPredicate create(String realm) {
|
||||
return new ClientInitialAccessPredicate(realm);
|
||||
}
|
||||
|
||||
public ClientInitialAccessPredicate expired(int time) {
|
||||
this.expired = time;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(Map.Entry<String, SessionEntity> entry) {
|
||||
SessionEntity e = entry.getValue();
|
||||
|
||||
if (!realm.equals(e.getRealm())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(e instanceof ClientInitialAccessEntity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ClientInitialAccessEntity entity = (ClientInitialAccessEntity) e;
|
||||
|
||||
if (expired != null) {
|
||||
if (entity.getRemainingCount() <= 0) {
|
||||
return true;
|
||||
} else if (entity.getExpiration() > 0 && (entity.getTimestamp() + entity.getExpiration()) < expired) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -18,8 +18,10 @@
|
|||
package org.keycloak.models.jpa;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.jpa.util.JpaUtils;
|
||||
import org.keycloak.migration.MigrationModel;
|
||||
import org.keycloak.models.ClientInitialAccessModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientTemplateModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
|
@ -29,6 +31,7 @@ import org.keycloak.models.RealmProvider;
|
|||
import org.keycloak.models.RoleContainerModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.jpa.entities.ClientEntity;
|
||||
import org.keycloak.models.jpa.entities.ClientInitialAccessEntity;
|
||||
import org.keycloak.models.jpa.entities.ClientTemplateEntity;
|
||||
import org.keycloak.models.jpa.entities.GroupEntity;
|
||||
import org.keycloak.models.jpa.entities.RealmEntity;
|
||||
|
@ -152,6 +155,8 @@ public class JpaRealmProvider implements RealmProvider {
|
|||
removeRole(adapter, role);
|
||||
}
|
||||
|
||||
num = em.createNamedQuery("removeClientInitialAccessByRealm")
|
||||
.setParameter("realm", realm).executeUpdate();
|
||||
|
||||
em.remove(realm);
|
||||
|
||||
|
@ -519,4 +524,82 @@ public class JpaRealmProvider implements RealmProvider {
|
|||
ClientTemplateAdapter adapter = new ClientTemplateAdapter(realm, em, session, app);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
|
||||
RealmEntity realmEntity = em.find(RealmEntity.class, realm.getId());
|
||||
|
||||
ClientInitialAccessEntity entity = new ClientInitialAccessEntity();
|
||||
entity.setId(KeycloakModelUtils.generateId());
|
||||
entity.setRealm(realmEntity);
|
||||
|
||||
entity.setCount(count);
|
||||
entity.setRemainingCount(count);
|
||||
|
||||
int currentTime = Time.currentTime();
|
||||
entity.setTimestamp(currentTime);
|
||||
entity.setExpiration(expiration);
|
||||
|
||||
em.persist(entity);
|
||||
|
||||
return entityToModel(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
|
||||
ClientInitialAccessEntity entity = em.find(ClientInitialAccessEntity.class, id);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
} else {
|
||||
return entityToModel(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeClientInitialAccessModel(RealmModel realm, String id) {
|
||||
ClientInitialAccessEntity entity = em.find(ClientInitialAccessEntity.class, id);
|
||||
if (entity != null) {
|
||||
em.remove(entity);
|
||||
em.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
|
||||
RealmEntity realmEntity = em.find(RealmEntity.class, realm.getId());
|
||||
|
||||
TypedQuery<ClientInitialAccessEntity> query = em.createNamedQuery("findClientInitialAccessByRealm", ClientInitialAccessEntity.class);
|
||||
query.setParameter("realm", realmEntity);
|
||||
List<ClientInitialAccessEntity> entities = query.getResultList();
|
||||
|
||||
return entities.stream()
|
||||
.map(entity -> entityToModel(entity))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeExpiredClientInitialAccess() {
|
||||
int currentTime = Time.currentTime();
|
||||
|
||||
em.createNamedQuery("removeExpiredClientInitialAccess")
|
||||
.setParameter("currentTime", currentTime)
|
||||
.executeUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess) {
|
||||
em.createNamedQuery("decreaseClientInitialAccessRemainingCount")
|
||||
.setParameter("id", clientInitialAccess.getId())
|
||||
.executeUpdate();
|
||||
}
|
||||
|
||||
private ClientInitialAccessModel entityToModel(ClientInitialAccessEntity entity) {
|
||||
ClientInitialAccessModel model = new ClientInitialAccessModel();
|
||||
model.setId(entity.getId());
|
||||
model.setCount(entity.getCount());
|
||||
model.setRemainingCount(entity.getRemainingCount());
|
||||
model.setExpiration(entity.getExpiration());
|
||||
model.setTimestamp(entity.getTimestamp());
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright 2017 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.jpa.entities;
|
||||
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.NamedQueries;
|
||||
import javax.persistence.NamedQuery;
|
||||
import javax.persistence.Table;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
@Entity
|
||||
@Table(name="CLIENT_INITIAL_ACCESS")
|
||||
@NamedQueries({
|
||||
@NamedQuery(name="findClientInitialAccessByRealm", query="select ia from ClientInitialAccessEntity ia where ia.realm = :realm order by timestamp"),
|
||||
@NamedQuery(name="removeClientInitialAccessByRealm", query="delete from ClientInitialAccessEntity ia where ia.realm = :realm"),
|
||||
@NamedQuery(name="removeExpiredClientInitialAccess", query="delete from ClientInitialAccessEntity ia where (ia.expiration > 0 and (ia.timestamp + ia.expiration) < :currentTime) or ia.remainingCount = 0"),
|
||||
@NamedQuery(name="decreaseClientInitialAccessRemainingCount", query="update ClientInitialAccessEntity ia set ia.remainingCount = ia.remainingCount - 1 where ia.id = :id")
|
||||
})
|
||||
public class ClientInitialAccessEntity {
|
||||
|
||||
@Id
|
||||
@Column(name="ID", length = 36)
|
||||
@Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
|
||||
protected String id;
|
||||
|
||||
@Column(name="TIMESTAMP")
|
||||
private int timestamp;
|
||||
|
||||
@Column(name="EXPIRATION")
|
||||
private int expiration;
|
||||
|
||||
@Column(name="COUNT")
|
||||
private int count;
|
||||
|
||||
@Column(name="REMAINING_COUNT")
|
||||
private int remainingCount;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "REALM_ID")
|
||||
protected RealmEntity realm;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(int timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public int getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
|
||||
public void setExpiration(int expiration) {
|
||||
this.expiration = expiration;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setCount(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public int getRemainingCount() {
|
||||
return remainingCount;
|
||||
}
|
||||
|
||||
public void setRemainingCount(int remainingCount) {
|
||||
this.remainingCount = remainingCount;
|
||||
}
|
||||
|
||||
public RealmEntity getRealm() {
|
||||
return realm;
|
||||
}
|
||||
|
||||
public void setRealm(RealmEntity realm) {
|
||||
this.realm = realm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null) return false;
|
||||
if (!(o instanceof ClientInitialAccessEntity)) return false;
|
||||
|
||||
ClientInitialAccessEntity that = (ClientInitialAccessEntity) o;
|
||||
|
||||
if (!id.equals(that.id)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.hashCode();
|
||||
}
|
||||
}
|
|
@ -22,6 +22,22 @@
|
|||
<dropPrimaryKey constraintName="CONSTRAINT_OFFL_CL_SES_PK2" tableName="OFFLINE_CLIENT_SESSION" />
|
||||
<dropColumn tableName="OFFLINE_CLIENT_SESSION" columnName="CLIENT_SESSION_ID" />
|
||||
<addPrimaryKey columnNames="USER_SESSION_ID,CLIENT_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_CL_SES_PK3" tableName="OFFLINE_CLIENT_SESSION"/>
|
||||
|
||||
<createTable tableName="CLIENT_INITIAL_ACCESS">
|
||||
<column name="ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="REALM_ID" type="VARCHAR(36)"/>
|
||||
|
||||
<column name="TIMESTAMP" type="INT"/>
|
||||
<column name="EXPIRATION" type="INT"/>
|
||||
<column name="COUNT" type="INT"/>
|
||||
<column name="REMAINING_COUNT" type="INT"/>
|
||||
</createTable>
|
||||
|
||||
<addPrimaryKey columnNames="ID" constraintName="CNSTR_CLIENT_INIT_ACC_PK" tableName="CLIENT_INITIAL_ACCESS"/>
|
||||
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="CLIENT_INITIAL_ACCESS" constraintName="FK_CLIENT_INIT_ACC_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
|
||||
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="glavoie@gmail.com" id="3.2.0.idx">
|
||||
|
@ -158,5 +174,9 @@
|
|||
<createIndex indexName="IDX_WEB_ORIG_CLIENT" tableName="WEB_ORIGINS">
|
||||
<column name="CLIENT_ID" type="VARCHAR(36)"/>
|
||||
</createIndex>
|
||||
|
||||
<createIndex indexName="IDX_CLIENT_INIT_ACC_REALM" tableName="CLIENT_INITIAL_ACCESS">
|
||||
<column name="REALM_ID" type="VARCHAR(36)"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
<class>org.keycloak.models.jpa.entities.UserGroupMembershipEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ClientTemplateEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.TemplateScopeMappingEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ClientInitialAccessEntity</class>
|
||||
|
||||
<!-- JpaAuditProviders -->
|
||||
<class>org.keycloak.events.jpa.EventEntity</class>
|
||||
|
|
51
server-spi/src/main/java/org/keycloak/models/ClientInitialAccessModel.java
Executable file → Normal file
51
server-spi/src/main/java/org/keycloak/models/ClientInitialAccessModel.java
Executable file → Normal file
|
@ -20,20 +20,55 @@ package org.keycloak.models;
|
|||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public interface ClientInitialAccessModel {
|
||||
public class ClientInitialAccessModel {
|
||||
|
||||
String getId();
|
||||
private String id;
|
||||
|
||||
RealmModel getRealm();
|
||||
private int timestamp;
|
||||
|
||||
int getTimestamp();
|
||||
private int expiration;
|
||||
|
||||
int getExpiration();
|
||||
private int count;
|
||||
|
||||
int getCount();
|
||||
private int remainingCount;
|
||||
|
||||
int getRemainingCount();
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
void decreaseRemainingCount();
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(int timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public int getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
|
||||
public void setExpiration(int expiration) {
|
||||
this.expiration = expiration;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setCount(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public int getRemainingCount() {
|
||||
return remainingCount;
|
||||
}
|
||||
|
||||
public void setRemainingCount(int remainingCount) {
|
||||
this.remainingCount = remainingCount;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,4 +90,11 @@ public interface RealmProvider extends Provider {
|
|||
List<RealmModel> getRealms();
|
||||
boolean removeRealm(String id);
|
||||
void close();
|
||||
|
||||
ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count);
|
||||
ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id);
|
||||
void removeClientInitialAccessModel(RealmModel realm, String id);
|
||||
List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
|
||||
void removeExpiredClientInitialAccess();
|
||||
void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess); // Separate provider method to ensure we decrease remainingCount atomically instead of doing classic update
|
||||
}
|
||||
|
|
|
@ -72,11 +72,6 @@ public interface UserSessionProvider extends Provider {
|
|||
/** Triggered by persister during pre-load. It optionally imports authenticatedClientSessions too if requested. Otherwise the imported UserSession will have empty list of AuthenticationSessionModel **/
|
||||
UserSessionModel importUserSession(UserSessionModel persistentUserSession, boolean offline, boolean importAuthenticatedClientSessions);
|
||||
|
||||
ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count);
|
||||
ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id);
|
||||
void removeClientInitialAccessModel(RealmModel realm, String id);
|
||||
List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
|
||||
|
||||
void close();
|
||||
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.keycloak.models.ClientInitialAccessModel;
|
|||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.models.utils.RepresentationToModel;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
|
@ -65,7 +66,8 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
|||
}
|
||||
|
||||
try {
|
||||
ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
ClientModel clientModel = RepresentationToModel.createClient(session, realm, client, true);
|
||||
|
||||
ClientRegistrationPolicyManager.triggerAfterRegister(context, registrationAuth, clientModel);
|
||||
|
||||
|
@ -78,7 +80,7 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
|||
|
||||
if (auth.isInitialAccessToken()) {
|
||||
ClientInitialAccessModel initialAccessModel = auth.getInitialAccessModel();
|
||||
initialAccessModel.decreaseRemainingCount();
|
||||
session.realms().decreaseRemainingCount(realm, initialAccessModel);
|
||||
}
|
||||
|
||||
event.client(client.getClientId()).success();
|
||||
|
|
|
@ -85,7 +85,7 @@ public class ClientRegistrationAuth {
|
|||
jwt = tokenVerification.getJwt();
|
||||
|
||||
if (isInitialAccessToken()) {
|
||||
initialAccessModel = session.sessions().getClientInitialAccessModel(session.getContext().getRealm(), jwt.getId());
|
||||
initialAccessModel = session.realms().getClientInitialAccessModel(session.getContext().getRealm(), jwt.getId());
|
||||
if (initialAccessModel == null) {
|
||||
throw unauthorized("Initial Access Token not found");
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ import org.keycloak.services.managers.ApplianceBootstrap;
|
|||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.managers.UserStorageSyncManager;
|
||||
import org.keycloak.services.resources.admin.AdminRoot;
|
||||
import org.keycloak.services.scheduled.ClearExpiredClientInitialAccessTokens;
|
||||
import org.keycloak.services.scheduled.ClearExpiredEvents;
|
||||
import org.keycloak.services.scheduled.ClearExpiredUserSessions;
|
||||
import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner;
|
||||
|
@ -331,6 +332,7 @@ public class KeycloakApplication extends Application {
|
|||
try {
|
||||
TimerProvider timer = session.getProvider(TimerProvider.class);
|
||||
timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredEvents(), interval), interval, "ClearExpiredEvents");
|
||||
timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredClientInitialAccessTokens(), interval), interval, "ClearExpiredClientInitialAccessTokens");
|
||||
timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, "ClearExpiredUserSessions");
|
||||
new UserStorageSyncManager().bootstrapPeriodic(sessionFactory, timer);
|
||||
} finally {
|
||||
|
|
|
@ -81,7 +81,7 @@ public class ClientInitialAccessResource {
|
|||
int expiration = config.getExpiration() != null ? config.getExpiration() : 0;
|
||||
int count = config.getCount() != null ? config.getCount() : 1;
|
||||
|
||||
ClientInitialAccessModel clientInitialAccessModel = session.sessions().createClientInitialAccessModel(realm, expiration, count);
|
||||
ClientInitialAccessModel clientInitialAccessModel = session.realms().createClientInitialAccessModel(realm, expiration, count);
|
||||
|
||||
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, clientInitialAccessModel.getId()).representation(config).success();
|
||||
|
||||
|
@ -101,7 +101,7 @@ public class ClientInitialAccessResource {
|
|||
public List<ClientInitialAccessPresentation> list() {
|
||||
auth.requireView();
|
||||
|
||||
List<ClientInitialAccessModel> models = session.sessions().listClientInitialAccess(realm);
|
||||
List<ClientInitialAccessModel> models = session.realms().listClientInitialAccess(realm);
|
||||
List<ClientInitialAccessPresentation> reps = new LinkedList<>();
|
||||
for (ClientInitialAccessModel m : models) {
|
||||
ClientInitialAccessPresentation r = wrap(m);
|
||||
|
@ -115,7 +115,7 @@ public class ClientInitialAccessResource {
|
|||
public void delete(final @PathParam("id") String id) {
|
||||
auth.requireManage();
|
||||
|
||||
session.sessions().removeClientInitialAccessModel(realm, id);
|
||||
session.realms().removeClientInitialAccessModel(realm, id);
|
||||
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2017 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.services.scheduled;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.timer.ScheduledTask;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ClearExpiredClientInitialAccessTokens implements ScheduledTask {
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession session) {
|
||||
session.realms().removeExpiredClientInitialAccess();
|
||||
}
|
||||
}
|
|
@ -164,6 +164,8 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
|
||||
session.sessions().removeExpired(realm);
|
||||
session.authenticationSessions().removeExpired(realm);
|
||||
session.realms().removeExpiredClientInitialAccess();
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
|
|
|
@ -20,14 +20,17 @@ package org.keycloak.testsuite.admin;
|
|||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.ClientInitialAccessResource;
|
||||
import org.keycloak.client.registration.ClientRegistrationException;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.events.admin.ResourceType;
|
||||
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
|
||||
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.util.AdminEventPaths;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
@ -88,4 +91,40 @@ public class InitialAccessTokenResourceTest extends AbstractAdminTest {
|
|||
assertEquals(5, list.get(0).getCount() + list.get(1).getCount());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testPeriodicExpiration() throws ClientRegistrationException, InterruptedException {
|
||||
ClientInitialAccessPresentation response1 = resource.create(new ClientInitialAccessCreatePresentation(1, 1));
|
||||
ClientInitialAccessPresentation response2 = resource.create(new ClientInitialAccessCreatePresentation(1000, 1));
|
||||
ClientInitialAccessPresentation response3 = resource.create(new ClientInitialAccessCreatePresentation(1000, 0));
|
||||
ClientInitialAccessPresentation response4 = resource.create(new ClientInitialAccessCreatePresentation(0, 1));
|
||||
|
||||
List<ClientInitialAccessPresentation> list = resource.list();
|
||||
assertEquals(4, list.size());
|
||||
|
||||
setTimeOffset(10);
|
||||
|
||||
testingClient.testing().removeExpired(REALM_NAME);
|
||||
|
||||
list = resource.list();
|
||||
assertEquals(2, list.size());
|
||||
|
||||
List<String> remainingIds = list.stream()
|
||||
.map(initialAccessPresentation -> initialAccessPresentation.getId())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Assert.assertNames(remainingIds, response2.getId(), response4.getId());
|
||||
|
||||
setTimeOffset(2000);
|
||||
|
||||
testingClient.testing().removeExpired(REALM_NAME);
|
||||
|
||||
list = resource.list();
|
||||
assertEquals(1, list.size());
|
||||
Assert.assertEquals(list.get(0).getId(), response4.getId());
|
||||
|
||||
// Cleanup
|
||||
realm.clientInitialAccess().delete(response4.getId());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue