Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f19e650
Add basic management of API Tokens
npomaroli Jul 9, 2026
242e542
Make Session an interface
npomaroli Jul 10, 2026
d04c08c
Add missing annotations
npomaroli Jul 10, 2026
72b193d
Fixes
npomaroli Jul 10, 2026
ae65034
Fix returning session
npomaroli Jul 10, 2026
157a27b
Fix to not use an instance of PageResourceImpl
npomaroli Jul 14, 2026
94ab96c
Fix test
npomaroli Jul 14, 2026
4d7e0da
Fix test
npomaroli Jul 14, 2026
9d1001d
Fix tests
npomaroli Jul 14, 2026
a4ec2b5
Fix test
npomaroli Jul 14, 2026
08fc307
Fix tests
npomaroli Jul 15, 2026
4b17924
Refactor Operator to be parameterized
npomaroli Jul 15, 2026
8e2f17a
Fix tests
npomaroli Jul 15, 2026
8481f64
Fix tests
npomaroli Jul 15, 2026
7e0d872
Fix tests
npomaroli Jul 17, 2026
5d7e5b8
Implement usage of API Token
npomaroli Jul 17, 2026
5496d81
Minor fixes
npomaroli Jul 20, 2026
7d656fd
Remove unnecessary transaction
npomaroli Jul 20, 2026
ae4d3e5
Fix handling of invalid sid
npomaroli Jul 20, 2026
cee2ea4
Fix tests
npomaroli Jul 20, 2026
59e89ef
Refactor tests to also use Api Tokens
npomaroli Jul 20, 2026
d65a87b
Remove sid from REST API
npomaroli Jul 21, 2026
370f7d0
Re-added validate Endpoint
npomaroli Jul 21, 2026
804846a
Remove sid from UI
npomaroli Jul 21, 2026
c0529a6
Fix tests
npomaroli Jul 21, 2026
df6a602
Fix build error
npomaroli Jul 21, 2026
7cfec31
Fix handling of invalid session
npomaroli Jul 21, 2026
060df02
Remove sid
npomaroli Jul 21, 2026
939e1f9
Remove sid and sessionSecret from constructor
npomaroli Jul 27, 2026
d9c8e5f
Remove unnecessary method
npomaroli Jul 27, 2026
10230b6
Fix invalidation of other session when current session is based on API
npomaroli Jul 27, 2026
8c14dd0
Fix setting the ui language.
npomaroli Jul 27, 2026
0022a9d
Fix rebase errors
npomaroli Aug 3, 2026
edf84f1
Remove build error
npomaroli Aug 4, 2026
fb8a8ef
Bump mesh version to next snapshot version
npomaroli Aug 4, 2026
ecb82f0
Fix missing cr in check/repair response when operation is put into
npomaroli Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.gentics.api.lib.resolving;

import org.apache.commons.beanutils.PropertyUtils;

/**
* Extension to the {@link Resolvable} that adds default implementations to the methods, which
* will resolve data by calling the getter methods of the class
*/
public interface IResolvableBean extends Resolvable {
@Override
default Object get(String key) {
// simply call the getter on the object
try {
return PropertyUtils.getProperty(this, key);
} catch (Exception e) {
return null;
}
}

@Override
default Object getProperty(String key) {
return get(key);
}

@Override
default boolean canResolve() {
return true;
}
}
17 changes: 4 additions & 13 deletions cms-aloha-plugins/src/main/js/gcn/lib/gcn-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,8 @@ define([
* the backend.
*/
isBackendMode: function () {
return !!(this.settings && this.settings.sid);
// FIXME: is there another way to check this?
return !!(this.settings);
},

_deferred: $.Deferred(),
Expand Down Expand Up @@ -527,8 +528,6 @@ define([
this.resolveCheckForInternalLink();

if (this.isBackendMode()) {
GCN.setSid(this.settings.sid);

// Set the GCN JS API to the right channel context.
if (this.settings.nodeId) {
GCN.channel(parseInt(this.settings.nodeId, 10));
Expand Down Expand Up @@ -1143,7 +1142,7 @@ define([

/**
* Perform a REST request to the GCN backend REST Service.
* The method will automatically add the sid as request parameters, additional parameters may be given.
* Additional parameters may be given.
* The data may contain the following properties:
* - url: URL for the specific request, must start with / and must not contain request parameters
* - params: additional request parameters
Expand All @@ -1160,14 +1159,6 @@ define([
* @return void
*/
performRESTRequest: function (data) {
if (!GCN.sid) {
var that = this;
GCN.sub('session.sid-set', function () {
that.performRESTRequest(data);
});
return;
}

if (!data.type) {
data.type = 'POST';
}
Expand All @@ -1180,7 +1171,7 @@ define([
data: JSON.stringify(data.body)
};

ajaxSettings.url = data.url + '?sid=' + GCN.sid + '&time=' + (new Date()).getTime();
ajaxSettings.url = data.url + '?time=' + (new Date()).getTime();

// add requestParams if given
if (data.params) {
Expand Down
12 changes: 7 additions & 5 deletions cms-aloha-plugins/src/main/js/gcn/lib/gcnjs-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ define('gcn/gcnjs-util', [
/**
* Creates a URL for GCN.
*
* Will automatically add the sid as request parameters, additional
* parameters may be given.
* Additional parameters may be given.
*
* The data may contain the following properties:
* - url: part of the URL for the specific request after /rest,
Expand All @@ -40,15 +39,18 @@ define('gcn/gcnjs-util', [
* @return {string} A GCN url
*/
function createUrl(data) {
var url = data.url + '?sid=' + GCN.sid;
var url = data.url;
var paramAdded = false;
if (data.noCache) {
url += '&time=' + (new Date()).getTime();
url += '?time=' + (new Date()).getTime();
paramAdded = true;
}
var name;
for (name in data.params) {
if (data.params.hasOwnProperty(name)) {
url += '&' + name
url += (paramAdded ? '&' : '?') + name
+ '=' + encodeURI(data.params[name]);
paramAdded = true;
}
}
return url;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.gentics.api.lib.etc.ObjectTransformer;
import com.gentics.api.lib.exception.NodeException;
import com.gentics.api.lib.exception.ReadOnlyException;
import com.gentics.contentnode.etc.ContentNodeHelper;
import com.gentics.contentnode.etc.Feature;
import com.gentics.contentnode.etc.MapPreferences;
Expand All @@ -61,19 +62,20 @@
import com.gentics.contentnode.object.parttype.PartType;
import com.gentics.contentnode.parser.tag.ParserTag;
import com.gentics.contentnode.perm.PermHandler;
import com.gentics.contentnode.perm.PermHandler.ObjectPermission;
import com.gentics.contentnode.render.RenderResult;
import com.gentics.contentnode.render.RenderType;
import com.gentics.contentnode.render.TemplateRenderer;
import com.gentics.contentnode.render.renderer.MetaEditableRenderer;
import com.gentics.contentnode.rest.model.Page;
import com.gentics.contentnode.rest.model.Reference;
import com.gentics.contentnode.rest.model.response.Message;
import com.gentics.contentnode.rest.model.response.Message.Type;
import com.gentics.contentnode.rest.model.response.PageLoadResponse;
import com.gentics.contentnode.rest.model.response.PageRenderResponse;
import com.gentics.contentnode.rest.model.response.PageRenderResponse.Editable;
import com.gentics.contentnode.rest.model.response.PageRenderResponse.MetaEditable;
import com.gentics.contentnode.rest.model.response.ResponseCode;
import com.gentics.contentnode.rest.resource.impl.PageResourceImpl;
import com.gentics.contentnode.rest.util.ModelBuilder;
import com.gentics.contentnode.runtime.NodeConfigRuntimeConfiguration;
import com.gentics.lib.etc.StringUtils;
import com.gentics.lib.i18n.CNI18nString;
Expand Down Expand Up @@ -524,7 +526,7 @@ public JsonNode getAlohaSettings(Node node, RenderResult renderResult, RenderTyp

// add the proxyURL as general setting
settings.put("proxyUrl",
prefs.getProperty("stag_prefix") + "?sid=" + t.getSessionId() + "&do=19191&url=");
prefs.getProperty("stag_prefix") + "?do=19191&url=");

// Log levels
ObjectNode logSettings = mapper.createObjectNode();
Expand Down Expand Up @@ -559,25 +561,26 @@ public JsonNode getAlohaSettings(Node node, RenderResult renderResult, RenderTyp
}

// Load page meta data
PageResourceImpl pageResource = new PageResourceImpl(t);

pageResource.omitTouchSession();
pageResource.setSessionId(t.getSessionId());
pageResource.setSessionSecret(t.getSession().getSessionSecret());
pageResource.setTransaction(t);
pageResource.initialize();
PageLoadResponse pageLoadResponse = pageResource.load(String.valueOf(pageId), !readonly, false, false, false, false, false, false, false, false, false, null, null);

if (pageLoadResponse.getResponseInfo().getResponseCode() != ResponseCode.OK) {
throw new NodeException("Error while loading page metadata", new Exception(pageLoadResponse.getResponseInfo().getResponseMessage()));
com.gentics.contentnode.object.Page reloadedPage;
try {
if (readonly) {
reloadedPage = PageResourceImpl.getPage(String.valueOf(pageId), true, ObjectPermission.view);
} else {
reloadedPage = PageResourceImpl.getLockedPage(String.valueOf(pageId), true, PermHandler.ObjectPermission.edit);
}
} catch (ReadOnlyException e) {
reloadedPage = t.getObject(com.gentics.contentnode.object.Page.class, pageId);
readonly = true;
}
Page page = pageLoadResponse.getPage();

Page page = ModelBuilder.getPage(reloadedPage, List.of(Reference.CONTENT_TAGS, Reference.OBJECT_TAGS_VISIBLE));
page.setReadOnly(readonly);

// add LinkChecker plugin config
ObjectNode linkCheckerPlugin = mapper.createObjectNode();

linkCheckerPlugin.put("proxyUrl",
prefs.getProperty("stag_prefix") + "?sid=" + t.getSessionId() + "&do=19191&url=");
prefs.getProperty("stag_prefix") + "?do=19191&url=");
plugins.put("linkchecker", linkCheckerPlugin);

for (AlohaPluginService service : alohaPluginServiceLoader) {
Expand All @@ -589,7 +592,6 @@ public JsonNode getAlohaSettings(Node node, RenderResult renderResult, RenderTyp

plugins.put("gcn", cnIntegrationPlugin);

cnIntegrationPlugin.put("sid", t.getSessionId());
cnIntegrationPlugin.put("buildRootTimestamp", buildRootTimestamp);
cnIntegrationPlugin.put("gcnLibVersion", gcnJSLibVersion);
cnIntegrationPlugin.put("webappPrefix", webappPrefix);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static String renderPage(Page restPage, String template) throws NodeExcep
com.gentics.contentnode.object.Page page = ModelBuilder.getPage(restPage, true);

NodePreferences nodePreferences = t.getNodeConfig().getDefaultPreferences();
RenderType renderType = RenderType.getDefaultRenderType(nodePreferences, RenderType.EM_ALOHA_READONLY, t.getSessionId(), 0);
RenderType renderType = RenderType.getDefaultRenderType(nodePreferences, RenderType.EM_ALOHA_READONLY, 0);
renderType.setLanguage(page.getLanguage());
t.setRenderType(renderType);
// push the page onto the rendertype stack
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.gentics.contentnode.auth;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.ResultSet;
import java.util.Base64;
import java.util.List;
import java.util.Optional;

import org.apache.commons.lang3.RandomUtils;

import com.gentics.api.lib.exception.NodeException;
import com.gentics.contentnode.db.DBUtils;
import com.gentics.contentnode.db.DBUtils.HandleSelectResultSet;
import com.gentics.contentnode.factory.Transaction;
import com.gentics.contentnode.factory.TransactionManager;
import com.gentics.contentnode.rest.model.token.ApiTokenCreationRequest;
import com.gentics.contentnode.rest.model.token.ApiTokenDataModel;

/**
* Factory for management of API Tokens
*/
public class ApiTokenFactory {
/**
* Byte count for generated tokens
*/
public final static int BYTE_COUNT = 32;

/**
* Name of the table for storing token data
*/
protected final static String TABLE_NAME = "api_token";

/**
* Select clause
*/
protected final static String SELECT_CLAUSE = "SELECT id, user_id, name, cdate, expires, last_used FROM %s".formatted(TABLE_NAME);

/**
* SQL to insert a new record
*/
protected final static String INSERT_SQL = "INSERT INTO %s (user_id, name, token_hash, cdate, expires) VALUES (?, ?, ?, ?, ?)".formatted(TABLE_NAME);

/**
* SQL to delete a record
*/
protected final static String DELETE_SQL = "DELETE FROM %s WHERE id = ?".formatted(TABLE_NAME);

/**
* Instance of {@link HandleSelectResultSet} which creates an instance of {@link ApiTokenDataModel} from the current row of the {@link ResultSet}
*/
protected final static DBUtils.HandleSelectResultSet<ResolvableApiTokenDataModel> ROW_HANDLER = rs -> {
ResolvableApiTokenDataModel model = new ResolvableApiTokenDataModel();
int now = TransactionManager.getCurrentTransaction().getUnixTimestamp();
int expiry = rs.getInt("expires");
model
.setId(rs.getInt("id"))
.setUserId(rs.getInt("user_id"))
.setName(rs.getString("name"))
.setCdate(rs.getInt("cdate"))
.setExpires(expiry)
.setLastUsed(rs.getInt("last_used"))
.setValid(expiry > 0 ? expiry > now : true);

return model;
};

/**
* Create a new token
* @return new token
*/
public final static String createToken() {
byte[] randomBytes = RandomUtils.secureStrong().randomBytes(BYTE_COUNT);
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
}

/**
* Get the hash of the given token
* @param token token
* @return hash
* @throws NodeException
*/
public final static String hash(String token) throws NodeException {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(token.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new NodeException("Error while hashing API Token", e);
}
}

/**
* Create a new API Token for the given user in the database
* @param request create request
* @param userId user ID
* @param token token
* @return stored instance
* @throws NodeException
*/
public final static ResolvableApiTokenDataModel create(ApiTokenCreationRequest request, int userId, String token)
throws NodeException {
Transaction t = TransactionManager.getCurrentTransaction();
int cDate = t.getUnixTimestamp();
String hash = hash(token);

List<Integer> ids = DBUtils.executeInsert(
INSERT_SQL,
new Object[] { userId, request.getName(), hash, cDate, request.getExpires() });

if (ids.size() != 1) {
throw new NodeException("Error while creating API Token. Unexpected number of inserts: %d".formatted(ids.size()));
}

int id = ids.get(0);

Optional<ResolvableApiTokenDataModel> optData = DBUtils.select(SELECT_CLAUSE + " WHERE id = ?", pst -> {
pst.setInt(1, id);
}, DBUtils.getFirst(ROW_HANDLER));

if (optData.isEmpty()) {
throw new NodeException("Error while creating API Token.");
}

return optData.get();
}

/**
* Load the token with given ID for the user
* @param userId user ID
* @param tokenId token ID
* @return optional token instance
* @throws NodeException
*/
public final static Optional<ResolvableApiTokenDataModel> load(int userId, int tokenId) throws NodeException {
return DBUtils.select(SELECT_CLAUSE + " WHERE id = ? AND user_id = ?", pst -> {
pst.setInt(1, tokenId);
pst.setInt(2, userId);
}, DBUtils.getFirst(ROW_HANDLER));
}

/**
* Get the list of API Tokens for the given user
* @param userId user ID
* @return list of tokens
* @throws NodeException
*/
public final static List<ResolvableApiTokenDataModel> list(int userId) throws NodeException {
return DBUtils.select(SELECT_CLAUSE + " WHERE user_id = ?", pst -> {
pst.setInt(1, userId);
}, DBUtils.getAll(ROW_HANDLER));
}

/**
* Delete the token with given ID
* @param tokenId token ID
* @throws NodeException
*/
public final static void delete(int tokenId) throws NodeException {
DBUtils.update(DELETE_SQL, tokenId);
}

/**
* Load the token with the given hash. Only return the token if it is still valid (not expired)
* @param hash token hash
* @return optional token instance
* @throws NodeException
*/
public final static Optional<ResolvableApiTokenDataModel> load(String hash) throws NodeException {
return DBUtils.select(SELECT_CLAUSE + " WHERE token_hash = ?", pst -> {
pst.setString(1, hash);
}, DBUtils.getFirst(ROW_HANDLER)).filter(ResolvableApiTokenDataModel::isValid);
}
}
Loading