Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion api/src/main/java/com/gentics/mesh/ElementType.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ public enum ElementType {

BRANCH,

NODE;
NODE,

APITOKEN;

/**
* Parse the string value into the Mesh element type, if possible.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Auth: The management of API Tokens for users has been enhanced to support multiple tokens per user.
New tokens must now be created with a `name` and optionally can have an `expires` date.
See the "Mesh Restful API documentation":https://www.gentics.com/mesh/docs/api/#users__userUuid__token_post
for details.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.gentics.mesh.auth.AuthenticationResult;
import com.gentics.mesh.cli.BootstrapInitializer;
import com.gentics.mesh.context.InternalActionContext;
import com.gentics.mesh.core.data.user.HibAPITokenData;
import com.gentics.mesh.core.data.user.HibUser;
import com.gentics.mesh.core.data.user.MeshAuthUser;
import com.gentics.mesh.core.db.Database;
Expand Down Expand Up @@ -230,7 +231,7 @@ public String generateToken(User user) {
* @param user
* @param tokenCode
* Code which will be part of the JWT. This code is used to verify that the JWT is still valid
* @param expireDuration
* @param expireDuration expire duration in seconds
* @return Generated API key
*/
public String generateAPIToken(HibUser user, String tokenCode, Integer expireDuration) {
Expand All @@ -240,7 +241,7 @@ public String generateAPIToken(HibUser user, String tokenCode, Integer expireDur
.put(API_KEY_TOKEN_CODE_FIELD_NAME, tokenCode);
JWTOptions jwtOptions = new JWTOptions().setAlgorithm(options.getAlgorithm());
if (expireDuration != null) {
jwtOptions.setExpiresInMinutes(expireDuration);
jwtOptions.setExpiresInSeconds(expireDuration);
}
return jwtProvider.generateToken(tokenData, jwtOptions);
}
Expand Down Expand Up @@ -271,15 +272,12 @@ private User loadUserByJWT(JsonObject jwt) throws Exception {
// }

// Check whether the token might be an API key token
if (!jwt.containsKey("exp")) {
String apiKeyToken = jwt.getString(API_KEY_TOKEN_CODE_FIELD_NAME);
// TODO: All tokens without exp must have a token code - See https://github.com/gentics/mesh/issues/412
if (apiKeyToken != null) {
String storedApiKey = user.getDelegate().getAPIKeyTokenCode();
// Verify that the API token is invalid.
if (apiKeyToken != null && !apiKeyToken.equals(storedApiKey)) {
throw new Exception("API key token is invalid.");
}
String apiKeyToken = jwt.getString(API_KEY_TOKEN_CODE_FIELD_NAME);
if (apiKeyToken != null) {
HibAPITokenData tokenData = tx.apiTokenDao().findByTokenId(user.getDelegate(), apiKeyToken);

if (tokenData == null) {
throw new Exception("API key token is invalid.");
}
}

Expand Down
7 changes: 5 additions & 2 deletions common/src/main/resources/i18n/translations_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ user_error_password_check_failed=Das angegebene Passwort stimmt nicht mit dem ak
user_error_provided_token_invalid=Der angegebene Token ist ungültig.
user_error_admin_privilege_needed_for_admin_flag=Die Admin Berechtigung ist notwendig um das Admin Feld zu setzen.

apitoken_conflicting_name=Es gibt bereits einen API Token mit diesem Namen.
apitoken_missing_name=Es wurde kein Name angegeben.
apitoken_expires_in_past=Für den Ablauf des API Tokens wurde ein Zeitpunkt in der Vergangenheit angegeben.
apitoken_deleted=Der API Token wurde entfernt.

role_deleted=Rolle "{0}" wurde gelöscht.
role_not_found=Rolle mit uuid "{0}" konnte nicht gefunden werden.
role_conflicting_name=Rollenname bereits belegt.
Expand Down Expand Up @@ -316,8 +321,6 @@ graphql_error_missing_perm=Nicht genügend Berechtigungen für Objekt "{1}" vom

error_backup=Es konnte kein gültiges Backup im Backup Ordner {0} gefunden werden.

api_key_invalidated=Der zur Zeit aktive API Key wurde ungültig gemacht.

job_error_invalid_state=Der Job {0} kann nicht gelöscht werden weil er bisher noch nicht fehlgeschlagen ist.
job_processing_invoked=Die Verarbeitung der Jobs wurde angestoßen.

Expand Down
5 changes: 5 additions & 0 deletions common/src/main/resources/i18n/translations_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ user_error_password_check_failed=The provided old password did not match up with
user_error_provided_token_invalid=The provided token is invalid.
user_error_admin_privilege_needed_for_admin_flag=The admin privilege is needed to set the admin flag.

apitoken_conflicting_name=API Token with this name already exists.
apitoken_missing_name=No name was specified.
apitoken_expires_in_past=A date in the past was specified for the expiration.
apitoken_deleted=The API Token has been removed.

role_deleted=Role "{0}" was deleted.
role_not_found=Role with uuid "{0}" could not be found.
role_conflicting_name=Role name is conflicting with an existing role.
Expand Down
2 changes: 0 additions & 2 deletions common/src/main/resources/i18n/translations_zh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,6 @@ graphql_error_missing_perm=对类型为“{1}”的对象“{0}”缺少权限

error_backup=在备份位置{0}中找不到有效的备份文件。

api_key_invalidated=当前活动的API密钥已失效。

job_error_invalid_state=作业{0}未处于错误状态,因此无法删除。只能删除以前失败的作业。
job_processing_invoked=作业处理已被调用。

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.gentics.mesh.liquibase.changelog.v3_3_0;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Triple;

import com.gentics.mesh.util.UUIDUtil;

import liquibase.change.custom.CustomTaskChange;
import liquibase.database.Database;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.CustomChangeException;
import liquibase.exception.CustomPreconditionErrorException;
import liquibase.exception.CustomPreconditionFailedException;
import liquibase.exception.DatabaseException;
import liquibase.exception.SetupException;
import liquibase.exception.ValidationErrors;
import liquibase.precondition.CustomPrecondition;
import liquibase.resource.ResourceAccessor;

/**
* Custom change task that migrates API Tokens stored in the user table to their
* own entities
*/
public class MigrateUserAPITokens implements CustomTaskChange, CustomPrecondition {
/**
* Name of the system property, which will omit this change, when set to "true"
*/
public final static String OMIT_PRECONDITION = "MigrateUserAPITokens.omit";

/**
* Create a random UUID and return it in the same format as the given uuid
* @param uuid example UUID
* @return random UUId in the same format as the example
*/
protected static Object createRandomUuid(Object uuid) throws CustomChangeException {
String randomUUID = UUIDUtil.randomUUID();
if (uuid instanceof byte[]) {
return UUIDUtil.toBytes(UUIDUtil.toJavaUuid(randomUUID));
} else if (uuid instanceof UUID) {
return UUIDUtil.toJavaUuid(randomUUID);
} else if (uuid instanceof String) {
return UUIDUtil.toFullUuid(randomUUID);
} else {
throw new CustomChangeException("UUID has unknown type %s".formatted(uuid.getClass()));
}
}

@Override
public String getConfirmationMessage() {
return "MigrateUserAPITokens confirmed";
}

@Override
public void setUp() throws SetupException {
}

@Override
public void setFileOpener(ResourceAccessor resourceAccessor) {
}

@Override
public ValidationErrors validate(Database database) {
return new ValidationErrors();
}

@Override
public void execute(Database database) throws CustomChangeException {
JdbcConnection conn = (JdbcConnection) database.getConnection();

List<Triple<Object, String, Long>> tokens = new ArrayList<>();
try (PreparedStatement pst = conn.prepareStatement(
"SELECT dbuuid, apitokenid, apitokenissuetimestamp FROM mesh_user")) {
try (ResultSet rs = pst.executeQuery()) {
while (rs.next()) {
Object uuid = rs.getObject("dbuuid");
String apiTokenId = rs.getString("apitokenid");
Long apiTokenIssueTimestamp = rs.getLong("apitokenissuetimestamp");

if (StringUtils.isNotBlank(apiTokenId)) {
tokens.add(Triple.of(uuid, apiTokenId, apiTokenIssueTimestamp));
}
}
}
} catch (DatabaseException | SQLException e) {
throw new CustomChangeException(e);
}

if (!tokens.isEmpty()) {
try (PreparedStatement pst = conn.prepareStatement(
"INSERT INTO mesh_apitoken (dbuuid, dbversion, name, user_dbuuid, tokenid, issued, lastused, expires) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) {
for (Triple<Object, String, Long> token : tokens) {
Object uuid = createRandomUuid(token.getLeft());
Timestamp issued = Timestamp.from(Instant.ofEpochMilli(token.getRight()));
Timestamp zero = Timestamp.from(Instant.ofEpochMilli(0));

pst.setObject(1, uuid); // dbuuid
pst.setLong(2, 1); // dbversion
pst.setString(3, "Migrated API Token"); // name
pst.setObject(4, token.getLeft()); // user_dbuuid
pst.setString(5, token.getMiddle()); // tokenid
pst.setTimestamp(6, issued); // issued
pst.setTimestamp(7, zero); // lastused
pst.setTimestamp(8, zero); // expires

pst.addBatch();
}

pst.executeBatch();
} catch (DatabaseException | SQLException e) {
throw new CustomChangeException(e);
}
}

}

@Override
public void check(Database database) throws CustomPreconditionFailedException, CustomPreconditionErrorException {
if ("true".equals(System.getProperty(OMIT_PRECONDITION))) {
throw new CustomPreconditionFailedException("Skipped in test");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@
<includeAll path="entries-3.0.x" relativeToChangelogFile="true" errorIfMissingOrEmpty="false"/>
<!-- 3.1.x changes -->
<includeAll path="entries-3.1.x" relativeToChangelogFile="true" errorIfMissingOrEmpty="false"/>
<!-- 3.3.x changes -->
<includeAll path="entries-3.3.x" relativeToChangelogFile="true" errorIfMissingOrEmpty="false"/>
</databaseChangeLog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-4.6.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">

<property name="smalltext.type" value="NVARCHAR(255)" dbms="mssql"/>
<property name="smalltext.type" value="VARCHAR(255)" dbms="oracle,postgresql,mariadb,mysql,hsqldb"/>

<property name="uuid.type" value="UUID" dbms="oracle,mssql,postgresql"/>
<property name="uuid.type" value="binary(16)" dbms="mariadb,mysql,hsqldb"/>

<property name="timestamp.type" value="TIMESTAMP" dbms="oracle,postgresql,hsqldb,mssql"/>
<property name="timestamp.type" value="datetime(6)" dbms="mariadb,mysql"/>

<changeSet id="gpu-2604-1" author="n.pomaroli@gentics.com">
<createTable tableName="mesh_apitoken">
<column name="dbuuid" type="${uuid.type}">
<constraints nullable="false" primaryKey="true" primaryKeyName="mesh_apitoken_pkey"/>
</column>
<column name="dbversion" type="BIGINT"/>
<column name="name" type="${smalltext.type}"/>
<column name="user_dbuuid" type="${uuid.type}"/>
<column name="tokenid" type="${smalltext.type}"/>
<column name="issued" type="${timestamp.type}"/>
<column name="lastused" type="${timestamp.type}"/>
<column name="expires" type="${timestamp.type}"/>
</createTable>
</changeSet>
<changeSet id="gpu-2604-2" author="n.pomaroli@gentics.com">
<createIndex tableName="mesh_apitoken" indexName="idx_mesh_apitoken_user_dbuuid_tokenid">
<column name="user_dbuuid"/>
<column name="tokenid"/>
</createIndex>
<createIndex tableName="mesh_apitoken" indexName="idx_mesh_apitoken_user_dbuuid_name" unique="true">
<column name="user_dbuuid"/>
<column name="name" />
</createIndex>
<addForeignKeyConstraint baseTableName="mesh_apitoken" baseColumnNames="user_dbuuid" constraintName="fk_mesh_apitoken_user_dbuuid" deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="dbuuid" referencedTableName="mesh_user" validate="true"/>
</changeSet>
<changeSet id ="gpu-2604-3" author="n.pomaroli@gentics.com">
<preConditions onFail="CONTINUE">
<customPrecondition className="com.gentics.mesh.liquibase.changelog.v3_3_0.MigrateUserAPITokens"/>
</preConditions>
<customChange class="com.gentics.mesh.liquibase.changelog.v3_3_0.MigrateUserAPITokens"></customChange>
</changeSet>
<changeSet id ="gpu-2604-4" author="s.plyhun@gentics.com">
<dropColumn tableName="mesh_user" columnName="apitokenid" />
<dropColumn tableName="mesh_user" columnName="apitokenissuetimestamp" />
</changeSet>
</databaseChangeLog>
Loading