Skip to content

Commit

Permalink
CDPD-35799 KNOX-2712 - Managing custom Knox Token metadata (apache#542)
Browse files Browse the repository at this point in the history
Change-Id: I68b16236000dafda0ae627a98426d93948dca9d4
  • Loading branch information
smolnar82 authored and Sandor Molnar committed Mar 11, 2022
1 parent e955cba commit 483bc15
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.TreeSet;
import java.util.UUID;

import javax.annotation.PostConstruct;
Expand All @@ -45,9 +47,9 @@
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.KeyLengthException;
Expand Down Expand Up @@ -118,6 +120,7 @@ public class TokenResource {
private static final String LIFESPAN_INPUT_ENABLED_PARAM = "knox.token.lifespan.input.enabled";
private static final String LIFESPAN_INPUT_ENABLED_TEXT = "lifespanInputEnabled";
static final String KNOX_TOKEN_USER_LIMIT_EXCEEDED_ACTION = "knox.token.user.limit.exceeded.action";
private static final String METADATA_QUERY_PARAM_PREFIX = "md_";
private static final long TOKEN_TTL_DEFAULT = 30000L;
static final String TOKEN_API_PATH = "knoxtoken/api/v1";
static final String RESOURCE_PATH = TOKEN_API_PATH + "/token";
Expand Down Expand Up @@ -405,11 +408,43 @@ public Response doPost() {
@GET
@Path(GET_USER_TOKENS)
@Produces({APPLICATION_JSON, APPLICATION_XML})
public Response getUserTokens(@QueryParam("userName") String userName) {
public Response getUserTokens(@Context UriInfo uriInfo) {
if (tokenStateService == null) {
return Response.status(Response.Status.SERVICE_UNAVAILABLE).entity("{\n \"error\": \"Token management is not configured\"\n}\n").build();
} else {
final Collection<KnoxToken> tokens = tokenStateService.getTokens(userName);
if (uriInfo == null) {
throw new IllegalArgumentException("URI info cannot be NULL.");
}
final Map<String, String> metadataMap = new HashMap<>();
uriInfo.getQueryParameters().entrySet().forEach(entry -> {
if (entry.getKey().startsWith(METADATA_QUERY_PARAM_PREFIX)) {
String metadataName = entry.getKey().substring(METADATA_QUERY_PARAM_PREFIX.length());
metadataMap.put(metadataName, entry.getValue().get(0));
}
});

final String userName = uriInfo.getQueryParameters().getFirst("userName");
final Collection<KnoxToken> userTokens = tokenStateService.getTokens(userName);
final Collection<KnoxToken> tokens = new TreeSet<>();
if (metadataMap.isEmpty()) {
tokens.addAll(userTokens);
} else {
userTokens.forEach(knoxToken -> {
for (Map.Entry<String, String> entry : metadataMap.entrySet()) {
if (StringUtils.isBlank(entry.getValue()) || "*".equals(entry.getValue())) {
// we should only filter tokens by metadata name
if (knoxToken.hasMetadata(entry.getKey())) {
tokens.add(knoxToken);
}
} else {
// metadata value should also match
if (entry.getValue().equals(knoxToken.getMetadataValue(entry.getKey()))) {
tokens.add(knoxToken);
}
}
}
});
}
return Response.status(Response.Status.OK).entity(JsonUtils.renderAsJsonString(Collections.singletonMap("tokens", tokens))).build();
}
}
Expand Down Expand Up @@ -740,6 +775,7 @@ private Response getAuthenticationToken() {
final String comment = request.getParameter(COMMENT);
final TokenMetadata tokenMetadata = new TokenMetadata(p.getName(), StringUtils.isBlank(comment) ? null : comment);
tokenMetadata.setPasscode(tokenMAC.hash(tokenId, issueTime, p.getName(), passcode));
addArbitraryTokenMetadata(tokenMetadata);
tokenStateService.addMetadata(tokenId, tokenMetadata);
log.storedToken(getTopologyName(), Tokens.getTokenDisplayText(accessToken), Tokens.getTokenIDDisplayText(tokenId));
}
Expand All @@ -754,6 +790,18 @@ private Response getAuthenticationToken() {
return Response.ok().entity("{ \"Unable to acquire token.\" }").build();
}

private void addArbitraryTokenMetadata(TokenMetadata tokenMetadata) {
final Enumeration<String> paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
final String paramName = paramNames.nextElement();
if (paramName.startsWith(METADATA_QUERY_PARAM_PREFIX)) {
final String metadataName = paramName.substring(METADATA_QUERY_PARAM_PREFIX.length());
final String metadataValue = request.getParameter(paramName);
tokenMetadata.add(metadataName, metadataValue);
}
}
}

private String generatePasscodeField(String tokenId, String passcode) {
final String base64TokenIdPasscode = Base64.encodeBase64String(tokenId.getBytes(StandardCharsets.UTF_8)) + "::" + Base64.encodeBase64String(passcode.getBytes(StandardCharsets.UTF_8));
return Base64.encodeBase64String(base64TokenIdPasscode.getBytes(StandardCharsets.UTF_8));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@
import javax.security.auth.Subject;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

import java.io.IOException;
import java.security.KeyPair;
Expand Down Expand Up @@ -152,6 +155,7 @@ private void configureCommonExpectations(Map<String, String> contextExpectations
if (contextExpectations.containsKey(TokenResource.LIFESPAN)) {
EasyMock.expect(request.getParameter(TokenResource.LIFESPAN)).andReturn(contextExpectations.get(TokenResource.LIFESPAN)).anyTimes();
}
EasyMock.expect(request.getParameterNames()).andReturn(Collections.emptyEnumeration()).anyTimes();

GatewayServices services = EasyMock.createNiceMock(GatewayServices.class);
EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services).anyTimes();
Expand Down Expand Up @@ -1002,7 +1006,7 @@ public void testTokenLimitChangeAfterAlreadyHavingTokens() throws Exception {
for (int i = 0; i < numberOfPreExistingTokens; i++) {
tr.doGet();
}
Response getKnoxTokensResponse = tr.getUserTokens(USER_NAME);
Response getKnoxTokensResponse = getUserTokensResponse(tr);
Collection<String> tokens = ((Map<String, Collection<String>>) JsonUtils.getObjectFromJsonString(getKnoxTokensResponse.getEntity().toString()))
.get("tokens");
assertEquals(tokens.size(), numberOfPreExistingTokens);
Expand All @@ -1016,6 +1020,15 @@ public void testTokenLimitChangeAfterAlreadyHavingTokens() throws Exception {
assertTrue(response.getEntity().toString().contains("Unable to get token - token limit exceeded."));
}

private Response getUserTokensResponse(TokenResource tokenResource) {
final MultivaluedMap<String, String> queryParameters = new MultivaluedHashMap<>();
queryParameters.put("userName", Arrays.asList(USER_NAME));
final UriInfo uriInfo = EasyMock.createNiceMock(UriInfo.class);
EasyMock.expect(uriInfo.getQueryParameters()).andReturn(queryParameters).anyTimes();
EasyMock.replay(uriInfo);
return tokenResource.getUserTokens(uriInfo);
}

@Test
public void testTokenLimitPerUserExceeded() throws Exception {
try {
Expand Down Expand Up @@ -1058,7 +1071,7 @@ private void testLimitingTokensPerUser(int configuredLimit, int numberOfTokens,
throw new Exception(getTokenResponse.getEntity().toString());
}
}
final Response getKnoxTokensResponse = tr.getUserTokens(USER_NAME);
final Response getKnoxTokensResponse = getUserTokensResponse(tr);
final Collection<String> tokens = ((Map<String, Collection<String>>) JsonUtils.getObjectFromJsonString(getKnoxTokensResponse.getEntity().toString()))
.get("tokens");
assertEquals(tokens.size(), revokeOldestToken ? configuredLimit : numberOfTokens);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ public void setMetadata(TokenMetadata metadata) {
this.metadata = metadata;
}

public String getMetadataValue(String key) {
return this.metadata == null ? null : metadata.getMetadata(key);
}

public boolean hasMetadata(String key) {
return getMetadataValue(key) != null;
}

@Override
public int compareTo(KnoxToken other) {
return Long.compare(this.issueTime, other.issueTime);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
*/
package org.apache.knox.gateway.services.security.token;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
Expand All @@ -33,6 +35,7 @@ public class TokenMetadata {
public static final String COMMENT = "comment";
public static final String ENABLED = "enabled";
public static final String PASSCODE = "passcode";
private static final List<String> KNOWN_MD_NAMES = Arrays.asList(USER_NAME, COMMENT, ENABLED, PASSCODE);

private final Map<String, String> metadataMap = new HashMap<>();

Expand Down Expand Up @@ -66,6 +69,21 @@ public Map<String, String> getMetadataMap() {
return new HashMap<String, String>(this.metadataMap);
}

@JsonIgnore
public String getMetadata(String key) {
return this.metadataMap.get(key);
}

public Map<String, String> getCustomMetadataMap() {
final Map<String, String> customMetadataMap = new HashMap<>();
this.metadataMap.forEach((key, value) -> {
if (!KNOWN_MD_NAMES.contains(key)) {
customMetadataMap.put(key, value);
}
});
return customMetadataMap;
}

public String getUserName() {
return metadataMap.get(USER_NAME);
}
Expand Down
1 change: 1 addition & 0 deletions knox-token-management-ui/token-management/app/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export class Metadata {
enabled: boolean;
userName: string;
comment: string;
customMetadataMap: Map<string, string>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<th>Issued</th>
<th>Expires</th>
<th>Comment</th>
<th>Additional Metadata</th>
<th>Actions</th>
</tr>
</thead>
Expand All @@ -38,6 +39,13 @@
<td *ngIf="!isTokenExpired(knoxToken.expirationLong)" style="color: green">{{formatDateTime(knoxToken.expirationLong)}}</td>
<td *ngIf="isTokenExpired(knoxToken.expirationLong)" style="color: red">{{formatDateTime(knoxToken.expirationLong)}}</td>
<td>{{knoxToken.metadata.comment}}</td>
<td>
<ul>
<li *ngFor="let metadata of getCustomMetadataArray(knoxToken)">
{{metadata[0]}} = {{metadata[1]}}
</li>
</ul>
</td>
<td>
<button *ngIf="knoxToken.metadata.enabled && !isTokenExpired(knoxToken.expirationLong)" (click)="disableToken(knoxToken.tokenId);" class="btn btn-primary">Disable</button>
<button *ngIf="!knoxToken.metadata.enabled && !isTokenExpired(knoxToken.expirationLong)" (click)="enableToken(knoxToken.tokenId);" class="btn btn-primary">Enable</button>
Expand All @@ -47,7 +55,7 @@
</tbody>
<tfoot>
<tr>
<td colspan="5">
<td colspan="6">
<mfBootstrapPaginator [rowsOnPageSet]="[5,10,15]"></mfBootstrapPaginator>
</td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,12 @@ export class TokenManagementComponent implements OnInit {
return Date.now() > expiration;
}

getCustomMetadataArray(knoxToken: KnoxToken): [string, string][] {
let mdMap = new Map();
if (knoxToken.metadata.customMetadataMap) {
mdMap = knoxToken.metadata.customMetadataMap;
}
return Array.from(Object.entries(mdMap));
}

}

0 comments on commit 483bc15

Please sign in to comment.