Skip to content
Open
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 @@ -65,7 +65,7 @@ public void addConnection(ConnectionContext context, ConnectionInfo info) throws
Thread.currentThread().setContextClassLoader(JaasAuthenticationBroker.class.getClassLoader());
SecurityContext securityContext = null;
try {
securityContext = authenticate(info.getUserName(), info.getPassword(), null);
securityContext = authenticate(info.getUserName(), info.getPassword(), null, info.getClientId());
context.setSecurityContext(securityContext);
securityContexts.add(securityContext);
super.addConnection(context, info);
Expand All @@ -85,8 +85,12 @@ public void addConnection(ConnectionContext context, ConnectionInfo info) throws

@Override
public SecurityContext authenticate(String username, String password, X509Certificate[] certificates) throws SecurityException {
return authenticate(username, password, certificates, null);
}

public SecurityContext authenticate(String username, String password, X509Certificate[] certificates, String clientId) throws SecurityException {
SecurityContext result = null;
JassCredentialCallbackHandler callback = new JassCredentialCallbackHandler(username, password);
JassCredentialCallbackHandler callback = new JassCredentialCallbackHandler(username, password, clientId);
try {
LoginContext lc = new LoginContext(jassConfiguration, callback);
lc.login();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.activemq.jaas;

import javax.security.auth.callback.Callback;

/**
* Callback used to pass the connection's requested clientId to a login module
* so it can authorize the clientId in addition to the user credentials.
*/
public class ClientIdCallback implements Callback {

private String clientId;

public String getClientId() {
return clientId;
}

public void setClientId(String clientId) {
this.clientId = clientId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.activemq.jaas;

import java.security.Principal;

/**
* Principal representing the clientId a connection was authenticated to use.
* Added to the Subject alongside the {@link UserPrincipal} when clientId
* authentication is enabled on the login module.
*/
public class ClientIdPrincipal implements Principal {

private final String name;
private transient int hash;

public ClientIdPrincipal(String name) {
if (name == null) {
throw new IllegalArgumentException("name cannot be null");
}
this.name = name;
}

@Override
public String getName() {
return name;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}

final ClientIdPrincipal that = (ClientIdPrincipal)o;

if (!name.equals(that.name)) {
return false;
}

return true;
}

@Override
public int hashCode() {
if (hash == 0) {
hash = name.hashCode();
}
return hash;
}

@Override
public String toString() {
return name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,16 @@ public class JassCredentialCallbackHandler implements CallbackHandler {

private final String username;
private final String password;
private final String clientId;

public JassCredentialCallbackHandler(String username, String password) {
this(username, password, null);
}

public JassCredentialCallbackHandler(String username, String password, String clientId) {
this.username = username;
this.password = password;
this.clientId = clientId;
}

@Override
Expand All @@ -55,6 +61,8 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback
} else {
nameCallback.setName(username);
}
} else if (callback instanceof ClientIdCallback) {
((ClientIdCallback)callback).setClientId(clientId);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@

import java.io.IOException;
import java.security.Principal;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;

import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
Expand All @@ -40,6 +41,10 @@ public class PropertiesLoginModule extends PropertiesLoader implements LoginModu

private static final String USER_FILE_PROP_NAME = "org.apache.activemq.jaas.properties.user";
private static final String GROUP_FILE_PROP_NAME = "org.apache.activemq.jaas.properties.group";
private static final String CLIENTID_FILE_PROP_NAME = "org.apache.activemq.jaas.properties.clientid";

/** matches the authenticated user name when expanded in a clientId pattern */
private static final String USER_TOKEN = "${userId}";

private static final Logger LOG = LoggerFactory.getLogger(PropertiesLoginModule.class);

Expand All @@ -48,8 +53,15 @@ public class PropertiesLoginModule extends PropertiesLoader implements LoginModu

private Properties users;
private Map<String,Set<String>> groups;
// Optional: userId -> comma-separated clientId patterns. Null when clientId
// authentication is not configured (the CLIENTID_FILE_PROP_NAME option is absent).
private Properties clientIds;
private String user;
private final Set<Principal> principals = new HashSet<Principal>();
private String clientId;
// LinkedHashSet so principal insertion order is preserved when copied into the
// Subject: UserPrincipal is always added first, then ClientIdPrincipal (when
// clientId authentication is enabled), then group principals.
private final Set<Principal> principals = new LinkedHashSet<Principal>();

/** the authentication status*/
private boolean succeeded = false;
Expand All @@ -63,6 +75,10 @@ public void initialize(Subject subject, CallbackHandler callbackHandler, Map sha
init(options);
users = load(USER_FILE_PROP_NAME, "user", options).getProps();
groups = load(GROUP_FILE_PROP_NAME, "group", options).invertedPropertiesValuesMap();
// clientId authentication is opt-in: only enabled when the file option is present
if (options.containsKey(CLIENTID_FILE_PROP_NAME)) {
clientIds = load(CLIENTID_FILE_PROP_NAME, "clientids", options).getProps();
}
}

@Override
Expand Down Expand Up @@ -94,6 +110,20 @@ public boolean login() throws LoginException {
if (!password.equals(new String(tmpPassword))) {
throw new FailedLoginException("Password does not match");
}

// When enabled, also authenticate the connection's clientId. A connection
// that presents a clientId it is not permitted to use fails to log in. A
// connection with no clientId is allowed (it cannot own durable subscriptions).
if (clientIds != null) {
String requestedClientId = getClientId();
if (requestedClientId != null && !requestedClientId.isEmpty()) {
if (!isClientIdAllowed(user, requestedClientId)) {
throw new FailedLoginException("clientId is not allowed for user");
}
clientId = requestedClientId;
}
}

succeeded = true;

if (debug) {
Expand All @@ -112,8 +142,14 @@ public boolean commit() throws LoginException {
return false;
}

// UserPrincipal is always added first; ClientIdPrincipal (when a clientId was
// authenticated) is added second, ahead of any group principals.
principals.add(new UserPrincipal(user));

if (clientId != null) {
principals.add(new ClientIdPrincipal(clientId));
}

Set<String> matchedGroups = groups.get(user);
if (matchedGroups != null) {
for (String entry : matchedGroups) {
Expand Down Expand Up @@ -164,7 +200,55 @@ public boolean logout() throws LoginException {

private void clear() {
user = null;
clientId = null;
principals.clear();
}

private String getClientId() throws LoginException {
ClientIdCallback clientIdCallback = new ClientIdCallback();
try {
callbackHandler.handle(new Callback[] {clientIdCallback});
} catch (IOException ioe) {
throw new LoginException(ioe.getMessage());
} catch (UnsupportedCallbackException uce) {
// callback handler does not supply a clientId; treat as none
return null;
}
return clientIdCallback.getClientId();
}

private boolean isClientIdAllowed(String userId, String clientId) {
String patterns = clientIds.getProperty(userId);
if (patterns == null) {
// fall back to the generic per-user rule, e.g. ${userId} = ${userId}-*
patterns = clientIds.getProperty(USER_TOKEN);
}
if (patterns == null) {
return false;
}
for (String pattern : patterns.split(",")) {
pattern = pattern.trim();
if (pattern.isEmpty()) {
continue;
}
if (matches(pattern.replace(USER_TOKEN, userId), clientId)) {
return true;
}
}
return false;
}

private static boolean matches(String pattern, String clientId) {
// '*' is a multi-character wildcard; all other characters match literally.
StringBuilder regex = new StringBuilder();
String[] segments = pattern.split("\\*", -1);
for (int i = 0; i < segments.length; i++) {
if (i > 0) {
regex.append(".*");
}
regex.append(Pattern.quote(segments[i]));
}
return clientId.matches(regex.toString());
}

}
Loading
Loading