Skip to content
Merged
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
5 changes: 5 additions & 0 deletions openam-cassandra/openam-cassandra-cts/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<extensions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2019 Open Identity Platform Community.
* Portions copyright 2025 3A Systems LLC.
* Portions copyright 2025-2026 3A Systems LLC.
*/

package org.openidentityplatform.openam.cassandra;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.time.Duration;
import java.time.Instant;
Expand Down Expand Up @@ -64,10 +65,24 @@
import com.datastax.oss.driver.api.querybuilder.select.Select;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.hash.Hashing;

public class TokenStorageAdapter implements org.forgerock.openam.sm.datalayer.api.TokenStorageAdapter {
final static Logger logger = LoggerFactory.getLogger(TokenStorageAdapter.class);

/**
* Renders a token id for log output as a short SHA-256 digest. The id is a
* session id or an OAuth2 token, so the raw value must never reach the logs;
* a prefix would not do either, because every session id starts with the same
* "AQIC" header. The digest still lets an operator holding the id find its lines.
*/
static String maskTokenId(String tokenId) {
if (tokenId == null) {
return "null";
}
return "sha256:" + Hashing.sha256().hashString(tokenId, StandardCharsets.UTF_8).toString().substring(0, 8);
}

private final DataLayerConfiguration cfg;
static ConnectionFactory<CqlSession> connectionFactory;

Expand Down Expand Up @@ -111,7 +126,7 @@ public Token update(Token token, boolean ifExists) throws DataLayerException {
try {
value = token.getAttribute(field);
}catch (Throwable e) {
logger.warn("create {} for {} {}",e.toString(),field,token);
logger.warn("update: unable to read {} of {} token {}: {}",field,token.getType(),maskTokenId(token.getTokenId()),e.toString());
throw e;
}
if (value!=null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2026 3A Systems, LLC.
*/
package org.openidentityplatform.openam.cassandra;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.RETURNS_SELF;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.Calendar;
import java.util.List;
import java.util.stream.Collectors;

import org.forgerock.openam.cts.api.tokens.Token;
import org.forgerock.openam.sm.datalayer.api.DataLayerException;
import org.forgerock.openam.tokens.CoreTokenField;
import org.forgerock.openam.tokens.TokenType;
import org.junit.After;
import org.junit.Test;
import org.slf4j.LoggerFactory;

import com.datastax.oss.driver.api.core.cql.BoundStatement;
import com.datastax.oss.driver.api.core.cql.PreparedStatement;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;

public class TokenStorageAdapterTest {

@After
public void forgetPreparedStatement() {
TokenStorageAdapter.static_statement_update = null;
}

/**
* A CTS token id is a session id or an OAuth2 token: it must never be written
* to the log, only a short digest that identifies the record. Every session id
* starts with the same "AQIC" header, so a prefix could not tell two apart.
*/
@Test
public void maskTokenIdReplacesTheIdWithAShortDigest() {
assertEquals("sha256:983d2944", TokenStorageAdapter.maskTokenId("AQIC5wM2LY4SfczntBcXfFoFJwA6zAV2i4fnU8Sd7ao"));
assertEquals("sha256:d5989e92", TokenStorageAdapter.maskTokenId("AQIC5wM2LY4Sfczn-expired-session-token"));
assertEquals("sha256:b8150354", TokenStorageAdapter.maskTokenId("AQIC5wM2LY4Sfczn-fresh-session-token"));
assertEquals("null", TokenStorageAdapter.maskTokenId(null));
}

/**
* The warning written when a token field cannot be read during {@code update}
* must carry the digest of the token id, never the id itself.
*/
@Test
public void updateLogsTheDigestNotTheIdWhenAFieldCannotBeRead() throws Exception {
String tokenId = "AQIC5wM2LY4SfczntBcXfFoFJwA6zAV2i4fnU8Sd7ao";
Token token = mock(Token.class);
when(token.getTokenId()).thenReturn(tokenId);
when(token.getType()).thenReturn(TokenType.SESSION);
when(token.getExpiryTimestamp()).thenReturn(Calendar.getInstance());
when(token.getAttribute(any(CoreTokenField.class))).thenThrow(new IllegalArgumentException("boom"));

// The statement is pre-set, so the adapter never reaches Cassandra: the read
// of the first field fails before the statement is executed.
BoundStatement bound = mock(BoundStatement.class, RETURNS_SELF);
PreparedStatement prepared = mock(PreparedStatement.class);
when(prepared.bind()).thenReturn(bound);
TokenStorageAdapter.static_statement_update = prepared;
TokenStorageAdapter adapter = new TokenStorageAdapter(null, null);

ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.start();
((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(TokenStorageAdapter.class)).addAppender(appender);
try {
adapter.update(token, true);
fail("update must propagate the failed field read");
} catch (DataLayerException expected) {
// the warning is written before the exception is wrapped
} finally {
((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(TokenStorageAdapter.class)).detachAppender(appender);
}

List<String> messages = appender.list.stream().map(ILoggingEvent::getFormattedMessage).collect(Collectors.toList());
assertTrue(messages.toString(), messages.stream().anyMatch(m -> m.contains("SESSION token sha256:983d2944: java.lang.IllegalArgumentException: boom")));
assertTrue(messages.toString(), messages.stream().noneMatch(m -> m.contains(tokenId)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2019 Open Identity Platform Community.
* Portions copyright 2026 3A Systems LLC.
*/

package org.openidentityplatform.openam.cassandra;
Expand Down Expand Up @@ -296,7 +297,7 @@ public Map<String, Set<String>> getAttributes(SSOToken token, IdType type,String
}
}
}catch(Throwable e){
logger.error("getAttributes {} {}",type,name,attrNames,e);
logger.error("getAttributes {} {} {}",type,name,attrNames,e);
throw new IdRepoException(e.getMessage());
}
return attr;
Expand Down Expand Up @@ -434,15 +435,15 @@ public void setAttributes(SSOToken token, IdType type, String name, Map<String,
}catch (IdRepoException e) {
throw e;
}catch(Throwable e){
logger.error("setAttributes {} {} {} {}",type,name,attributes_in,isAdd,e.getMessage());
logger.error("setAttributes {} {} {} {}: {}",type,name,names(attributes_in),isAdd,e.getMessage());
Comment thread
vharseko marked this conversation as resolved.
Dismissed
Comment thread
vharseko marked this conversation as resolved.
Dismissed
throw new IdRepoException(e.getMessage());
}
}

@Override
public void setBinaryAttributes(SSOToken token, IdType type, String name,Map<String, byte[][]> attributes, boolean isAdd) throws IdRepoException, SSOException {
//validate(type, IdOperation.EDIT);
logger.warn("unsupported setBinaryAttributes {} {} {} {}",type,name,attributes,isAdd);
logger.warn("unsupported setBinaryAttributes {} {} {} {}",type,name,names(attributes),isAdd);
Comment thread
vharseko marked this conversation as resolved.
Dismissed
Comment thread
vharseko marked this conversation as resolved.
Dismissed
throw new IdRepoUnsupportedOpException("unsupported setBinaryAttributes");
}

Expand Down Expand Up @@ -470,7 +471,7 @@ public void removeAttributes(SSOToken token, IdType type, String name, Set<Strin
else
new ExecuteCallback(profile,session, statement).execute();
}catch(Throwable e){
logger.error("removeAttributes {} {} {}",type,name,attrNames,e.getMessage());
logger.error("removeAttributes {} {} {}: {}",type,name,attrNames,e.getMessage());
throw new IdRepoException(e.getMessage());
}
}
Expand Down Expand Up @@ -649,14 +650,14 @@ public RepoSearchResults search(SSOToken token, IdType type, String pattern, int
}
//test result
if (filterOp==Repo.AND_MOD && (result.isEmpty())) {
logger.debug("break search by empty query {} {}: {}",pattern,avPairs,filterEntry);
logger.debug("break search by empty query {} {}: {}",pattern,names(avPairs),filterEntry.getKey());
break;
}
}
}
return new RepoSearchResults(result.keySet(),(maxResults>0&&result.size()>maxResults)?RepoSearchResults.SIZE_LIMIT_EXCEEDED:RepoSearchResults.SUCCESS,result,type);
}catch(Throwable e){
logger.error("search {} {} {} {} {} {} {} {} {}: {}",type,pattern,maxTime,maxResults,returnAttrs,returnAllAttrs,filterOp,avPairs,recursive,logger.isDebugEnabled()?e:e.getMessage());
logger.error("search {} {} {} {} {} {} {} {} {}: {}",type,pattern,maxTime,maxResults,returnAttrs,returnAllAttrs,filterOp,names(avPairs),recursive,logger.isDebugEnabled()?e:e.getMessage());
Comment thread
vharseko marked this conversation as resolved.
Dismissed
throw new IdRepoException(e.getMessage());
}
}
Expand Down Expand Up @@ -744,7 +745,7 @@ public void assignService(SSOToken token, IdType type, String name, String servi
attrMap.put("serviceName", attr.get("serviceName"));
setAttributes(token, type, name, attrMap, false);
}catch(Throwable e){
logger.error("assignService {} {} {} {}",type,name,serviceName,attrMap,e.getMessage());
logger.error("assignService {} {} {} {}: {}",type,name,serviceName,names(attrMap),e.getMessage());
Comment thread
vharseko marked this conversation as resolved.
Dismissed
throw new IdRepoException(e.getMessage());
}
}
Expand All @@ -756,7 +757,7 @@ public Set<String> getAssignedServices(SSOToken token, IdType type, String name,
Map<String, Set<String>> attr=getAttributes(token, type, name, new HashSet<String>(Arrays.asList(new String[]{"serviceName"})));
return (attr.containsKey("serviceName"))?attr.get("serviceName"):new HashSet<String>(0);
}catch(Throwable e){
logger.error("getAssignedServices {} {} {}",type,name,mapOfServicesAndOCs,e.getMessage());
logger.error("getAssignedServices {} {} {}: {}",type,name,mapOfServicesAndOCs,e.getMessage());
throw new IdRepoException(e.getMessage());
}
}
Expand All @@ -772,7 +773,7 @@ public void unassignService(SSOToken token, IdType type, String name, String ser
attrMap.put("serviceName", attr.get("serviceName"));
setAttributes(token, type, name, attrMap, false);
}catch(Throwable e){
logger.error("unassignService {} {} {} {}",type,name,serviceName,attrMap,e.getMessage());
logger.error("unassignService {} {} {} {}: {}",type,name,serviceName,names(attrMap),e.getMessage());
Comment thread
vharseko marked this conversation as resolved.
Dismissed
throw new IdRepoException(e.getMessage());
}
}
Expand Down Expand Up @@ -801,7 +802,7 @@ public void modifyService(SSOToken token, IdType type, String name, String servi
attrMap.put("serviceName", attr.get("serviceName"));
setAttributes(token, type, name, attrMap, false);
}catch(Throwable e){
logger.error("modifyService {} {} {} {}",type,name,serviceName,attrMap,e.getMessage());
logger.error("modifyService {} {} {} {}: {}",type,name,serviceName,names(attrMap),e.getMessage());
Comment thread
vharseko marked this conversation as resolved.
Dismissed
throw new IdRepoException(e.getMessage());
}
}
Expand All @@ -817,6 +818,13 @@ public void removeListener() {
}

///////////////////////////////////////////////////////////////////////////
/**
* Attribute names for log output: the values may carry userPassword.
*/
static Set<String> names(Map<String, ?> attributes) {
return attributes==null?null:attributes.keySet();
}

void validate(IdType type,IdOperation service) throws IdRepoUnsupportedOpException{
if (!supportedOps.containsKey(type)||!supportedOps.get(type).contains(service))
throw new IdRepoUnsupportedOpException("operation "+service.getName()+" not supported for "+type.getName());
Expand Down
Loading
Loading