Skip to content

Commit

Permalink
KNOX-2961 - Knox SSO cookie Invalidation - Phase II (#799)
Browse files Browse the repository at this point in the history
- Allow end-users to show/hide previously disabled KnoxSSO Cookies on the Token Management page.
- Pre-configured users can see all tokens on the Token Management page.
- End-users can execute batch operations on selected Knox Tokens.
  • Loading branch information
smolnar82 authored Oct 9, 2023
1 parent c49302a commit f913856
Show file tree
Hide file tree
Showing 23 changed files with 463 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,11 @@ public TokenMetadata getTokenMetadata(String tokenId) throws UnknownTokenExcepti
return tokenMetadata.get(tokenId);
}

@Override
public Collection<KnoxToken> getAllTokens() {
return fetchTokens(null, false);
}

@Override
public Collection<KnoxToken> getTokens(String userName) {
return fetchTokens(userName, false);
Expand All @@ -499,10 +504,14 @@ public Collection<KnoxToken> getDoAsTokens(String createdBy) {
private Collection<KnoxToken> fetchTokens(String userName, boolean createdBy) {
final Collection<KnoxToken> tokens = new TreeSet<>();
final Predicate<Map.Entry<String, TokenMetadata>> filterPredicate;
if (createdBy) {
filterPredicate = entry -> userName.equals(entry.getValue().getCreatedBy());
if (userName == null) {
filterPredicate = entry -> true;
} else {
filterPredicate = entry -> userName.equals(entry.getValue().getUserName());
if (createdBy) {
filterPredicate = entry -> userName.equals(entry.getValue().getCreatedBy());
} else {
filterPredicate = entry -> userName.equals(entry.getValue().getUserName());
}
}
tokenMetadata.entrySet().stream().filter(filterPredicate).forEach(metadata -> {
String tokenId = metadata.getKey();
Expand Down
7 changes: 7 additions & 0 deletions gateway-release/home/conf/gateway-site.xml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ limitations under the License.
<description>Enable/disable logout from the Knox Homepage.</description>
</property>

<!-- @since 2.1.0 KnoxSSO Cookie Invalidation -->
<property>
<name>gateway.knox.token.management.users.can.see.all.tokens</name>
<value>admin</value>
<description>A comma separated list of user names who can see all tokens on the Token Management page</description>
</property>

<!-- @since 1.6.0 token management related properties -->
<property>
<name>gateway.knox.token.eviction.grace.period</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ public class GatewayConfigImpl extends Configuration implements GatewayConfig {
public static final String X_FORWARD_CONTEXT_HEADER_APPEND_SERVICES = GATEWAY_CONFIG_FILE_PREFIX + ".xforwarded.header.context.append.servicename";

private static final String TOKEN_STATE_SERVER_MANAGED = GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.exp.server-managed";
private static final String USERS_CAN_SEE_ALL_TOKENS = GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.management.users.can.see.all.tokens";

private static final String CLOUDERA_MANAGER_DESCRIPTORS_MONITOR_INTERVAL = GATEWAY_CONFIG_FILE_PREFIX + ".cloudera.manager.descriptors.monitor.interval";
private static final String CLOUDERA_MANAGER_ADVANCED_SERVICE_DISCOVERY_CONF_MONITOR_INTERVAL = GATEWAY_CONFIG_FILE_PREFIX + ".cloudera.manager.advanced.service.discovery.config.monitor.interval";
Expand Down Expand Up @@ -1489,4 +1490,10 @@ public boolean isAsyncSupported() {
return getBoolean(GATEWAY_SERVLET_ASYNC_SUPPORTED, GATEWAY_SERVLET_ASYNC_SUPPORTED_DEFAULT);
}

@Override
public boolean canSeeAllTokens(String userName) {
final Collection<String> usersCanSeeAllTokens = getTrimmedStringCollection(USERS_CAN_SEE_ALL_TOKENS);
return usersCanSeeAllTokens == null ? false : usersCanSeeAllTokens.contains(userName);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,11 @@ public TokenMetadata getTokenMetadata(String tokenId) throws UnknownTokenExcepti
return metadataMap.get(tokenId);
}

@Override
public Collection<KnoxToken> getAllTokens() {
return fetchTokens(null, false);
}

@Override
public Collection<KnoxToken> getTokens(String userName) {
return fetchTokens(userName, false);
Expand All @@ -432,10 +437,14 @@ public Collection<KnoxToken> getDoAsTokens(String createdBy) {
private Collection<KnoxToken> fetchTokens(String userName, boolean createdBy) {
final Collection<KnoxToken> tokens = new TreeSet<>();
final Predicate<Map.Entry<String, TokenMetadata>> filterPredicate;
if (createdBy) {
filterPredicate = entry -> userName.equals(entry.getValue().getCreatedBy());
if (userName == null) {
filterPredicate = entry -> true;
} else {
filterPredicate = entry -> userName.equals(entry.getValue().getUserName());
if (createdBy) {
filterPredicate = entry -> userName.equals(entry.getValue().getCreatedBy());
} else {
filterPredicate = entry -> userName.equals(entry.getValue().getUserName());
}
}
metadataMap.entrySet().stream().filter(filterPredicate).forEach(metadata -> {
String tokenId = metadata.getKey();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,16 @@ public TokenMetadata getTokenMetadata(String tokenId) throws UnknownTokenExcepti
return tokenMetadata;
}

@Override
public Collection<KnoxToken> getAllTokens() {
try {
return tokenDatabase.getAllTokens();
} catch (SQLException e) {
log.errorFetchingAllTokensFromDatabase(e.getMessage(), e);
return Collections.emptyList();
}
}

@Override
public Collection<KnoxToken> getTokens(String userName) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ public class TokenStateDatabase {
private static final String ADD_METADATA_SQL = "INSERT INTO " + TOKEN_METADATA_TABLE_NAME + "(token_id, md_name, md_value) VALUES(?, ?, ?)";
private static final String UPDATE_METADATA_SQL = "UPDATE " + TOKEN_METADATA_TABLE_NAME + " SET md_value = ? WHERE token_id = ? AND md_name = ?";
private static final String GET_METADATA_SQL = "SELECT md_name, md_value FROM " + TOKEN_METADATA_TABLE_NAME + " WHERE token_id = ?";
private static final String GET_TOKENS_BY_USER_NAME_SQL = "SELECT kt.token_id, kt.issue_time, kt.expiration, kt.max_lifetime, ktm.md_name, ktm.md_value FROM " + TOKENS_TABLE_NAME
+ " kt, " + TOKEN_METADATA_TABLE_NAME + " ktm WHERE kt.token_id = ktm.token_id AND kt.token_id IN (SELECT token_id FROM " + TOKEN_METADATA_TABLE_NAME + " WHERE md_name = '" + TokenMetadata.USER_NAME + "' AND md_value = ? )"
private static final String GET_ALL_TOKENS_SQL = "SELECT kt.token_id, kt.issue_time, kt.expiration, kt.max_lifetime, ktm.md_name, ktm.md_value FROM " + TOKENS_TABLE_NAME
+ " kt, " + TOKEN_METADATA_TABLE_NAME + " ktm WHERE kt.token_id = ktm.token_id";
private static final String GET_TOKENS_BY_USER_NAME_SQL = GET_ALL_TOKENS_SQL + " AND kt.token_id IN (SELECT token_id FROM " + TOKEN_METADATA_TABLE_NAME + " WHERE md_name = '" + TokenMetadata.USER_NAME + "' AND md_value = ? )"
+ " ORDER BY kt.issue_time";
private static final String GET_TOKENS_CREATED_BY_USER_NAME_SQL = "SELECT kt.token_id, kt.issue_time, kt.expiration, kt.max_lifetime, ktm.md_name, ktm.md_value FROM " + TOKENS_TABLE_NAME
+ " kt, " + TOKEN_METADATA_TABLE_NAME + " ktm WHERE kt.token_id = ktm.token_id AND kt.token_id IN (SELECT token_id FROM " + TOKEN_METADATA_TABLE_NAME + " WHERE md_name = '" + TokenMetadata.CREATED_BY + "' AND md_value = ? )"
private static final String GET_TOKENS_CREATED_BY_USER_NAME_SQL = GET_ALL_TOKENS_SQL + " AND kt.token_id IN (SELECT token_id FROM " + TOKEN_METADATA_TABLE_NAME + " WHERE md_name = '" + TokenMetadata.CREATED_BY + "' AND md_value = ? )"
+ " ORDER BY kt.issue_time";

private final DataSource dataSource;
Expand Down Expand Up @@ -181,6 +181,10 @@ private static String decodeMetadata(String metadataName, String metadataValue)
return metadataName.equals(TokenMetadata.PASSCODE) ? new String(Base64.decodeBase64(metadataValue.getBytes(UTF_8)), UTF_8) : metadataValue;
}

Collection<KnoxToken> getAllTokens() throws SQLException {
return fetchTokens(null, GET_ALL_TOKENS_SQL);
}

Collection<KnoxToken> getTokens(String userName) throws SQLException {
return fetchTokens(userName, GET_TOKENS_BY_USER_NAME_SQL);
}
Expand All @@ -192,7 +196,9 @@ Collection<KnoxToken> getDoAsTokens(String userName) throws SQLException {
private Collection<KnoxToken> fetchTokens(String userName, String sql) throws SQLException {
Map<String, KnoxToken> tokenMap = new LinkedHashMap<>();
try (Connection connection = dataSource.getConnection(); PreparedStatement getTokenIdsStatement = connection.prepareStatement(sql)) {
getTokenIdsStatement.setString(1, userName);
if (userName != null) {
getTokenIdsStatement.setString(1, userName);
}
try (ResultSet rs = getTokenIdsStatement.executeQuery()) {
while (rs.next()) {
String tokenId = rs.getString(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ public interface TokenStateServiceMessages {
@Message(level = MessageLevel.ERROR, text = "An error occurred while fetching metadata for {0} from the database : {1}")
void errorFetchingMetadataFromDatabase(String tokenId, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);

@Message(level = MessageLevel.ERROR, text = "An error occurred while fetching all tokens from the database : {0}")
void errorFetchingAllTokensFromDatabase(String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);

@Message(level = MessageLevel.ERROR, text = "An error occurred while fetching tokens for user {0} from the database : {1}")
void errorFetchingTokensForUserFromDatabase(String userName, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ public void testAddTokensForMultipleUsers() throws Exception {
String id1 = "token1";
String id2 = "token2";
String id3 = "token3";
String createdBy3 = "createdBy3";

long issueTime1 = 1;
long expiration1 = 1;
Expand All @@ -144,15 +145,39 @@ public void testAddTokensForMultipleUsers() throws Exception {

List<KnoxToken> user2Tokens = new ArrayList<>(jdbcTokenStateService.getTokens(user2));
assertEquals(1, user2Tokens.size());
assertToken(user2Tokens.get(0), id3, expiration3, comment3, issueTime3);
KnoxToken token3 = user2Tokens.get(0);
assertToken(token3, id3, expiration3, comment3, issueTime3);

// check doAs tokens
TokenMetadata token3Metadata = new TokenMetadata(token3.getMetadata().getUserName());
token3Metadata.add(TokenMetadata.CREATED_BY, createdBy3);
jdbcTokenStateService.addMetadata(id3, token3Metadata);
List<KnoxToken> createdBy3Tokens = new ArrayList<>(jdbcTokenStateService.getDoAsTokens(createdBy3));
assertEquals(1, createdBy3Tokens.size());
KnoxToken createdBy3Token = createdBy3Tokens.get(0);
assertToken(createdBy3Token, id3, expiration3, comment3, issueTime3, createdBy3);

// check all tokens
List<KnoxToken> allTokens = new ArrayList<>(jdbcTokenStateService.getAllTokens());
assertEquals(3, allTokens.size());
assertToken(allTokens.get(0), id1, expiration1, comment1, issueTime1);
assertToken(allTokens.get(1), id2, expiration2, comment2, issueTime2);
assertToken(allTokens.get(2), id3, expiration3, comment3, issueTime3, createdBy3);
}

private void assertToken(KnoxToken knoxToken, String tokenId, long expiration, String comment, long issueTime) {
assertToken(knoxToken, tokenId, expiration, comment, issueTime, null);
}

private void assertToken(KnoxToken knoxToken, String tokenId, long expiration, String comment, long issueTime, String createdBy) {
SimpleDateFormat df = new SimpleDateFormat(KnoxToken.DATE_FORMAT, Locale.getDefault());
assertEquals(tokenId, knoxToken.getTokenId());
assertEquals(df.format(new Date(issueTime)), knoxToken.getIssueTime());
assertEquals(df.format(new Date(expiration)), knoxToken.getExpiration());
assertEquals(comment, knoxToken.getMetadata().getComment());
if (createdBy != null) {
assertEquals(createdBy, knoxToken.getMetadata().getCreatedBy());
}
}

private void saveToken(String user, String tokenId, long issueTime, long expiration, String comment) {
Expand Down
5 changes: 5 additions & 0 deletions gateway-service-knoxtoken/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@
</exclusions>
</dependency>

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>

<dependency>
<groupId>org.apache.knox</groupId>
<artifactId>gateway-test-utils</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import javax.security.auth.Subject;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
Expand All @@ -56,6 +57,7 @@
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

import com.google.gson.Gson;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.KeyLengthException;
import com.nimbusds.jose.crypto.MACSigner;
Expand Down Expand Up @@ -138,14 +140,18 @@ public class TokenResource {
static final String GET_TSS_STATUS_PATH = "/getTssStatus";
static final String RENEW_PATH = "/renew";
static final String REVOKE_PATH = "/revoke";
static final String BATCH_REVOKE_PATH = "/revokeTokens";
static final String ENABLE_PATH = "/enable";
static final String BATCH_ENABLE_PATH = "/enableTokens";
static final String DISABLE_PATH = "/disable";
static final String BATCH_DISABLE_PATH = "/disableTokens";
private static final String TARGET_ENDPOINT_PULIC_CERT_PEM = TOKEN_PARAM_PREFIX + "target.endpoint.cert.pem";
static final String QUERY_PARAMETER_DOAS = "doAs";
private static final String IMPERSONATION_ENABLED_TEXT = "impersonationEnabled";
public static final String KNOX_TOKEN_INCLUDE_GROUPS = TOKEN_PARAM_PREFIX + "include.groups";
public static final String KNOX_TOKEN_ISSUER = TOKEN_PARAM_PREFIX + "issuer";
private static TokenServiceMessages log = MessagesFactory.get(TokenServiceMessages.class);
private static final Gson GSON = new Gson();
private long tokenTTL = TOKEN_TTL_DEFAULT;
private String tokenType;
private String tokenTTLAsText;
Expand Down Expand Up @@ -189,7 +195,8 @@ public enum ErrorCode {
INVALID_TOKEN(40),
UNKNOWN_TOKEN(50),
ALREADY_DISABLED(60),
ALREADY_ENABLED(70);
ALREADY_ENABLED(70),
DISABLED_KNOXSSO_COOKIE(80);

private final int code;

Expand Down Expand Up @@ -452,8 +459,11 @@ public Response getUserTokens(@Context UriInfo uriInfo) {
final String userName = uriInfo.getQueryParameters().getFirst("userName");
final String createdBy = uriInfo.getQueryParameters().getFirst("createdBy");
final String userNameOrCreatedBy = uriInfo.getQueryParameters().getFirst("userNameOrCreatedBy");
final boolean allTokens = Boolean.parseBoolean(uriInfo.getQueryParameters().getFirst("allTokens"));
final Collection<KnoxToken> userTokens;
if (userNameOrCreatedBy == null) {
if (allTokens) {
userTokens = tokenStateService.getAllTokens();
} else if (userNameOrCreatedBy == null) {
userTokens = createdBy == null ? tokenStateService.getTokens(userName) : tokenStateService.getDoAsTokens(createdBy);
} else {
userTokens = new HashSet<>(tokenStateService.getTokens(userNameOrCreatedBy));
Expand Down Expand Up @@ -559,6 +569,22 @@ public Response renew(String token) {
return resp;
}

@DELETE
@Path(BATCH_REVOKE_PATH)
@Produces({APPLICATION_JSON})
public Response revokeTokens(String tokenIds) {
final List<String> ids = GSON.fromJson(tokenIds, List.class);
Response response = null;
Response error = null;
for (String tokenId : ids) {
response = revoke(tokenId);
if (response.getStatus() != Response.Status.OK.getStatusCode()) {
error = response;
}
}
return error == null ? response : error;
}

@DELETE
@Path(REVOKE_PATH)
@Produces({APPLICATION_JSON})
Expand All @@ -580,8 +606,7 @@ public Response revoke(String token) {
errorStatus = Response.Status.FORBIDDEN;
error = "SSO cookie (" + Tokens.getTokenIDDisplayText(tokenId) + ") cannot not be revoked." ;
errorCode = ErrorCode.UNAUTHORIZED;
}
if (StringUtils.isBlank(error) && (triesToRevokeOwnToken(tokenId, revoker) || allowedRenewers.contains(revoker))) {
} else if (triesToRevokeOwnToken(tokenId, revoker) || allowedRenewers.contains(revoker)) {
tokenStateService.revokeToken(tokenId);
log.revokedToken(getTopologyName(),
Tokens.getTokenDisplayText(token),
Expand Down Expand Up @@ -648,17 +673,47 @@ private String getTokenId(String token) throws ParseException {
@Path(ENABLE_PATH)
@Produces({ APPLICATION_JSON })
public Response enable(String tokenId) {
return setTokenEnabledFlag(tokenId, true);
return setTokenEnabledFlag(tokenId, true, false);
}

@PUT
@Path(BATCH_ENABLE_PATH)
@Consumes({ APPLICATION_JSON })
@Produces({ APPLICATION_JSON })
public Response enableTokens(String tokenIds) {
return setTokenEnabledFlags(tokenIds, true);
}

@PUT
@Path(DISABLE_PATH)
@Produces({ APPLICATION_JSON })
public Response disable(String tokenId) {
return setTokenEnabledFlag(tokenId, false);
return setTokenEnabledFlag(tokenId, false, false);
}

@PUT
@Path(BATCH_DISABLE_PATH)
@Consumes({ APPLICATION_JSON })
@Produces({ APPLICATION_JSON })
public Response disableTokens(String tokenIds) {
return setTokenEnabledFlags(tokenIds, false);
}

private Response setTokenEnabledFlag(String tokenId, boolean enabled) {
@SuppressWarnings("unchecked")
private Response setTokenEnabledFlags(String tokenIds, boolean enabled) {
final List<String> ids = GSON.fromJson(tokenIds, List.class);
Response response = null;
Response error = null;
for (String tokenId : ids) {
response = setTokenEnabledFlag(tokenId, enabled, true);
if (response.getStatus() != Response.Status.OK.getStatusCode()) {
error = response;
}
}
return error == null ? response : error;
}

private Response setTokenEnabledFlag(String tokenId, boolean enabled, boolean batch) {
String error = "";
ErrorCode errorCode = ErrorCode.UNKNOWN;
if (tokenStateService == null) {
Expand All @@ -667,12 +722,15 @@ private Response setTokenEnabledFlag(String tokenId, boolean enabled) {
} else {
try {
final TokenMetadata tokenMetadata = tokenStateService.getTokenMetadata(tokenId);
if (enabled && tokenMetadata.isEnabled()) {
if (!batch && enabled && tokenMetadata.isEnabled()) {
error = "Token is already enabled";
errorCode = ErrorCode.ALREADY_ENABLED;
} else if (!enabled && !tokenMetadata.isEnabled()) {
} else if (!batch && !enabled && !tokenMetadata.isEnabled()) {
error = "Token is already disabled";
errorCode = ErrorCode.ALREADY_DISABLED;
} else if (enabled && tokenMetadata.isKnoxSsoCookie()) {
error = "Disabled KnoxSSO Cookies cannot not be enabled";
errorCode = ErrorCode.DISABLED_KNOXSSO_COOKIE;
} else {
tokenMetadata.setEnabled(enabled);
tokenStateService.addMetadata(tokenId, tokenMetadata);
Expand All @@ -682,7 +740,9 @@ private Response setTokenEnabledFlag(String tokenId, boolean enabled) {
errorCode = ErrorCode.UNKNOWN_TOKEN;
}
}

if (error.isEmpty()) {
log.setEnabledFlag(getTopologyName(), enabled, Tokens.getTokenIDDisplayText(tokenId));
return Response.status(Response.Status.OK).entity("{\n \"setEnabledFlag\": \"true\",\n \"isEnabled\": \"" + enabled + "\"\n}\n").build();
} else {
log.badSetEnabledFlagRequest(getTopologyName(), Tokens.getTokenIDDisplayText(tokenId), error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public interface TokenServiceMessages {
@Message( level = MessageLevel.INFO, text = "Knox Token service ({0}) revoked token {1} ({2}) (renewer={3})")
void revokedToken(String topologyName, String tokenDisplayText, String tokenId, String renewer);

@Message( level = MessageLevel.INFO, text = "Knox Token service ({0}) set enabled flag to {1} on token {2}")
void setEnabledFlag(String topologyName, boolean enabled, String tokenId);

@Message( level = MessageLevel.ERROR, text = "Unable to issue token.")
void unableToIssueToken(@StackTrace( level = MessageLevel.DEBUG) Exception e);

Expand Down
Loading

0 comments on commit f913856

Please sign in to comment.