Commit e55befcc authored by Nils Bandener's avatar Nils Bandener
Browse files

Merge remote-tracking branch 'origin/SGD-24-rebase' into es-7.9.3-auth-token-beta

parents c5982d1d 6bdcc098
......@@ -67,7 +67,7 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
private Configuration jsonPathConfig;
private Map<String, JsonPath> attributeMapping;
public AbstractHTTPJwtAuthenticator(Settings settings, Path configPath) {
protected AbstractHTTPJwtAuthenticator(Settings settings, Path configPath) {
jwtUrlParameter = settings.get("jwt_url_parameter");
jwtHeaderName = settings.get("jwt_header", "Authorization");
rolesKey = settings.get("roles_key");
......@@ -83,16 +83,15 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
}
if ((subjectKey != null && jsonSubjectPath != null) || (rolesKey != null && jsonRolesPath != null)) {
throw new IllegalStateException("Both, subject_key and subject_path or roles_key and roles_path have simultaneously provided." +
" Please provide only one combination.");
throw new IllegalStateException("Both, subject_key and subject_path or roles_key and roles_path have simultaneously provided."
+ " Please provide only one combination.");
}
jsonPathConfig = Configuration.builder().options(Option.ALWAYS_RETURN_LIST).build();
attributeMapping = UserAttributes.getAttributeMapping(settings.getAsSettings("map_claims_to_user_attrs"));
}
@Override
public AuthCredentials extractCredentials(RestRequest request, ThreadContext context)
throws ElasticsearchSecurityException {
public AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws ElasticsearchSecurityException {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
......@@ -188,8 +187,7 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
// We expect a String. If we find something else, convert to String but issue a
// warning
if (!(subjectObject instanceof String)) {
log.warn(
"Expected type String for roles in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.",
log.warn("Expected type String for roles in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.",
subjectKey, subjectObject, subjectObject.getClass());
subject = String.valueOf(subjectObject);
} else {
......@@ -224,8 +222,7 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator
Object rolesObject = claims.getClaim(rolesKey);
if (rolesObject == null) {
log.warn(
"Failed to get roles from JWT claims with roles_key '{}'. Check if this key is correct and available in the JWT payload.",
log.warn("Failed to get roles from JWT claims with roles_key '{}'. Check if this key is correct and available in the JWT payload.",
rolesKey);
return new String[0];
}
......
/*
* Copyright 2020 by floragunn GmbH - All rights reserved
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed here is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* This software is free of charge for non-commercial and academic use.
* For commercial use in a production environment you have to obtain a license
* from https://floragunn.com
*
*/
package com.floragunn.searchguard.authtoken;
import java.io.IOException;
import java.io.Serializable;
import java.time.Instant;
import java.util.Map;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import com.fasterxml.jackson.databind.JsonNode;
import com.floragunn.searchsupport.config.validation.ConfigValidationException;
import com.floragunn.searchsupport.config.validation.MissingAttribute;
import com.floragunn.searchsupport.config.validation.ValidatingJsonNode;
import com.floragunn.searchsupport.config.validation.ValidationErrors;
import com.google.common.collect.ImmutableMap;
public class AuthToken implements ToXContentObject, Writeable, Serializable {
public static final Map<String, Object> INDEX_MAPPING = ImmutableMap.of("dynamic", true, "properties",
ImmutableMap.of("created_at", ImmutableMap.of("type", "date"), "expires_at", ImmutableMap.of("type", "date")));
private static final long serialVersionUID = 6038589333544878668L;
private final String userName;
private final String tokenName;
private final String id;
private final Instant creationTime;
private final Instant expiryTime;
private final Instant revokedAt;
private final RequestedPrivileges requestedPrivileges;
private final AuthTokenPrivilegeBase base;
AuthToken(String id, String userName, String tokenName, RequestedPrivileges requestedPrivileges, AuthTokenPrivilegeBase base, Instant creationTime,
Instant expiryTime, Instant revokedAt) {
this.id = id;
this.userName = userName;
this.tokenName = tokenName;
this.requestedPrivileges = requestedPrivileges;
this.base = base;
this.creationTime = creationTime;
this.expiryTime = expiryTime;
this.revokedAt = revokedAt;
}
public AuthToken(StreamInput in) throws IOException {
this.id = in.readString();
this.userName = in.readString();
this.tokenName = in.readOptionalString();
this.creationTime = in.readInstant();
this.expiryTime = in.readOptionalInstant();
this.revokedAt = in.readOptionalInstant();
this.requestedPrivileges = new RequestedPrivileges(in);
this.base = new AuthTokenPrivilegeBase(in);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
toXContentFragment(builder, params);
builder.endObject();
return builder;
}
public XContentBuilder toXContentFragment(XContentBuilder builder, Params params) throws IOException {
builder.field("user_name", userName);
builder.field("token_name", tokenName);
builder.field("requested", requestedPrivileges);
builder.field("base");
base.toXContent(builder, params);
builder.field("created_at", creationTime.toEpochMilli());
if (expiryTime != null) {
builder.field("expires_at", expiryTime.toEpochMilli());
}
if (revokedAt != null) {
builder.field("revoked_at", revokedAt.toEpochMilli());
}
return builder;
}
public String getId() {
return id;
}
public RequestedPrivileges getRequestedPrivileges() {
return requestedPrivileges;
}
public String getUserName() {
return userName;
}
public String getTokenName() {
return tokenName;
}
public AuthTokenPrivilegeBase getBase() {
return base;
}
public boolean isRevoked() {
return revokedAt != null;
}
AuthToken getRevokedInstance() {
AuthToken revoked = new AuthToken(id, userName, tokenName, requestedPrivileges, base, creationTime, expiryTime, Instant.now());
revoked.getBase().setConfigSnapshot(null);
return revoked;
}
public static AuthToken parse(String id, JsonNode jsonNode) throws ConfigValidationException {
ValidationErrors validationErrors = new ValidationErrors();
ValidatingJsonNode vJsonNode = new ValidatingJsonNode(jsonNode, validationErrors);
String userName = vJsonNode.requiredString("user_name");
String tokenName = vJsonNode.string("token_name");
AuthTokenPrivilegeBase base = null;
RequestedPrivileges requestedPrivilges = null;
if (vJsonNode.hasNonNull("base")) {
try {
base = AuthTokenPrivilegeBase.parse(vJsonNode.get("base"));
} catch (ConfigValidationException e) {
validationErrors.add("base", e);
}
} else {
validationErrors.add(new MissingAttribute("base", jsonNode));
}
if (vJsonNode.hasNonNull("requested")) {
try {
requestedPrivilges = RequestedPrivileges.parse(vJsonNode.get("requested"));
} catch (ConfigValidationException e) {
validationErrors.add("requested", e);
}
} else {
validationErrors.add(new MissingAttribute("requested", jsonNode));
}
Instant createdAt = vJsonNode.requiredValue("created_at", (v) -> Instant.ofEpochMilli(v.longValue()));
Instant expiry = vJsonNode.value("expires_at", (v) -> Instant.ofEpochMilli(v.longValue()), null);
Instant revokedAt = vJsonNode.value("revoked_at", (v) -> Instant.ofEpochMilli(v.longValue()), null);
validationErrors.throwExceptionForPresentErrors();
return new AuthToken(id, userName, tokenName, requestedPrivilges, base, createdAt, expiry, revokedAt);
}
public Instant getCreationTime() {
return creationTime;
}
public Instant getExpiryTime() {
return expiryTime;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(this.id);
out.writeString(this.userName);
out.writeOptionalString(this.tokenName);
out.writeInstant(this.creationTime);
out.writeOptionalInstant(this.expiryTime);
out.writeOptionalInstant(this.revokedAt);
this.requestedPrivileges.writeTo(out);
this.base.writeTo(out);
}
public Instant getRevokedAt() {
return revokedAt;
}
@Override
public String toString() {
return "AuthToken [userName=" + userName + ", tokenName=" + tokenName + ", id=" + id + ", creationTime=" + creationTime + ", expiryTime="
+ expiryTime + ", revokedAt=" + revokedAt + ", requestedPrivilges=" + requestedPrivileges + ", base=" + base + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((base == null) ? 0 : base.hashCode());
result = prime * result + ((creationTime == null) ? 0 : creationTime.hashCode());
result = prime * result + ((expiryTime == null) ? 0 : expiryTime.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((requestedPrivileges == null) ? 0 : requestedPrivileges.hashCode());
result = prime * result + ((tokenName == null) ? 0 : tokenName.hashCode());
result = prime * result + ((userName == null) ? 0 : userName.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AuthToken other = (AuthToken) obj;
if (base == null) {
if (other.base != null)
return false;
} else if (!base.equals(other.base))
return false;
if (creationTime == null) {
if (other.creationTime != null)
return false;
} else if (!creationTime.equals(other.creationTime))
return false;
if (expiryTime == null) {
if (other.expiryTime != null)
return false;
} else if (!expiryTime.equals(other.expiryTime))
return false;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (requestedPrivileges == null) {
if (other.requestedPrivileges != null)
return false;
} else if (!requestedPrivileges.equals(other.requestedPrivileges))
return false;
if (tokenName == null) {
if (other.tokenName != null)
return false;
} else if (!tokenName.equals(other.tokenName))
return false;
if (userName == null) {
if (other.userName != null)
return false;
} else if (!userName.equals(other.userName))
return false;
return true;
}
}
/*
* Copyright 2020 by floragunn GmbH - All rights reserved
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed here is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* This software is free of charge for non-commercial and academic use.
* For commercial use in a production environment you have to obtain a license
* from https://floragunn.com
*
*/
package com.floragunn.searchguard.authtoken;
import org.elasticsearch.ElasticsearchSecurityException;
import com.floragunn.searchguard.auth.AuthenticationBackend;
import com.floragunn.searchguard.user.AuthCredentials;
import com.floragunn.searchguard.user.User;
public class AuthTokenAuthenticationBackend implements AuthenticationBackend {
private AuthTokenService authTokenService;
public AuthTokenAuthenticationBackend(AuthTokenService authTokenService) {
this.authTokenService = authTokenService;
}
@Override
public String getType() {
return "sg_auth_token";
}
@Override
public User authenticate(AuthCredentials credentials) throws ElasticsearchSecurityException {
try {
AuthToken authToken = authTokenService.getByClaims(credentials.getClaims());
return User.forUser(authToken.getUserName()).subName("AuthToken " + authToken.getTokenName() + " [" + authToken.getId() + "]")
.type(AuthTokenService.USER_TYPE).specialAuthzConfig(authToken.getId()).attributes(authToken.getBase().getAttributes())
.authzComplete().build();
} catch (NoSuchAuthTokenException | InvalidTokenException e) {
throw new ElasticsearchSecurityException(e.getMessage(), e);
}
}
@Override
public boolean exists(User user) {
// This is only related to impersonation. Auth tokens don't support impersonation.
return false;
}
@Override
public UserCachingPolicy userCachingPolicy() {
return UserCachingPolicy.NEVER;
}
}
/*
* Copyright 2020 by floragunn GmbH - All rights reserved
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed here is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* This software is free of charge for non-commercial and academic use.
* For commercial use in a production environment you have to obtain a license
* from https://floragunn.com
*
*/
package com.floragunn.searchguard.authtoken;
import java.security.AccessController;
import java.security.PrivilegedAction;
import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
import org.apache.cxf.rs.security.jose.jwt.JwtConstants;
import org.apache.cxf.rs.security.jose.jwt.JwtException;
import org.apache.cxf.rs.security.jose.jwt.JwtToken;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.SpecialPermission;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestChannel;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestStatus;
import com.floragunn.searchguard.auth.HTTPAuthenticator;
import com.floragunn.searchguard.user.AuthCredentials;
public class AuthTokenHttpJwtAuthenticator implements HTTPAuthenticator {
private final static Logger log = LogManager.getLogger(AuthTokenHttpJwtAuthenticator.class);
private final AuthTokenService authTokenService;
private final String jwtHeaderName;
private final String subjectKey;
public AuthTokenHttpJwtAuthenticator(AuthTokenService authTokenService) {
this.authTokenService = authTokenService;
this.jwtHeaderName = "Authorization";
this.subjectKey = JwtConstants.CLAIM_SUBJECT;
}
@Override
public String getType() {
return "sg_auth_token";
}
@Override
public AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws ElasticsearchSecurityException {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
return AccessController.doPrivileged((PrivilegedAction<AuthCredentials>) () -> extractCredentials0(request));
}
private AuthCredentials extractCredentials0(RestRequest request) throws ElasticsearchSecurityException {
String encodedJwt = getJwtTokenString(request);
if (Strings.isNullOrEmpty(encodedJwt)) {
return null;
}
try {
JwtToken jwt = authTokenService.getVerifiedJwtToken(encodedJwt);
JwtClaims claims = jwt.getClaims();
String subject = extractSubject(claims);
if (subject == null) {
log.error("No subject found in JWT token: " + claims);
return null;
}
return AuthCredentials.forUser(subject).claims(claims.asMap()).complete().build();
} catch (JwtException e) {
log.info("JWT is invalid", e);
return null;
}
}
protected String getJwtTokenString(RestRequest request) {
String authzHeader = request.header(jwtHeaderName);
if (authzHeader == null) {
return null;
}
authzHeader = authzHeader.trim();
int separatorIndex = authzHeader.indexOf(' ');
if (separatorIndex == -1) {
log.info("Illegal Authorization header: " + authzHeader);
return null;
}
String scheme = authzHeader.substring(0, separatorIndex);
if (!scheme.equalsIgnoreCase("bearer")) {
if (log.isDebugEnabled()) {
log.debug("Unsupported authentication scheme " + scheme);
}
return null;
}
return authzHeader.substring(separatorIndex + 1).trim();
}
protected String extractSubject(JwtClaims claims) {
String subject = claims.getSubject();
if (subjectKey != null) {
Object subjectObject = claims.getClaim(subjectKey);
if (subjectObject == null) {
log.warn("Failed to get subject from JWT claims, check if subject_key '{}' is correct.", subjectKey);
return null;
}
// We expect a String. If we find something else, convert to String but issue a
// warning
if (!(subjectObject instanceof String)) {
log.warn("Expected type String for roles in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.",
subjectKey, subjectObject, subjectObject.getClass());
subject = String.valueOf(subjectObject);
} else {
subject = (String) subjectObject;
}
}
return subject;
}
@Override
public boolean reRequestAuthentication(RestChannel channel, AuthCredentials authCredentials) {
final BytesRestResponse wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, "");
wwwAuthenticateResponse.addHeader("WWW-Authenticate", "Bearer realm=\"Search Guard\"");
channel.sendResponse(wwwAuthenticateResponse);
return true;
}
}
/*
* Copyright 2020 by floragunn GmbH - All rights reserved
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed here is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* This software is free of charge for non-commercial and academic use.
* For commercial use in a production environment you have to obtain a license
* from https://floragunn.com
*
*/
package com.floragunn.searchguard.authtoken;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Supplier;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.IndexScopedSettings;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.SettingsFilter;
import org.elasticsearch.plugins.ActionPlugin.ActionHandler;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestHandler;
import org.elasticsearch.script.ScriptService;