diff --git a/backend/src/main/java/ervu_business_metrics/dao/FailedAuthDao.java b/backend/src/main/java/ervu_business_metrics/dao/FailedAuthDao.java new file mode 100644 index 0000000..b5483d6 --- /dev/null +++ b/backend/src/main/java/ervu_business_metrics/dao/FailedAuthDao.java @@ -0,0 +1,37 @@ +package ervu_business_metrics.dao; + +import java.sql.Timestamp; + +import ervu_business_metrics.config.KafkaEnabledCondition; +import ervu_business_metrics.model.sso.AuthEventResponse; +import org.jooq.DSLContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Repository; + +import static ru.micord.webbpm.ervu.business_metrics.db_beans.auth.Tables.FAILED_AUTH; + +/** + * @author gulnaz + */ +@Repository +@Conditional(KafkaEnabledCondition.class) +public class FailedAuthDao { + + private final DSLContext dslContext; + + public FailedAuthDao(DSLContext dslContext) { + this.dslContext = dslContext; + } + + public void save(AuthEventResponse authEventResponse) { + dslContext.insertInto(FAILED_AUTH) + .set(FAILED_AUTH.ID, authEventResponse.id()) + .set(FAILED_AUTH.USER_ID, authEventResponse.userId()) + .set(FAILED_AUTH.DATETIME, new Timestamp(Long.parseLong(authEventResponse.timeCreated()))) + .execute(); + } + + public void clear() { + dslContext.truncate(FAILED_AUTH).execute(); + } +} diff --git a/backend/src/main/java/ervu_business_metrics/kafka/listener/FailedAuthListener.java b/backend/src/main/java/ervu_business_metrics/kafka/listener/FailedAuthListener.java new file mode 100644 index 0000000..28676fa --- /dev/null +++ b/backend/src/main/java/ervu_business_metrics/kafka/listener/FailedAuthListener.java @@ -0,0 +1,44 @@ +package ervu_business_metrics.kafka.listener; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import ervu_business_metrics.config.KafkaEnabledCondition; +import ervu_business_metrics.model.sso.AuthEventResponse; +import ervu_business_metrics.service.FailedAuthService; +import exception.JsonParseException; +import org.springframework.context.annotation.Conditional; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +/** + * @author gulnaz + */ +@Component +@Conditional(KafkaEnabledCondition.class) +public class FailedAuthListener { + private static final String FAILED_AUTH_EVENT = "USER_AUTHENTICATION_ERROR"; + + private final ObjectMapper objectMapper; + private final FailedAuthService failedAuthService; + + public FailedAuthListener(ObjectMapper objectMapper, FailedAuthService failedAuthService) { + this.objectMapper = objectMapper; + this.failedAuthService = failedAuthService; + } + + @KafkaListener(id = "${kafka.auth.events.group.id}", topics = "${kafka.sso.auth.events.response}") + public void listenFailedAuthEvent(String kafkaMessage) { + AuthEventResponse response; + + try { + response = objectMapper.readValue(kafkaMessage, AuthEventResponse.class); + } + catch (JsonProcessingException e) { + throw new JsonParseException(e); + } + + if (response.userId() != null && FAILED_AUTH_EVENT.equals(response.eventType())) { + failedAuthService.save(response); + } + } +} diff --git a/backend/src/main/java/ervu_business_metrics/model/sso/AuthEventResponse.java b/backend/src/main/java/ervu_business_metrics/model/sso/AuthEventResponse.java new file mode 100644 index 0000000..de65571 --- /dev/null +++ b/backend/src/main/java/ervu_business_metrics/model/sso/AuthEventResponse.java @@ -0,0 +1,10 @@ +package ervu_business_metrics.model.sso; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * @author gulnaz + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record AuthEventResponse(String id, String userId, String eventType, String timeCreated) { +} diff --git a/backend/src/main/java/ervu_business_metrics/service/FailedAuthService.java b/backend/src/main/java/ervu_business_metrics/service/FailedAuthService.java new file mode 100644 index 0000000..037bf20 --- /dev/null +++ b/backend/src/main/java/ervu_business_metrics/service/FailedAuthService.java @@ -0,0 +1,26 @@ +package ervu_business_metrics.service; + +import ervu_business_metrics.dao.FailedAuthDao; +import ervu_business_metrics.model.sso.AuthEventResponse; +import org.springframework.stereotype.Service; + +/** + * @author gulnaz + */ +@Service +public class FailedAuthService { + + private final FailedAuthDao failedAuthDao; + + public FailedAuthService(FailedAuthDao failedAuthDao) { + this.failedAuthDao = failedAuthDao; + } + + public void save(AuthEventResponse authEventResponse) { + failedAuthDao.save(authEventResponse); + } + + public void clear() { + failedAuthDao.clear(); + } +} diff --git a/backend/src/main/java/ervu_business_metrics/service/scheduler/FailedAuthSchedulerService.java b/backend/src/main/java/ervu_business_metrics/service/scheduler/FailedAuthSchedulerService.java new file mode 100644 index 0000000..4318212 --- /dev/null +++ b/backend/src/main/java/ervu_business_metrics/service/scheduler/FailedAuthSchedulerService.java @@ -0,0 +1,28 @@ +package ervu_business_metrics.service.scheduler; + +import ervu_business_metrics.config.KafkaEnabledCondition; +import ervu_business_metrics.service.FailedAuthService; +import net.javacrumbs.shedlock.core.SchedulerLock; +import org.springframework.context.annotation.Conditional; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +/** + * @author gulnaz + */ +@Service +@Conditional(KafkaEnabledCondition.class) +public class FailedAuthSchedulerService { + + private final FailedAuthService failedAuthService; + + public FailedAuthSchedulerService(FailedAuthService failedAuthService) { + this.failedAuthService = failedAuthService; + } + + @Scheduled(cron = "${scheduler.failed_auth_cleanup.cron:0 0 0 * * *}") + @SchedulerLock(name = "clearFailedAuth") + public void clear() { + failedAuthService.clear(); + } +} diff --git a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Auth.java b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Auth.java index 193c038..5ad6d3e 100644 --- a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Auth.java +++ b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Auth.java @@ -13,6 +13,7 @@ import org.jooq.impl.SchemaImpl; import ru.micord.webbpm.ervu.business_metrics.db_beans.DefaultCatalog; import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.ActiveSession; +import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.FailedAuth; /** @@ -33,6 +34,11 @@ public class Auth extends SchemaImpl { */ public final ActiveSession ACTIVE_SESSION = ActiveSession.ACTIVE_SESSION; + /** + * События неуспешной аутентификации + */ + public final FailedAuth FAILED_AUTH = FailedAuth.FAILED_AUTH; + /** * No further instances allowed */ @@ -49,7 +55,8 @@ public class Auth extends SchemaImpl { @Override public final List> getTables() { return Arrays.asList( - ActiveSession.ACTIVE_SESSION + ActiveSession.ACTIVE_SESSION, + FailedAuth.FAILED_AUTH ); } } diff --git a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Keys.java b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Keys.java index cdff6e9..b6df807 100644 --- a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Keys.java +++ b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Keys.java @@ -10,7 +10,9 @@ import org.jooq.impl.DSL; import org.jooq.impl.Internal; import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.ActiveSession; +import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.FailedAuth; import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.records.ActiveSessionRecord; +import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.records.FailedAuthRecord; /** @@ -25,4 +27,5 @@ public class Keys { // ------------------------------------------------------------------------- public static final UniqueKey PK_ACTIVE_SESSION = Internal.createUniqueKey(ActiveSession.ACTIVE_SESSION, DSL.name("pk_active_session"), new TableField[] { ActiveSession.ACTIVE_SESSION.SESSION_ID, ActiveSession.ACTIVE_SESSION.DOMAIN_ID }, true); + public static final UniqueKey FAILED_AUTH_PKEY = Internal.createUniqueKey(FailedAuth.FAILED_AUTH, DSL.name("failed_auth_pkey"), new TableField[] { FailedAuth.FAILED_AUTH.ID }, true); } diff --git a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Tables.java b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Tables.java index a956554..f657710 100644 --- a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Tables.java +++ b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/Tables.java @@ -5,6 +5,7 @@ package ru.micord.webbpm.ervu.business_metrics.db_beans.auth; import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.ActiveSession; +import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.FailedAuth; /** @@ -17,4 +18,9 @@ public class Tables { * The table auth.active_session. */ public static final ActiveSession ACTIVE_SESSION = ActiveSession.ACTIVE_SESSION; + + /** + * События неуспешной аутентификации + */ + public static final FailedAuth FAILED_AUTH = FailedAuth.FAILED_AUTH; } diff --git a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/tables/FailedAuth.java b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/tables/FailedAuth.java new file mode 100644 index 0000000..c18de5b --- /dev/null +++ b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/tables/FailedAuth.java @@ -0,0 +1,230 @@ +/* + * This file is generated by jOOQ. + */ +package ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables; + + +import java.sql.Timestamp; +import java.util.Collection; + +import org.jooq.Condition; +import org.jooq.Field; +import org.jooq.Name; +import org.jooq.PlainSQL; +import org.jooq.QueryPart; +import org.jooq.SQL; +import org.jooq.Schema; +import org.jooq.Select; +import org.jooq.Stringly; +import org.jooq.Table; +import org.jooq.TableField; +import org.jooq.TableOptions; +import org.jooq.UniqueKey; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; +import org.jooq.impl.TableImpl; + +import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.Auth; +import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.Keys; +import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.records.FailedAuthRecord; + + +/** + * События неуспешной аутентификации + */ +@SuppressWarnings({ "all", "unchecked", "rawtypes" }) +public class FailedAuth extends TableImpl { + + private static final long serialVersionUID = 1L; + + /** + * The reference instance of auth.failed_auth + */ + public static final FailedAuth FAILED_AUTH = new FailedAuth(); + + /** + * The class holding records for this type + */ + @Override + public Class getRecordType() { + return FailedAuthRecord.class; + } + + /** + * The column auth.failed_auth.id. Идентификатор события + */ + public final TableField ID = createField(DSL.name("id"), SQLDataType.VARCHAR(128).nullable(false), this, "Идентификатор события"); + + /** + * The column auth.failed_auth.user_id. Идентификатор + * пользователя + */ + public final TableField USER_ID = createField(DSL.name("user_id"), SQLDataType.VARCHAR(128).nullable(false), this, "Идентификатор пользователя"); + + /** + * The column auth.failed_auth.datetime. Дата и время события + */ + public final TableField DATETIME = createField(DSL.name("datetime"), SQLDataType.TIMESTAMP(0), this, "Дата и время события"); + + private FailedAuth(Name alias, Table aliased) { + this(alias, aliased, (Field[]) null, null); + } + + private FailedAuth(Name alias, Table aliased, Field[] parameters, Condition where) { + super(alias, null, aliased, parameters, DSL.comment("События неуспешной аутентификации"), TableOptions.table(), where); + } + + /** + * Create an aliased auth.failed_auth table reference + */ + public FailedAuth(String alias) { + this(DSL.name(alias), FAILED_AUTH); + } + + /** + * Create an aliased auth.failed_auth table reference + */ + public FailedAuth(Name alias) { + this(alias, FAILED_AUTH); + } + + /** + * Create a auth.failed_auth table reference + */ + public FailedAuth() { + this(DSL.name("failed_auth"), null); + } + + @Override + public Schema getSchema() { + return aliased() ? null : Auth.AUTH; + } + + @Override + public UniqueKey getPrimaryKey() { + return Keys.FAILED_AUTH_PKEY; + } + + @Override + public FailedAuth as(String alias) { + return new FailedAuth(DSL.name(alias), this); + } + + @Override + public FailedAuth as(Name alias) { + return new FailedAuth(alias, this); + } + + @Override + public FailedAuth as(Table alias) { + return new FailedAuth(alias.getQualifiedName(), this); + } + + /** + * Rename this table + */ + @Override + public FailedAuth rename(String name) { + return new FailedAuth(DSL.name(name), null); + } + + /** + * Rename this table + */ + @Override + public FailedAuth rename(Name name) { + return new FailedAuth(name, null); + } + + /** + * Rename this table + */ + @Override + public FailedAuth rename(Table name) { + return new FailedAuth(name.getQualifiedName(), null); + } + + /** + * Create an inline derived table from this table + */ + @Override + public FailedAuth where(Condition condition) { + return new FailedAuth(getQualifiedName(), aliased() ? this : null, null, condition); + } + + /** + * Create an inline derived table from this table + */ + @Override + public FailedAuth where(Collection conditions) { + return where(DSL.and(conditions)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public FailedAuth where(Condition... conditions) { + return where(DSL.and(conditions)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public FailedAuth where(Field condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public FailedAuth where(SQL condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public FailedAuth where(@Stringly.SQL String condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public FailedAuth where(@Stringly.SQL String condition, Object... binds) { + return where(DSL.condition(condition, binds)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public FailedAuth where(@Stringly.SQL String condition, QueryPart... parts) { + return where(DSL.condition(condition, parts)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public FailedAuth whereExists(Select select) { + return where(DSL.exists(select)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public FailedAuth whereNotExists(Select select) { + return where(DSL.notExists(select)); + } +} diff --git a/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/tables/records/FailedAuthRecord.java b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/tables/records/FailedAuthRecord.java new file mode 100644 index 0000000..d9e319b --- /dev/null +++ b/backend/src/main/java/ru/micord/webbpm/ervu/business_metrics/db_beans/auth/tables/records/FailedAuthRecord.java @@ -0,0 +1,98 @@ +/* + * This file is generated by jOOQ. + */ +package ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.records; + + +import java.sql.Timestamp; + +import org.jooq.Record1; +import org.jooq.impl.UpdatableRecordImpl; + +import ru.micord.webbpm.ervu.business_metrics.db_beans.auth.tables.FailedAuth; + + +/** + * События неуспешной аутентификации + */ +@SuppressWarnings({ "all", "unchecked", "rawtypes" }) +public class FailedAuthRecord extends UpdatableRecordImpl { + + private static final long serialVersionUID = 1L; + + /** + * Setter for auth.failed_auth.id. Идентификатор события + */ + public void setId(String value) { + set(0, value); + } + + /** + * Getter for auth.failed_auth.id. Идентификатор события + */ + public String getId() { + return (String) get(0); + } + + /** + * Setter for auth.failed_auth.user_id. Идентификатор + * пользователя + */ + public void setUserId(String value) { + set(1, value); + } + + /** + * Getter for auth.failed_auth.user_id. Идентификатор + * пользователя + */ + public String getUserId() { + return (String) get(1); + } + + /** + * Setter for auth.failed_auth.datetime. Дата и время события + */ + public void setDatetime(Timestamp value) { + set(2, value); + } + + /** + * Getter for auth.failed_auth.datetime. Дата и время события + */ + public Timestamp getDatetime() { + return (Timestamp) get(2); + } + + // ------------------------------------------------------------------------- + // Primary key information + // ------------------------------------------------------------------------- + + @Override + public Record1 key() { + return (Record1) super.key(); + } + + // ------------------------------------------------------------------------- + // Constructors + // ------------------------------------------------------------------------- + + /** + * Create a detached FailedAuthRecord + */ + public FailedAuthRecord() { + super(FailedAuth.FAILED_AUTH); + } + + /** + * Create a detached, initialised FailedAuthRecord + */ + public FailedAuthRecord(String id, String userId, Timestamp datetime) { + super(FailedAuth.FAILED_AUTH); + + setId(id); + setUserId(userId); + setDatetime(datetime); + resetChangedOnNotNull(); + } +} diff --git a/backend/src/main/resources/config/v_1.0/20250512-SUPPORT-9166_add_failed_auth_table.xml b/backend/src/main/resources/config/v_1.0/20250512-SUPPORT-9166_add_failed_auth_table.xml new file mode 100644 index 0000000..a0bf683 --- /dev/null +++ b/backend/src/main/resources/config/v_1.0/20250512-SUPPORT-9166_add_failed_auth_table.xml @@ -0,0 +1,25 @@ + + + + + create failed_auth table + + CREATE TABLE IF NOT EXISTS auth.failed_auth ( + id VARCHAR(128) NOT NULL PRIMARY KEY, + user_id VARCHAR(128) NOT NULL, + datetime timestamp without time zone + ); + + ALTER TABLE auth.failed_auth OWNER TO ervu_business_metrics; + + COMMENT ON TABLE auth.failed_auth IS 'События неуспешной аутентификации'; + COMMENT ON COLUMN auth.failed_auth.id IS 'Идентификатор события'; + COMMENT ON COLUMN auth.failed_auth.user_id IS 'Идентификатор пользователя'; + COMMENT ON COLUMN auth.failed_auth.datetime IS 'Дата и время события'; + + + diff --git a/backend/src/main/resources/config/v_1.0/changelog-1.0.xml b/backend/src/main/resources/config/v_1.0/changelog-1.0.xml index 81e4a68..e134261 100644 --- a/backend/src/main/resources/config/v_1.0/changelog-1.0.xml +++ b/backend/src/main/resources/config/v_1.0/changelog-1.0.xml @@ -32,6 +32,7 @@ + \ No newline at end of file diff --git a/config/micord.env b/config/micord.env index 9cb6bd8..dcac6eb 100644 --- a/config/micord.env +++ b/config/micord.env @@ -37,4 +37,6 @@ SESSION_SYNC_CRON=0 */10 * * * * KAFKA_SSO_SESSIONS_STATE_RECEIVE=sso.session.state.receive KAFKA_SSO_SESSIONS_STATE_RESPONSE=sso.sessions.state.response KAFKA_SESSION_GROUP_ID=ervu-business-metrics-backend-session -SESSION_FETCH_TIMEOUT=15 \ No newline at end of file +SESSION_FETCH_TIMEOUT=15 +KAFKA_SSO_AUTH_EVENTS_RESPONSE=sso.auth.events +KAFKA_AUTH_EVENTS_GROUP_ID=ervu-business-metrics-backend-auth-events