Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KNOX-2712 - Managing custom Knox Token metadata #542

Merged
merged 1 commit into from
Mar 10, 2022
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
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 @@ -46,9 +48,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 @@ -119,6 +121,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 @@ -733,6 +768,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 @@ -747,6 +783,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 @@ -65,7 +65,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 @@ -153,6 +156,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 @@ -1007,7 +1011,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 @@ -1021,6 +1025,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 @@ -1063,7 +1076,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);">Disable</button>
<button *ngIf="!knoxToken.metadata.enabled && !isTokenExpired(knoxToken.expirationLong)" (click)="enableToken(knoxToken.tokenId);">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));
}

}