Events Map JPA implementation

Closes #9667
This commit is contained in:
Stefan Guilhen 2022-05-24 10:44:18 -03:00 committed by Hynek Mlnařík
parent 3f5741e988
commit 7d96f3ad5a
30 changed files with 1341 additions and 18 deletions

View file

@ -17,6 +17,8 @@
package org.keycloak.models.map.storage.jpa;
public interface Constants {
public static final Integer CURRENT_SCHEMA_VERSION_ADMIN_EVENT = 1;
public static final Integer CURRENT_SCHEMA_VERSION_AUTH_EVENT = 1;
public static final Integer CURRENT_SCHEMA_VERSION_AUTH_SESSION = 1;
public static final Integer CURRENT_SCHEMA_VERSION_AUTHZ_PERMISSION = 1;
public static final Integer CURRENT_SCHEMA_VERSION_AUTHZ_POLICY = 1;

View file

@ -56,6 +56,8 @@ import org.keycloak.common.Profile;
import org.keycloak.common.util.StackUtil;
import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.events.Event;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.GroupModel;
@ -108,6 +110,10 @@ import org.keycloak.models.map.storage.jpa.client.JpaClientMapKeycloakTransactio
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
import org.keycloak.models.map.storage.jpa.clientscope.JpaClientScopeMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.clientscope.entity.JpaClientScopeEntity;
import org.keycloak.models.map.storage.jpa.event.admin.JpaAdminEventMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.event.admin.entity.JpaAdminEventEntity;
import org.keycloak.models.map.storage.jpa.event.auth.JpaAuthEventMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.event.auth.entity.JpaAuthEventEntity;
import org.keycloak.models.map.storage.jpa.group.JpaGroupMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.group.entity.JpaGroupEntity;
import org.keycloak.models.map.storage.jpa.hibernate.listeners.JpaAutoFlushListener;
@ -158,6 +164,9 @@ public class JpaMapStorageProviderFactory implements
.constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new)
//client-scope
.constructor(JpaClientScopeEntity.class, JpaClientScopeEntity::new)
//event
.constructor(JpaAdminEventEntity.class, JpaAdminEventEntity::new)
.constructor(JpaAuthEventEntity.class, JpaAuthEventEntity::new)
//group
.constructor(JpaGroupEntity.class, JpaGroupEntity::new)
//realm
@ -184,6 +193,9 @@ public class JpaMapStorageProviderFactory implements
MODEL_TO_TX.put(RootAuthenticationSessionModel.class, JpaRootAuthenticationSessionMapKeycloakTransaction::new);
MODEL_TO_TX.put(ClientScopeModel.class, JpaClientScopeMapKeycloakTransaction::new);
MODEL_TO_TX.put(ClientModel.class, JpaClientMapKeycloakTransaction::new);
//event
MODEL_TO_TX.put(AdminEvent.class, JpaAdminEventMapKeycloakTransaction::new);
MODEL_TO_TX.put(Event.class, JpaAuthEventMapKeycloakTransaction::new);
MODEL_TO_TX.put(GroupModel.class, JpaGroupMapKeycloakTransaction::new);
MODEL_TO_TX.put(RealmModel.class, JpaRealmMapKeycloakTransaction::new);
MODEL_TO_TX.put(RoleModel.class, JpaRoleMapKeycloakTransaction::new);

View file

@ -17,6 +17,7 @@
package org.keycloak.models.map.storage.jpa;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

View file

@ -0,0 +1,63 @@
/*
* 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.jpa.event.admin;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.models.map.events.MapAdminEventEntity;
import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder;
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
import org.keycloak.models.map.storage.jpa.event.admin.entity.JpaAdminEventEntity;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_ADMIN_EVENT;
/**
* A {@link org.keycloak.models.map.storage.MapKeycloakTransaction} implementation for admin event entities.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaAdminEventMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaAdminEventEntity, MapAdminEventEntity, AdminEvent> {
public JpaAdminEventMapKeycloakTransaction(final EntityManager em) {
super(JpaAdminEventEntity.class, AdminEvent.class, em);
}
@Override
protected Selection<? extends JpaAdminEventEntity> selectCbConstruct(CriteriaBuilder cb, Root<JpaAdminEventEntity> root) {
return root;
}
@Override
protected void setEntityVersion(JpaRootEntity entity) {
entity.setEntityVersion(CURRENT_SCHEMA_VERSION_ADMIN_EVENT);
}
@Override
protected JpaModelCriteriaBuilder createJpaModelCriteriaBuilder() {
return new JpaAdminEventModelCriteriaBuilder();
}
@Override
protected MapAdminEventEntity mapToEntityDelegate(JpaAdminEventEntity original) {
return original;
}
}

View file

@ -0,0 +1,162 @@
/*
* 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.jpa.event.admin;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.models.map.storage.CriterionNotSupportedException;
import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder;
import org.keycloak.models.map.storage.jpa.event.admin.entity.JpaAdminEventEntity;
import org.keycloak.storage.SearchableModelField;
import org.keycloak.util.EnumWithStableIndex;
/**
* A {@link JpaModelCriteriaBuilder} implementation for admin events.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaAdminEventModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaAdminEventEntity, AdminEvent, JpaAdminEventModelCriteriaBuilder> {
private static final Map<String, String> FIELD_TO_JSON_PROP = new HashMap<>();
static {
FIELD_TO_JSON_PROP.put(AdminEvent.SearchableFields.AUTH_CLIENT_ID.getName(), "fAuthClientId");
FIELD_TO_JSON_PROP.put(AdminEvent.SearchableFields.AUTH_REALM_ID.getName(), "fAuthRealmId");
FIELD_TO_JSON_PROP.put(AdminEvent.SearchableFields.AUTH_USER_ID.getName(), "fAuthUserId");
FIELD_TO_JSON_PROP.put(AdminEvent.SearchableFields.AUTH_IP_ADDRESS.getName(), "fAuthIpAddress");
FIELD_TO_JSON_PROP.put(AdminEvent.SearchableFields.RESOURCE_PATH.getName(), "fResourcePath");
FIELD_TO_JSON_PROP.put(AdminEvent.SearchableFields.RESOURCE_TYPE.getName(), "fResourceType");
FIELD_TO_JSON_PROP.put(AdminEvent.SearchableFields.OPERATION_TYPE.getName(), "fOperationType");
}
public JpaAdminEventModelCriteriaBuilder() {
super(JpaAdminEventModelCriteriaBuilder::new);
}
private JpaAdminEventModelCriteriaBuilder(BiFunction<CriteriaBuilder, Root<JpaAdminEventEntity>, Predicate> predicateFunc) {
super(JpaAdminEventModelCriteriaBuilder::new, predicateFunc);
}
@Override
public JpaAdminEventModelCriteriaBuilder compare(SearchableModelField<? super AdminEvent> modelField, Operator op, Object... value) {
switch(op) {
case EQ:
if (modelField == AdminEvent.SearchableFields.REALM_ID) {
validateValue(value, modelField, op, String.class);
return new JpaAdminEventModelCriteriaBuilder((cb, root) ->
cb.equal(root.get(modelField.getName()), value[0])
);
} else if (modelField == AdminEvent.SearchableFields.AUTH_CLIENT_ID ||
modelField == AdminEvent.SearchableFields.AUTH_REALM_ID ||
modelField == AdminEvent.SearchableFields.AUTH_USER_ID ||
modelField == AdminEvent.SearchableFields.AUTH_IP_ADDRESS) {
validateValue(value, modelField, op, String.class);
return new JpaAdminEventModelCriteriaBuilder((cb, root) ->
cb.equal(
cb.function("->>", String.class, root.get("metadata"),
cb.literal(FIELD_TO_JSON_PROP.get(modelField.getName()))), value[0])
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case LE:
if (modelField == AdminEvent.SearchableFields.TIMESTAMP) {
validateValue(value, modelField, op, Number.class);
return new JpaAdminEventModelCriteriaBuilder((cb, root) ->
cb.le(root.get(modelField.getName()), (Number) value[0])
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case LT:
if (modelField == AdminEvent.SearchableFields.TIMESTAMP) {
validateValue(value, modelField, op, Number.class);
return new JpaAdminEventModelCriteriaBuilder((cb, root) ->
cb.lt(root.get(modelField.getName()), (Number) value[0])
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case LIKE:
if (modelField == AdminEvent.SearchableFields.RESOURCE_PATH) {
validateValue(value, modelField, op, String.class);
return new JpaAdminEventModelCriteriaBuilder((cb, root) ->
cb.like(
cb.function("->>", String.class, root.get("metadata"), cb.literal(FIELD_TO_JSON_PROP.get(modelField.getName()))),
value[0].toString())
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case GE:
if (modelField == AdminEvent.SearchableFields.TIMESTAMP) {
validateValue(value, modelField, op, Number.class);
return new JpaAdminEventModelCriteriaBuilder((cb, root) ->
cb.ge(root.get(modelField.getName()), (Number) value[0])
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case IN:
if (modelField == AdminEvent.SearchableFields.OPERATION_TYPE) {
Set<Integer> values = super.getValuesForInOperator(value, modelField).stream()
.map(o -> ((EnumWithStableIndex) o).getStableIndex()).collect(Collectors.toSet());
if (values.isEmpty()) return new JpaAdminEventModelCriteriaBuilder((cb, root) -> cb.or());
return new JpaAdminEventModelCriteriaBuilder((cb, root) -> {
CriteriaBuilder.In<Integer> in = cb.in(cb.function("->>", String.class, root.get("metadata"),
cb.literal(FIELD_TO_JSON_PROP.get(modelField.getName()))).as(Integer.class));
values.forEach(in::value);
return in;
});
}
else if (modelField == AdminEvent.SearchableFields.RESOURCE_TYPE) {
Set<String> values = super.getValuesForInOperator(value, modelField).stream()
.map(Object::toString).collect(Collectors.toSet());
if (values.isEmpty()) return new JpaAdminEventModelCriteriaBuilder((cb, root) -> cb.or());
return new JpaAdminEventModelCriteriaBuilder((cb, root) -> {
CriteriaBuilder.In<String> in = cb.in(cb.function("->>", String.class, root.get("metadata"),
cb.literal(FIELD_TO_JSON_PROP.get(modelField.getName()))));
values.forEach(in::value);
return in;
});
} else {
throw new CriterionNotSupportedException(modelField, op);
}
default:
throw new CriterionNotSupportedException(modelField, op);
}
}
}

View file

@ -0,0 +1,263 @@
/*
* 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.jpa.event.admin.entity;
import java.util.Objects;
import java.util.UUID;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.UuidValidator;
import org.keycloak.models.map.events.MapAdminEventEntity;
import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_ADMIN_EVENT;
/**
* JPA {@link MapAdminEventEntity} implementation. Some fields are annotated with {@code @Column(insertable = false, updatable = false)}
* to indicate that they are automatically generated from json fields. As such, these fields are non-insertable and non-updatable.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
@Entity
@Table(name = "kc_admin_event")
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
public class JpaAdminEventEntity extends MapAdminEventEntity.AbstractAdminEventEntity implements JpaRootVersionedEntity {
@Id
@Column
private UUID id;
//used for implicit optimistic locking
@Version
@Column
private int version;
@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
private final JpaAdminEventMetadata metadata;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private Integer entityVersion;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String realmId;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private Long timestamp;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private Long expiration;
/**
* No-argument constructor, used by hibernate to instantiate entities.
*/
public JpaAdminEventEntity() {
this.metadata = new JpaAdminEventMetadata();
}
public JpaAdminEventEntity(final DeepCloner cloner) {
this.metadata = new JpaAdminEventMetadata(cloner);
}
public boolean isMetadataInitialized() {
return metadata != null;
}
@Override
public Integer getEntityVersion() {
if (this.isMetadataInitialized()) return metadata.getEntityVersion();
return this.entityVersion;
}
@Override
public void setEntityVersion(final Integer entityVersion) {
this.metadata.setEntityVersion(entityVersion);
}
@Override
public int getVersion() {
return this.version;
}
@Override
public String getId() {
return id == null ? null : id.toString();
}
@Override
public void setId(final String id) {
String validatedId = UuidValidator.validateAndConvert(id);
this.id = UUID.fromString(validatedId);
}
@Override
public Integer getCurrentSchemaVersion() {
return CURRENT_SCHEMA_VERSION_ADMIN_EVENT;
}
@Override
public Long getExpiration() {
if (this.isMetadataInitialized()) return this.metadata.getExpiration();
return this.expiration;
}
@Override
public void setExpiration(final Long expiration) {
this.metadata.setExpiration(expiration);
}
@Override
public Long getTimestamp() {
if (this.isMetadataInitialized()) return this.metadata.getTimestamp();
return this.timestamp;
}
@Override
public void setTimestamp(final Long time) {
this.metadata.setTimestamp(time);
}
@Override
public String getRealmId() {
if (this.isMetadataInitialized()) return this.metadata.getRealmId();
return this.realmId;
}
@Override
public void setRealmId(final String realmId) {
this.metadata.setRealmId(realmId);
}
@Override
public OperationType getOperationType() {
return this.metadata.getOperationType();
}
@Override
public void setOperationType(final OperationType operationType) {
this.metadata.setOperationType(operationType);
}
@Override
public String getResourcePath() {
return this.metadata.getResourcePath();
}
@Override
public void setResourcePath(final String resourcePath) {
this.metadata.setResourcePath(resourcePath);
}
@Override
public String getResourceType() {
return this.metadata.getResourceType();
}
@Override
public void setResourceType(final String resourceType) {
this.metadata.setResourceType(resourceType);
}
@Override
public String getRepresentation() {
return this.metadata.getRepresentation();
}
@Override
public void setRepresentation(final String representation) {
this.metadata.setRepresentation(representation);
}
@Override
public String getError() {
return this.metadata.getError();
}
@Override
public void setError(final String error) {
this.metadata.setError(error);
}
@Override
public String getAuthClientId() {
return this.metadata.getAuthClientId();
}
@Override
public void setAuthClientId(final String clientId) {
this.metadata.setAuthClientId(clientId);
}
@Override
public String getAuthRealmId() {
return this.metadata.getAuthRealmId();
}
@Override
public void setAuthRealmId(final String realmId) {
this.metadata.setAuthRealmId(realmId);
}
@Override
public String getAuthUserId() {
return this.metadata.getAuthUserId();
}
@Override
public void setAuthUserId(final String userId) {
this.metadata.setAuthUserId(userId);
}
@Override
public String getAuthIpAddress() {
return this.metadata.getAuthIpAddress();
}
@Override
public void setAuthIpAddress(final String ipAddress) {
this.metadata.setAuthIpAddress(ipAddress);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof JpaAdminEventEntity)) return false;
return Objects.equals(getId(), ((JpaAdminEventEntity) obj).getId());
}
}

View file

@ -0,0 +1,48 @@
/*
* 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.jpa.event.admin.entity;
import java.io.Serializable;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.events.MapAdminEventEntityImpl;
/**
* Class that contains all the admin event metadata that is written as JSON into the database.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaAdminEventMetadata extends MapAdminEventEntityImpl implements Serializable {
public JpaAdminEventMetadata(final DeepCloner cloner) {
super(cloner);
}
public JpaAdminEventMetadata() {
super();
}
private Integer entityVersion;
public Integer getEntityVersion() {
return entityVersion;
}
public void setEntityVersion(Integer entityVersion) {
this.entityVersion = entityVersion;
}
}

View file

@ -0,0 +1,63 @@
/*
* 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.jpa.event.auth;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;
import org.keycloak.events.Event;
import org.keycloak.models.map.events.MapAuthEventEntity;
import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder;
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
import org.keycloak.models.map.storage.jpa.event.auth.entity.JpaAuthEventEntity;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTH_EVENT;
/**
* A {@link org.keycloak.models.map.storage.MapKeycloakTransaction} implementation for auth event entities.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaAuthEventMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaAuthEventEntity, MapAuthEventEntity, Event> {
public JpaAuthEventMapKeycloakTransaction(final EntityManager em) {
super(JpaAuthEventEntity.class, Event.class, em);
}
@Override
protected Selection<? extends JpaAuthEventEntity> selectCbConstruct(CriteriaBuilder cb, Root<JpaAuthEventEntity> root) {
return root;
}
@Override
protected void setEntityVersion(JpaRootEntity entity) {
entity.setEntityVersion(CURRENT_SCHEMA_VERSION_AUTH_EVENT);
}
@Override
protected JpaModelCriteriaBuilder createJpaModelCriteriaBuilder() {
return new JpaAuthEventModelCriteriaBuilder();
}
@Override
protected MapAuthEventEntity mapToEntityDelegate(JpaAuthEventEntity original) {
return original;
}
}

View file

@ -0,0 +1,128 @@
/*
* 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.jpa.event.auth;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.keycloak.events.Event;
import org.keycloak.models.map.storage.CriterionNotSupportedException;
import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder;
import org.keycloak.models.map.storage.jpa.event.auth.entity.JpaAuthEventEntity;
import org.keycloak.storage.SearchableModelField;
import org.keycloak.util.EnumWithStableIndex;
public class JpaAuthEventModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaAuthEventEntity, Event, JpaAuthEventModelCriteriaBuilder> {
private static final Map<String, String> FIELD_TO_JSON_PROP = new HashMap<>();
static {
FIELD_TO_JSON_PROP.put(Event.SearchableFields.CLIENT_ID.getName(), "fClientId");
FIELD_TO_JSON_PROP.put(Event.SearchableFields.USER_ID.getName(), "fUserId");
FIELD_TO_JSON_PROP.put(Event.SearchableFields.IP_ADDRESS.getName(), "fIpAddress");
FIELD_TO_JSON_PROP.put(Event.SearchableFields.EVENT_TYPE.getName(), "fType");
}
public JpaAuthEventModelCriteriaBuilder() {
super(JpaAuthEventModelCriteriaBuilder::new);
}
private JpaAuthEventModelCriteriaBuilder(BiFunction<CriteriaBuilder, Root<JpaAuthEventEntity>, Predicate> predicateFunc) {
super(JpaAuthEventModelCriteriaBuilder::new, predicateFunc);
}
@Override
public JpaAuthEventModelCriteriaBuilder compare(SearchableModelField<? super Event> modelField, Operator op, Object... value) {
switch (op) {
case EQ:
if (modelField == Event.SearchableFields.REALM_ID) {
validateValue(value, modelField, op, String.class);
return new JpaAuthEventModelCriteriaBuilder((cb, root) ->
cb.equal(root.get(modelField.getName()), value[0])
);
} else if (modelField == Event.SearchableFields.CLIENT_ID ||
modelField == Event.SearchableFields.USER_ID ||
modelField == Event.SearchableFields.IP_ADDRESS) {
validateValue(value, modelField, op, String.class);
return new JpaAuthEventModelCriteriaBuilder((cb, root) ->
cb.equal(
cb.function("->>", String.class, root.get("metadata"),
cb.literal(FIELD_TO_JSON_PROP.get(modelField.getName()))), value[0])
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case LE:
if (modelField == Event.SearchableFields.TIMESTAMP) {
validateValue(value, modelField, op, Number.class);
return new JpaAuthEventModelCriteriaBuilder((cb, root) ->
cb.le(root.get(modelField.getName()), (Number) value[0])
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case LT:
if (modelField == Event.SearchableFields.TIMESTAMP) {
validateValue(value, modelField, op, Number.class);
return new JpaAuthEventModelCriteriaBuilder((cb, root) ->
cb.lt(root.get(modelField.getName()), (Number) value[0])
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case GE:
if (modelField == Event.SearchableFields.TIMESTAMP) {
validateValue(value, modelField, op, Number.class);
return new JpaAuthEventModelCriteriaBuilder((cb, root) ->
cb.ge(root.get(modelField.getName()), (Number) value[0])
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case IN:
if (modelField == Event.SearchableFields.EVENT_TYPE) {
Set<Integer> values = super.getValuesForInOperator(value, modelField).stream()
.map(o -> ((EnumWithStableIndex) o).getStableIndex()).collect(Collectors.toSet());
if (values.isEmpty()) return new JpaAuthEventModelCriteriaBuilder((cb, root) -> cb.or());
return new JpaAuthEventModelCriteriaBuilder((cb, root) -> {
CriteriaBuilder.In<Integer> in = cb.in(cb.function("->>", String.class, root.get("metadata"),
cb.literal(FIELD_TO_JSON_PROP.get(modelField.getName()))).as(Integer.class));
values.forEach(in::value);
return in;
});
} else {
throw new CriterionNotSupportedException(modelField, op);
}
default:
throw new CriterionNotSupportedException(modelField, op);
}
}
}

View file

@ -0,0 +1,43 @@
/*
* 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.jpa.event.auth.entity;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.keycloak.models.map.storage.jpa.JpaAttributeEntity;
/**
* JPA implementation for auth event details.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
@Entity
@Table(name = "kc_auth_event_detail", uniqueConstraints = {
@UniqueConstraint(columnNames = {"fk_root", "name", "value"})
})
public class JpaAuthEventDetailEntity extends JpaAttributeEntity<JpaAuthEventEntity> {
public JpaAuthEventDetailEntity() {
}
public JpaAuthEventDetailEntity(final JpaAuthEventEntity root, final String name, final String value) {
super(root, name, value);
}
}

View file

@ -0,0 +1,255 @@
/*
* 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.jpa.event.auth.entity;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Version;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;
import org.keycloak.events.EventType;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.UuidValidator;
import org.keycloak.models.map.events.MapAuthEventEntity;
import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTH_EVENT;
/**
* JPA {@link MapAuthEventEntity} implementation. Some fields are annotated with {@code @Column(insertable = false, updatable = false)}
* to indicate that they are automatically generated from json fields. As such, these fields are non-insertable and non-updatable.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
@Entity
@Table(name = "kc_auth_event")
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
public class JpaAuthEventEntity extends MapAuthEventEntity.AbstractAuthEventEntity implements JpaRootVersionedEntity {
@Id
@Column
private UUID id;
//used for implicit optimistic locking
@Version
@Column
private int version;
@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
private final JpaAuthEventMetadata metadata;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private Integer entityVersion;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String realmId;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private Long timestamp;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private Long expiration;
@OneToMany(mappedBy = "root", cascade = CascadeType.PERSIST, orphanRemoval = true)
private final Set<JpaAuthEventDetailEntity> details = new HashSet<>();
/**
* No-argument constructor, used by hibernate to instantiate entities.
*/
public JpaAuthEventEntity() {
this.metadata = new JpaAuthEventMetadata();
}
public JpaAuthEventEntity(final DeepCloner cloner) {
this.metadata = new JpaAuthEventMetadata(cloner);
}
public boolean isMetadataInitialized() {
return metadata != null;
}
@Override
public Integer getCurrentSchemaVersion() {
return CURRENT_SCHEMA_VERSION_AUTH_EVENT;
}
@Override
public Integer getEntityVersion() {
if (this.isMetadataInitialized()) return metadata.getEntityVersion();
return this.entityVersion;
}
@Override
public void setEntityVersion(final Integer entityVersion) {
this.metadata.setEntityVersion(entityVersion);
}
@Override
public int getVersion() {
return this.version;
}
@Override
public String getId() {
return id == null ? null : id.toString();
}
@Override
public void setId(final String id) {
String validatedId = UuidValidator.validateAndConvert(id);
this.id = UUID.fromString(validatedId);
}
@Override
public Long getExpiration() {
if (this.isMetadataInitialized()) return this.metadata.getExpiration();
return this.expiration;
}
@Override
public void setExpiration(final Long expiration) {
this.metadata.setExpiration(expiration);
}
@Override
public Long getTimestamp() {
if (this.isMetadataInitialized()) return this.metadata.getTimestamp();
return this.timestamp;
}
@Override
public void setTimestamp(final Long timestamp) {
this.metadata.setTimestamp(timestamp);
}
@Override
public String getClientId() {
return this.metadata.getClientId();
}
@Override
public void setClientId(final String clientId) {
this.metadata.setClientId(clientId);
}
@Override
public Map<String, String> getDetails() {
return this.details.stream().collect(Collectors.toMap(JpaAuthEventDetailEntity::getName, JpaAuthEventDetailEntity::getValue));
}
@Override
public void setDetails(final Map<String, String> details) {
this.details.clear();
if (details != null) {
details.forEach((key, value) -> this.details.add(new JpaAuthEventDetailEntity(this, key, value)));
}
}
@Override
public String getError() {
return this.metadata.getError();
}
@Override
public void setError(final String error) {
this.metadata.setError(error);
}
@Override
public String getIpAddress() {
return this.metadata.getIpAddress();
}
@Override
public void setIpAddress(final String ipAddress) {
this.metadata.setIpAddress(ipAddress);
}
@Override
public String getRealmId() {
if (this.isMetadataInitialized()) return this.metadata.getRealmId();
return this.realmId;
}
@Override
public void setRealmId(final String realmId) {
this.metadata.setRealmId(realmId);
}
@Override
public String getSessionId() {
return this.metadata.getSessionId();
}
@Override
public void setSessionId(final String sessionId) {
this.metadata.setSessionId(sessionId);
}
@Override
public String getUserId() {
return this.metadata.getUserId();
}
@Override
public void setUserId(final String userId) {
this.metadata.setUserId(userId);
}
@Override
public EventType getType() {
return this.metadata.getType();
}
@Override
public void setType(final EventType type) {
this.metadata.setType(type);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public boolean equals(final Object obj) {
if (this == obj) return true;
if (!(obj instanceof JpaAuthEventEntity)) return false;
return Objects.equals(getId(), ((JpaAuthEventEntity) obj).getId());
}
}

View file

@ -0,0 +1,48 @@
/*
* 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.jpa.event.auth.entity;
import java.io.Serializable;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.events.MapAuthEventEntityImpl;
/**
* Class that contains all the auth event metadata that is written as JSON into the database.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaAuthEventMetadata extends MapAuthEventEntityImpl implements Serializable {
public JpaAuthEventMetadata(final DeepCloner cloner) {
super(cloner);
}
public JpaAuthEventMetadata() {
super();
}
private Integer entityVersion;
public Integer getEntityVersion() {
return entityVersion;
}
public void setEntityVersion(Integer entityVersion) {
this.entityVersion = entityVersion;
}
}

View file

@ -25,6 +25,7 @@ public class JsonbPostgreSQL95Dialect extends PostgreSQL95Dialect {
public JsonbPostgreSQL95Dialect() {
super();
registerFunction("->", new SQLFunctionTemplate(JsonbType.INSTANCE, "?1->?2"));
registerFunction("->>", new SQLFunctionTemplate(StandardBasicTypes.STRING, "?1->>?2"));
registerFunction("@>", new SQLFunctionTemplate(StandardBasicTypes.BOOLEAN, "?1@>?2::jsonb"));
}
}

View file

@ -32,7 +32,11 @@ import org.keycloak.models.map.storage.jpa.authorization.scope.entity.JpaScopeMe
import org.keycloak.models.map.storage.jpa.authorization.resourceServer.entity.JpaResourceServerMetadata;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientMetadata;
import org.keycloak.models.map.storage.jpa.clientscope.entity.JpaClientScopeMetadata;
import org.keycloak.models.map.storage.jpa.event.admin.entity.JpaAdminEventMetadata;
import org.keycloak.models.map.storage.jpa.event.auth.entity.JpaAuthEventMetadata;
import org.keycloak.models.map.storage.jpa.group.entity.JpaGroupMetadata;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaAdminEventMigration;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaAuthEventMigration;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaAuthenticationSessionMigration;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaClientMigration;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaClientScopeMigration;
@ -52,11 +56,13 @@ import org.keycloak.models.map.storage.jpa.realm.entity.JpaComponentMetadata;
import org.keycloak.models.map.storage.jpa.realm.entity.JpaRealmMetadata;
import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleMetadata;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_ADMIN_EVENT;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTHZ_PERMISSION;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTHZ_POLICY;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTHZ_RESOURCE;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTHZ_RESOURCE_SERVER;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTHZ_SCOPE;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTH_EVENT;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTH_SESSION;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_CLIENT;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_CLIENT_SCOPE;
@ -85,6 +91,10 @@ public class JpaEntityMigration {
MIGRATIONS.put(JpaResourceMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_AUTHZ_RESOURCE, tree, JpaResourceMigration.MIGRATORS));
MIGRATIONS.put(JpaResourceServerMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_AUTHZ_RESOURCE_SERVER, tree, JpaResourceServerMigration.MIGRATORS));
MIGRATIONS.put(JpaScopeMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_AUTHZ_SCOPE, tree, JpaScopeMigration.MIGRATORS));
// events
MIGRATIONS.put(JpaAdminEventMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_ADMIN_EVENT, tree, JpaAdminEventMigration.MIGRATORS));
MIGRATIONS.put(JpaAuthEventMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_AUTH_EVENT, tree, JpaAuthEventMigration.MIGRATORS));
}
private static ObjectNode migrateTreeTo(int entityVersion, Integer supportedVersion, ObjectNode node, List<Function<ObjectNode, ObjectNode>> migrators) {

View file

@ -0,0 +1,34 @@
/*
* 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.jpa.hibernate.jsonb.migration;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
/**
* Migration functions for admin events.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaAdminEventMigration {
public static final List<Function<ObjectNode, ObjectNode>> MIGRATORS = Arrays.asList(
o -> o // no migration yet
);
}

View file

@ -0,0 +1,34 @@
/*
* 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.jpa.hibernate.jsonb.migration;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
/**
* Migration functions for authentication events.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaAuthEventMigration {
public static final List<Function<ObjectNode, ObjectNode>> MIGRATORS = Arrays.asList(
o -> o // no migration yet
);
}

View file

@ -53,8 +53,10 @@ import liquibase.statement.SqlStatement;
* &lt;/ext:createJsonIndex&gt;
* &lt;/changeSet&gt;
* </pre>
* The above configuration is creating an index for the {@code name} property of JSON files stored in column {@code metadata} in
* table {@code test}.
* The above configuration is creating an inverted (GIN) index for the {@code name} property of JSON files stored in column
* {@code metadata} in table {@code test}.
* <p/>
* The {@code jsonProperty} is optional - when it is absent the index will be created for the whole JSON.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/

View file

@ -62,7 +62,6 @@ public class CreateJsonIndexGenerator extends AbstractSqlGenerator<CreateJsonInd
Arrays.stream(createIndexStatement.getColumns()).map(JsonEnabledColumnConfig.class::cast)
.forEach(config -> {
validationErrors.checkRequiredField("jsonColumn", config.getJsonColumn());
validationErrors.checkRequiredField("jsonProperty", config.getJsonProperty());
});
return validationErrors;
}
@ -100,13 +99,16 @@ public class CreateJsonIndexGenerator extends AbstractSqlGenerator<CreateJsonInd
if (database instanceof CockroachDatabase) {
builder.append(" USING gin (");
builder.append(Arrays.stream(statement.getColumns()).map(JsonEnabledColumnConfig.class::cast)
.map(c -> "(" + c.getJsonColumn() + "->'" + c.getJsonProperty() + "')")
.map(c -> c.getJsonProperty() == null ? c.getJsonColumn() :
"(" + c.getJsonColumn() + "->'" + c.getJsonProperty() + "')")
.collect(Collectors.joining(", ")))
.append(")");
}
else if (database instanceof PostgresDatabase) { builder.append(" USING gin (");
else if (database instanceof PostgresDatabase) {
builder.append(" USING gin (");
builder.append(Arrays.stream(statement.getColumns()).map(JsonEnabledColumnConfig.class::cast)
.map(c -> "(" + c.getJsonColumn() + "->'" + c.getJsonProperty() + "') jsonb_path_ops")
.map(c -> c.getJsonProperty() == null ? c.getJsonColumn() :
"(" + c.getJsonColumn() + "->'" + c.getJsonProperty() + "') jsonb_path_ops")
.collect(Collectors.joining(", ")))
.append(")");
}

View file

@ -194,6 +194,10 @@ public class MapJpaLiquibaseUpdaterProvider implements MapJpaUpdaterProvider {
// for authorization services there is used single name for all modelTypes
modelName = modelName.startsWith("authz-") ? "authz" : modelName;
// for events, map both event types to a single changelog name
if (modelName.equals("auth-events") || modelName.equals("admin-events"))
modelName = "events";
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
// if database is cockroachdb, use the aggregate changelog (see GHI #11230).
String changelog = database instanceof CockroachDatabase ? "META-INF/jpa-aggregate-changelog.xml" : "META-INF/jpa-" + modelName + "-changelog.xml";

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet author="keycloak" id="admin-events-1">
<createTable tableName="kc_admin_event">
<column name="id" type="UUID">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="version" type="INTEGER" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="metadata" type="json"/>
</createTable>
<ext:addGeneratedColumn tableName="kc_admin_event">
<ext:column name="entityversion" type="INTEGER" jsonColumn="metadata" jsonProperty="entityVersion"/>
<ext:column name="realmid" type="KC_KEY" jsonColumn="metadata" jsonProperty="fRealmId"/>
<ext:column name="timestamp" type="BIGINT" jsonColumn="metadata" jsonProperty="fTimestamp"/>
<ext:column name="expiration" type="BIGINT" jsonColumn="metadata" jsonProperty="fExpiration"/>
</ext:addGeneratedColumn>
<createIndex tableName="kc_admin_event" indexName="admin_event_entityversion">
<column name="entityversion"/>
</createIndex>
<createIndex tableName="kc_admin_event" indexName="admin_event_expiration">
<column name="expiration"/>
</createIndex>
<createIndex tableName="kc_admin_event" indexName="admin_event_realmid_timestamp">
<column name="realmid"/>
<column name="timestamp"/>
</createIndex>
</changeSet>
</databaseChangeLog>

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet author="keycloak" id="auth-events-1">
<createTable tableName="kc_auth_event">
<column name="id" type="UUID">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="version" type="INTEGER" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="metadata" type="json"/>
</createTable>
<ext:addGeneratedColumn tableName="kc_auth_event">
<ext:column name="entityversion" type="INTEGER" jsonColumn="metadata" jsonProperty="entityVersion"/>
<ext:column name="realmid" type="KC_KEY" jsonColumn="metadata" jsonProperty="fRealmId"/>
<ext:column name="timestamp" type="BIGINT" jsonColumn="metadata" jsonProperty="fTimestamp"/>
<ext:column name="expiration" type="BIGINT" jsonColumn="metadata" jsonProperty="fExpiration"/>
</ext:addGeneratedColumn>
<createIndex tableName="kc_auth_event" indexName="auth_event_entityversion">
<column name="entityversion"/>
</createIndex>
<createIndex tableName="kc_auth_event" indexName="auth_event_expiration">
<column name="expiration"/>
</createIndex>
<createIndex tableName="kc_auth_event" indexName="auth_event_realmid_timestamp">
<column name="realmid"/>
<column name="timestamp"/>
</createIndex>
<createTable tableName="kc_auth_event_detail">
<column name="id" type="UUID">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="fk_root" type="UUID">
<constraints foreignKeyName="auth_event_detail_fk_root_fkey" references="kc_auth_event(id)" deleteCascade="true"/>
</column>
<column name="name" type="VARCHAR(255)"/>
<column name="value" type="TEXT"/>
</createTable>
<createIndex tableName="kc_auth_event_detail" indexName="auth_event_detail_fk_root">
<column name="fk_root"/>
</createIndex>
</changeSet>
<changeSet author="keycloak" id="auth-events-2" dbms="postgresql">
<!-- this is deferrable and initiallyDeferred as hibernate will first insert new entries and then delete the old by default -->
<!-- this will not work on cockroachdb as deferred indexes are not supported in version 22.1 yet, therefore, only run it on postgresql -->
<!-- see https://go.crdb.dev/issue-v/31632/v21.2 for the current status of the implementation -->
<addUniqueConstraint tableName="kc_auth_event_detail" columnNames="fk_root, name, value" deferrable="true" initiallyDeferred="true" />
</changeSet>
</databaseChangeLog>

View file

@ -23,6 +23,7 @@ limitations under the License.
<include file="META-INF/jpa-authz-changelog.xml"/>
<include file="META-INF/jpa-client-scopes-changelog.xml"/>
<include file="META-INF/jpa-clients-changelog.xml"/>
<include file="META-INF/jpa-events-changelog.xml"/>
<include file="META-INF/jpa-groups-changelog.xml"/>
<include file="META-INF/jpa-realms-changelog.xml"/>
<include file="META-INF/jpa-roles-changelog.xml"/>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<include file="META-INF/events/admin-events/jpa-admin-events-changelog-1.xml"/>
<include file="META-INF/events/auth-events/jpa-auth-events-changelog-1.xml"/>
</databaseChangeLog>

View file

@ -18,6 +18,10 @@
<!--clients-->
<class>org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity</class>
<class>org.keycloak.models.map.storage.jpa.client.entity.JpaClientAttributeEntity</class>
<!--events-->
<class>org.keycloak.models.map.storage.jpa.event.admin.entity.JpaAdminEventEntity</class>
<class>org.keycloak.models.map.storage.jpa.event.auth.entity.JpaAuthEventEntity</class>
<class>org.keycloak.models.map.storage.jpa.event.auth.entity.JpaAuthEventDetailEntity</class>
<!--groups-->
<class>org.keycloak.models.map.storage.jpa.group.entity.JpaGroupEntity</class>
<class>org.keycloak.models.map.storage.jpa.group.entity.JpaGroupAttributeEntity</class>

View file

@ -17,13 +17,11 @@
package org.keycloak.models.map.events;
import org.keycloak.common.util.Time;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.admin.AdminEvent.SearchableFields;
import org.keycloak.events.admin.AdminEventQuery;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.models.map.storage.QueryParameters;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
@ -45,7 +43,6 @@ public class MapAdminEventQuery implements AdminEventQuery {
private Integer firstResult;
private Integer maxResults;
private DefaultModelCriteria<AdminEvent> mcb = criteria();
private final DefaultModelCriteria<AdminEvent> criteria = criteria();
private final Function<QueryParameters<AdminEvent>, Stream<AdminEvent>> resultProducer;
public MapAdminEventQuery(Function<QueryParameters<AdminEvent>, Stream<AdminEvent>> resultProducer) {

View file

@ -17,12 +17,10 @@
package org.keycloak.models.map.events;
import org.keycloak.common.util.Time;
import org.keycloak.events.Event;
import org.keycloak.events.Event.SearchableFields;
import org.keycloak.events.EventQuery;
import org.keycloak.events.EventType;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.models.map.storage.QueryParameters;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
@ -43,7 +41,6 @@ public class MapAuthEventQuery implements EventQuery {
private Integer firstResult;
private Integer maxResults;
private DefaultModelCriteria<Event> mcb = criteria();
private final DefaultModelCriteria<Event> criteria = criteria();
private final Function<QueryParameters<Event>, Stream<Event>> resultProducer;
public MapAuthEventQuery(Function<QueryParameters<Event>, Stream<Event>> resultProducer) {

View file

@ -890,6 +890,8 @@
<keycloak.authSession.map.storage.provider>jpa</keycloak.authSession.map.storage.provider>
<keycloak.client.map.storage.provider>jpa</keycloak.client.map.storage.provider>
<keycloak.clientScope.map.storage.provider>jpa</keycloak.clientScope.map.storage.provider>
<keycloak.adminEventsStore.map.storage.provider>jpa</keycloak.adminEventsStore.map.storage.provider>
<keycloak.authEventsStore.map.storage.provider>jpa</keycloak.authEventsStore.map.storage.provider>
<keycloak.group.map.storage.provider>jpa</keycloak.group.map.storage.provider>
<keycloak.loginFailure.map.storage.provider>jpa</keycloak.loginFailure.map.storage.provider>
<keycloak.realm.map.storage.provider>jpa</keycloak.realm.map.storage.provider>

View file

@ -28,10 +28,10 @@
},
"map": {
"storage-admin-events": {
"provider": "${keycloak.eventStore.map.storage.provider:concurrenthashmap}"
"provider": "${keycloak.adminEventsStore.map.storage.provider:concurrenthashmap}"
},
"storage-auth-events": {
"provider": "${keycloak.eventStore.map.storage.provider:concurrenthashmap}"
"provider": "${keycloak.authEventsStore.map.storage.provider:concurrenthashmap}"
}
}
},

View file

@ -101,7 +101,7 @@ public class JpaMapStorage extends KeycloakModelParameters {
.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)
.config("storage-auth-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID);
.spi(EventStoreSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID) .config("storage-admin-events.provider", JpaMapStorageProviderFactory.PROVIDER_ID)
.config("storage-auth-events.provider", JpaMapStorageProviderFactory.PROVIDER_ID);
}
}

View file

@ -12,12 +12,15 @@
"eventsStore": {
"provider": "${keycloak.eventsStore.provider:jpa}",
"jpa": {
"max-detail-length": "${keycloak.eventsStore.maxDetailLength:1000}"
},
"map": {
"storage-admin-events": {
"provider": "${keycloak.eventStore.map.storage.provider:concurrenthashmap}"
"provider": "${keycloak.adminEventsStore.map.storage.provider:concurrenthashmap}"
},
"storage-auth-events": {
"provider": "${keycloak.eventStore.map.storage.provider:concurrenthashmap}"
"provider": "${keycloak.authEventsStore.map.storage.provider:concurrenthashmap}"
}
}
},