package org.jgroups.protocols; import org.jgroups.*; import org.jgroups.annotations.MBean; import org.jgroups.annotations.Property; import org.jgroups.annotations.XmlAttribute; import org.jgroups.auth.AuthToken; import org.jgroups.auth.X509Token; import org.jgroups.conf.ClassConfigurator; import org.jgroups.protocols.pbcast.GMS; import org.jgroups.protocols.pbcast.JoinRsp; import org.jgroups.stack.Protocol; import org.jgroups.util.MessageBatch; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; /** * The AUTH protocol adds a layer of authentication to JGroups. It intercepts join and merge requests and rejects them * if the joiner or merger is not permitted to join a or merge into a cluster. AUTH should be placed right below * {@link GMS} in the configuration. * @author Chris Mills * @author Bela Ban */ @XmlAttribute(attrs={ "auth_value", // SimpleToken, MD5Token, X509Token "fixed_members_value", "fixed_members_seperator", // FixedMembershipToken "block_time", "challenge_size", // ChallengeResponseToken "client_principal_name", "client_password", "service_principal_name", // Krb5Token "token_hash", // MD5Token "match_string", "match_ip_address", "match_logical_name", // RegexMembership "keystore_type", "cert_alias", "keystore_path", "cipher_type", "cert_password", "keystore_password" // X509Token }) @MBean(description="Provides authentication of joiners, to prevent un-authorized joining of a cluster") public class AUTH extends Protocol { /** Interface to provide callbacks for handling up events */ public interface UpHandler { /** * Called when a message has been received * @param msg the message * @return true if the message should be passed up, else false */ boolean handleUpMessage(Message msg); } /** Used on the coordinator to authentication joining member requests against */ protected AuthToken auth_token; protected static final short GMS_ID=ClassConfigurator.getProtocolId(GMS.class); /** List of UpHandler which are called when an up event has been received. Usually used by AuthToken impls */ protected final List<UpHandler> up_handlers=new ArrayList<>(); protected Address local_addr; public AUTH() {} protected volatile boolean authenticate_coord=true; @Property(description="Do join or merge responses from the coordinator also need to be authenticated") public AUTH setAuthCoord( boolean authenticateCoord) { this.authenticate_coord= authenticateCoord; return this; } @Property(name="auth_class",description="The fully qualified name of the class implementing the AuthToken interface") public void setAuthClass(String class_name) throws Exception { Object obj=Class.forName(class_name).newInstance(); auth_token=(AuthToken)obj; auth_token.setAuth(this); } public String getAuthClass() {return auth_token != null? auth_token.getClass().getName() : null;} public AuthToken getAuthToken() {return auth_token;} public AUTH setAuthToken(AuthToken token) {this.auth_token=token; return this;} public AUTH register(UpHandler handler) {up_handlers.add(handler); return this;} public AUTH unregister(UpHandler handler) {up_handlers.remove(handler);return this;} public Address getAddress() {return local_addr;} public PhysicalAddress getPhysicalAddress() {return getTransport().getPhysicalAddress();} protected List<Object> getConfigurableObjects() { List<Object> retval=new LinkedList<>(); if(auth_token != null) retval.add(auth_token); return retval; } public void init() throws Exception { super.init(); if(auth_token == null) throw new IllegalStateException("no authentication mechanism configured"); if(auth_token instanceof X509Token) { X509Token tmp=(X509Token)auth_token; tmp.setCertificate(); } auth_token.init(); } public void start() throws Exception { super.start(); if(auth_token != null) auth_token.start(); } public void stop() { if(auth_token != null) auth_token.stop(); super.stop(); } public void destroy() { if(auth_token != null) auth_token.destroy(); super.destroy(); } /** * An event was received from the layer below. Usually the current layer will want to examine the event type and * - depending on its type - perform some computation (e.g. removing headers from a MSG event type, or updating * the internal membership list when receiving a VIEW_CHANGE event). * Finally the event is either a) discarded, or b) an event is sent down the stack using {@code down_prot.down()} * or c) the event (or another event) is sent up the stack using {@code up_prot.up()}. */ public Object up(Message msg) { // If we have a join or merge request --> authenticate, else pass up GMS.GmsHeader gms_hdr=getGMSHeader(msg); if(gms_hdr != null && needsAuthentication(gms_hdr)) { AuthHeader auth_hdr=msg.getHeader(id); if(auth_hdr == null) throw new IllegalStateException(String.format("found %s from %s but no AUTH header", gms_hdr, msg.src())); if(!handleAuthHeader(gms_hdr, auth_hdr, msg)) // authentication failed return null; // don't pass up } if(!callUpHandlers(msg)) return null; return up_prot.up(msg); } public void up(MessageBatch batch) { for(Message msg: batch) { // If we have a join or merge request --> authenticate, else pass up GMS.GmsHeader gms_hdr=getGMSHeader(msg); if(gms_hdr != null && needsAuthentication(gms_hdr)) { AuthHeader auth_hdr=msg.getHeader(id); if(auth_hdr == null) { log.warn("%s: found GMS join or merge request from %s but no AUTH header", local_addr, batch.sender()); sendRejectionMessage(gms_hdr.getType(), batch.sender(), "join or merge without an AUTH header"); batch.remove(msg); } else if(!handleAuthHeader(gms_hdr, auth_hdr, msg)) // authentication failed batch.remove(msg); // don't pass up } } if(!batch.isEmpty()) up_prot.up(batch); } /** * An event is to be sent down the stack. The layer may want to examine its type and perform * some action on it, depending on the event's type. If the event is a message MSG, then * the layer may need to add a header to it (or do nothing at all) before sending it down * the stack using {@code down_prot.down()}. In case of a GET_ADDRESS event (which tries to * retrieve the stack's address from one of the bottom layers), the layer may need to send * a new response event back up the stack using {@code up_prot.up()}. */ public Object down(Event evt) { if(evt.getType() == Event.SET_LOCAL_ADDRESS) local_addr=evt.getArg(); return down_prot.down(evt); } public Object down(Message msg) { GMS.GmsHeader hdr = getGMSHeader(msg); if(hdr != null && needsAuthentication(hdr)) { // we found a join request message - now add an AUTH Header msg.putHeader(this.id, new AuthHeader(this.auth_token)); } return down_prot.down(msg); } protected boolean needsAuthentication(GMS.GmsHeader hdr) { switch(hdr.getType()) { case GMS.GmsHeader.JOIN_REQ: case GMS.GmsHeader.JOIN_REQ_WITH_STATE_TRANSFER: case GMS.GmsHeader.MERGE_REQ: return true; case GMS.GmsHeader.JOIN_RSP: case GMS.GmsHeader.MERGE_RSP: case GMS.GmsHeader.INSTALL_MERGE_VIEW: return this.authenticate_coord; default: return false; } } /** * Handles a GMS header * @param gms_hdr * @param msg * @return true if the message should be passed up, or else false */ protected boolean handleAuthHeader(GMS.GmsHeader gms_hdr, AuthHeader auth_hdr, Message msg) { if(needsAuthentication(gms_hdr)) { if(this.auth_token.authenticate(auth_hdr.getToken(), msg)) return true; // authentication passed, send message up the stack else { log.warn("%s: failed to validate AuthHeader (token: %s) from %s; dropping message", local_addr, auth_token.getClass().getSimpleName(), msg.src()); sendRejectionMessage(gms_hdr.getType(), msg.getSrc(), "authentication failed"); return false; } } return true; } protected void sendRejectionMessage(byte type, Address dest, String error_msg) { switch(type) { case GMS.GmsHeader.JOIN_REQ: case GMS.GmsHeader.JOIN_REQ_WITH_STATE_TRANSFER: sendJoinRejectionMessage(dest, error_msg); break; case GMS.GmsHeader.MERGE_REQ: sendMergeRejectionMessage(dest); break; } } protected void sendJoinRejectionMessage(Address dest, String error_msg) { if(dest == null) return; JoinRsp joinRes=new JoinRsp(error_msg); // specify the error message on the JoinRsp Message msg = new Message(dest).putHeader(GMS_ID, new GMS.GmsHeader(GMS.GmsHeader.JOIN_RSP)) .setBuffer(GMS.marshal(joinRes)); if(this.authenticate_coord) msg.putHeader(this.id, new AuthHeader(this.auth_token)); down_prot.down(msg); } protected void sendMergeRejectionMessage(Address dest) { GMS.GmsHeader hdr=new GMS.GmsHeader(GMS.GmsHeader.MERGE_RSP).setMergeRejected(true); Message msg=new Message(dest).setFlag(Message.Flag.OOB).putHeader(GMS_ID, hdr); if(this.authenticate_coord) msg.putHeader(this.id, new AuthHeader(this.auth_token)); log.debug("merge response=%s", hdr); down_prot.down(msg); } protected boolean callUpHandlers(Message msg) { boolean pass_up=true; for(UpHandler handler: up_handlers) { if(!handler.handleUpMessage(msg)) pass_up=false; } return pass_up; } protected static GMS.GmsHeader getGMSHeader(Message msg) { Header hdr = msg.getHeader(GMS_ID); if(hdr instanceof GMS.GmsHeader) return (GMS.GmsHeader)hdr; return null; } }