/*
* Copyright 2011 Sonian Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.sonian.elasticsearch.http.jetty.security;
import org.eclipse.jetty.security.*;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.security.Constraint;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import static org.elasticsearch.common.collect.Maps.newHashMap;
/*
* Handler to enforce SecurityConstraints. This implementation is
* based on org.eclipse.jetty.security.ConstraintSecurityHandler but it
* is using ElasticSearch path specification instead of servlet spec and
* fixes an issue that prevents multiple url specs to be used with
* non-empty http method.
* It precomputes the constraint combinations for runtime efficiency.
*/
public class RestConstraintSecurityHandler extends SecurityHandler implements ConstraintAware {
private final static String PATH_SPEC_SEPARATORS = ":,";
private final List<ConstraintMapping> constraintMappings = new CopyOnWriteArrayList<ConstraintMapping>();
private final Set<String> roles = new CopyOnWriteArraySet<String>();
private final Map<String, RestPathMap<RoleInfo>> constraintMap = newHashMap();
private RoleInfo defaultRoleInfo = null;
private boolean strict = true;
/**
* Get the strict mode.
*
* @return true if the security handler is running in strict mode.
*/
public boolean isStrict() {
return strict;
}
/**
* Set the strict mode of the security handler.
* <p/>
* When in strict mode (the default), the full servlet specification
* will be implemented.
* If not in strict mode, some additional flexibility in configuration
* is allowed:<ul>
* <li>All users do not need to have a role defined in the deployment descriptor
* <li>The * role in a constraint applies to ANY role rather than all roles defined in
* the deployment descriptor.
* </ul>
*
* @param strict the strict to set
* @see #setRoles(Set)
* @see #setConstraintMappings(List, Set)
*/
public void setStrict(boolean strict) {
this.strict = strict;
}
/**
* @return Returns the constraintMappings.
*/
public List<ConstraintMapping> getConstraintMappings() {
return constraintMappings;
}
public Set<String> getRoles() {
return roles;
}
/**
* Process the constraints following the combining rules in Servlet 3.0 EA
* spec section 13.7.1 Note that much of the logic is in the RoleInfo class.
*
* @param constraintMappings The constraintMappings to set, from which the set of known roles
* is determined.
*/
public void setConstraintMappings(List<ConstraintMapping> constraintMappings) {
setConstraintMappings(constraintMappings, null);
}
/**
* Process the constraints following the combining rules in Servlet 3.0 EA
* spec section 13.7.1 Note that much of the logic is in the RoleInfo class.
*
* @param constraintMappings The constraintMappings to set as array, from which the set of known roles
* is determined. Needed to retain API compatibility for 7.x
*/
public void setConstraintMappings(ConstraintMapping[] constraintMappings) {
setConstraintMappings(Arrays.asList(constraintMappings), null);
}
/**
* Process the constraints following the combining rules in Servlet 3.0 EA
* spec section 13.7.1 Note that much of the logic is in the RoleInfo class.
*
* @param constraintMappings The constraintMappings to set.
* @param roles The known roles (or null to determine them from the mappings)
*/
public void setConstraintMappings(List<ConstraintMapping> constraintMappings, Set<String> roles) {
if (isStarted())
throw new IllegalStateException("Started");
this.constraintMappings.clear();
this.constraintMappings.addAll(constraintMappings);
if (roles == null) {
roles = new HashSet<String>();
for (ConstraintMapping cm : constraintMappings) {
String[] cmr = cm.getConstraint().getRoles();
if (cmr != null) {
for (String r : cmr)
if (!"*".equals(r))
roles.add(r);
}
}
}
setRoles(roles);
}
/**
* Set the known roles.
* This may be overridden by a subsequent call to {@link #setConstraintMappings(ConstraintMapping[])} or
* {@link #setConstraintMappings(List, Set)}.
*
* @param roles The known roles (or null to determine them from the mappings)
* @see #setStrict(boolean)
*/
public void setRoles(Set<String> roles) {
if (isStarted())
throw new IllegalStateException("Started");
this.roles.clear();
this.roles.addAll(roles);
}
/**
* @see org.eclipse.jetty.security.ConstraintAware#addConstraintMapping(org.eclipse.jetty.security.ConstraintMapping)
*/
public void addConstraintMapping(ConstraintMapping mapping) {
constraintMappings.add(mapping);
if (mapping.getConstraint() != null && mapping.getConstraint().getRoles() != null)
for (String role : mapping.getConstraint().getRoles())
if (!"*".equals(role))
addRole(role);
if (isStarted()) {
processConstraintMapping(mapping);
}
}
/**
* @see org.eclipse.jetty.security.ConstraintAware#addRole(java.lang.String)
*/
public void addRole(String role) {
boolean modified = roles.add(role);
if (isStarted() && modified && strict) {
// Add the new role to currently defined any role role infos
for (Map<String, RoleInfo> map : constraintMap.values()) {
for (RoleInfo info : map.values()) {
if (info.isAnyRole())
info.addRole(role);
}
}
}
}
/**
* @see org.eclipse.jetty.security.SecurityHandler#doStart()
*/
@Override
protected void doStart() throws Exception {
constraintMap.clear();
if (constraintMappings != null) {
for (ConstraintMapping mapping : constraintMappings) {
processConstraintMapping(mapping);
}
}
super.doStart();
}
@Override
protected void doStop() throws Exception {
constraintMap.clear();
constraintMappings.clear();
roles.clear();
super.doStop();
}
private void addConstraint(RoleInfo roleInfo, Constraint constraint) {
if (roleInfo.isForbidden())
return;
boolean forbidden = constraint.isForbidden();
roleInfo.setForbidden(forbidden);
if (!forbidden) {
UserDataConstraint userDataConstraint = UserDataConstraint.get(constraint.getDataConstraint());
roleInfo.setUserDataConstraint(userDataConstraint);
boolean checked = constraint.getAuthenticate();
roleInfo.setChecked(checked);
if (roleInfo.isChecked()) {
if (constraint.isAnyRole()) {
if (strict) {
// * means "all defined roles"
for (String role : roles)
roleInfo.addRole(role);
} else
// * means any role
roleInfo.setAnyRole(true);
} else {
String[] newRoles = constraint.getRoles();
for (String role : newRoles) {
if (strict && !roles.contains(role))
throw new IllegalArgumentException("Attempt to use undeclared role: " + role + ", known roles: " + roles);
roleInfo.addRole(role);
}
}
}
}
}
protected void processConstraintMapping(ConstraintMapping mapping) {
String pathSpec = mapping.getPathSpec();
StringTokenizer tok = new StringTokenizer(pathSpec, PATH_SPEC_SEPARATORS);
String httpMethod = mapping.getMethod();
while (tok.hasMoreTokens()) {
String spec = tok.nextToken().trim();
if (httpMethod == null) {
if ("*".equals(spec)) {
if (defaultRoleInfo == null) {
defaultRoleInfo = new RoleInfo();
}
addConstraint(defaultRoleInfo, mapping.getConstraint());
} else {
throw new IllegalArgumentException("No method specified for PathSpec " + pathSpec + ".");
}
}
RestPathMap<RoleInfo> mappings = constraintMap.get(httpMethod);
if (mappings == null) {
mappings = new RestPathMap<RoleInfo>();
constraintMap.put(httpMethod, mappings);
}
RoleInfo roleInfo = mappings.get(spec);
if (roleInfo == null) {
roleInfo = new RoleInfo();
mappings.put(spec, roleInfo);
}
addConstraint(roleInfo, mapping.getConstraint());
}
}
protected Object prepareConstraintInfo(String pathInContext, Request request) {
String httpMethod = request.getMethod();
RestPathMap<RoleInfo> mappings = constraintMap.get(httpMethod);
if (mappings != null) {
RoleInfo roleInfo = mappings.match(pathInContext);
if (roleInfo != null) {
return roleInfo;
}
}
return defaultRoleInfo;
}
protected boolean checkUserDataPermissions(String pathInContext, Request request, Response response, Object constraintInfo) throws IOException {
if (constraintInfo == null)
return true;
RoleInfo roleInfo = (RoleInfo) constraintInfo;
if (roleInfo.isForbidden())
return false;
UserDataConstraint dataConstraint = roleInfo.getUserDataConstraint();
if (dataConstraint == null || dataConstraint == UserDataConstraint.None) {
return true;
}
AbstractHttpConnection connection = AbstractHttpConnection.getCurrentConnection();
Connector connector = connection.getConnector();
if (dataConstraint == UserDataConstraint.Integral) {
if (connector.isIntegral(request))
return true;
if (connector.getIntegralPort() > 0) {
String url = connector.getIntegralScheme() + "://" + request.getServerName() + ":" + connector.getIntegralPort() + request.getRequestURI();
if (request.getQueryString() != null)
url += "?" + request.getQueryString();
response.setContentLength(0);
response.sendRedirect(url);
} else
response.sendError(Response.SC_FORBIDDEN, "!Integral");
request.setHandled(true);
return false;
} else if (dataConstraint == UserDataConstraint.Confidential) {
if (connector.isConfidential(request))
return true;
if (connector.getConfidentialPort() > 0) {
String url = connector.getConfidentialScheme() + "://" + request.getServerName() + ":" + connector.getConfidentialPort()
+ request.getRequestURI();
if (request.getQueryString() != null)
url += "?" + request.getQueryString();
response.setContentLength(0);
response.sendRedirect(url);
} else
response.sendError(Response.SC_FORBIDDEN, "!Confidential");
request.setHandled(true);
return false;
} else {
throw new IllegalArgumentException("Invalid dataConstraint value: " + dataConstraint);
}
}
protected boolean isAuthMandatory(Request baseRequest, Response base_response, Object constraintInfo) {
if (constraintInfo == null) {
return false;
}
return ((RoleInfo) constraintInfo).isChecked();
}
@Override
protected boolean checkWebResourcePermissions(String pathInContext, Request request, Response response, Object constraintInfo, UserIdentity userIdentity)
throws IOException {
if (constraintInfo == null) {
return true;
}
RoleInfo roleInfo = (RoleInfo) constraintInfo;
if (!roleInfo.isChecked()) {
return true;
}
if (roleInfo.isAnyRole() && request.getAuthType() != null)
return true;
for (String role : roleInfo.getRoles()) {
if (userIdentity.isUserInRole(role, null))
return true;
}
return false;
}
@Override
public void dump(Appendable out, String indent) throws IOException {
dumpThis(out);
dump(out, indent,
Collections.singleton(getLoginService()),
Collections.singleton(getIdentityService()),
Collections.singleton(getAuthenticator()),
Collections.singleton(roles),
constraintMap.entrySet(),
getBeans(),
TypeUtil.asList(getHandlers()));
}
}