Merge pull request #4248 from mposolda/client-initial-access-db

KEYCLOAK-4631 Move ClientInitialAccessModel from userSession model to…
This commit is contained in:
Marek Posolda 2017-06-22 06:27:25 +02:00 committed by GitHub
commit ab7a0c2252
20 changed files with 399 additions and 413 deletions

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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>

View file

@ -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>

View 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;
}
}

View file

@ -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
}

View file

@ -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();
}

View file

@ -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();

View file

@ -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");
}

View file

@ -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 {

View file

@ -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();
}

View file

@ -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();
}
}

View file

@ -164,6 +164,8 @@ public class TestingResourceProvider implements RealmResourceProvider {
session.sessions().removeExpired(realm);
session.authenticationSessions().removeExpired(realm);
session.realms().removeExpiredClientInitialAccess();
return Response.ok().build();
}

View file

@ -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());
}
}