+ added email and user name resolving capabilities

+ added Messages.properties for i18n
# plugin renamed to script-realm-modular
# minor doc fixes
This commit is contained in:
nicobo 2015-04-26 02:17:53 +02:00
parent de44af4086
commit 4dfbd174de
9 changed files with 182 additions and 9 deletions

19
pom.xml
View file

@ -6,11 +6,11 @@
<version>1.566</version>
</parent>
<artifactId>script-realm</artifactId>
<artifactId>script-realm-modular</artifactId>
<version>1.6-SNAPSHOT</version>
<packaging>hpi</packaging>
<name>Security Realm by custom script</name>
<description>Supports authentication by executing a custom script, to resolve groups for a user, a second script can be defined.</description>
<name>Security Realm by custom script using modular resolvers</name>
<description>Supports authentication by custom script, groups resolving for a user, and triggers e-mail and name resolvers.</description>
<url>https://wiki.jenkins-ci.org/display/JENKINS/Script+Security+Realm</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@ -33,12 +33,25 @@
</plugin>
</plugins>
</build>
<dependencies>
<!-- Required for e-mail resolving (MailAddressResolver,...) -->
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>mailer</artifactId>
<version>1.8</version>
</dependency>
</dependencies>
<developers>
<developer>
<id>imod</id>
<name>Dominik Bartholdi</name>
<email>-</email>
</developer>
<developer>
<id>nicobo</id>
<name>Nicolas BONARDELLE</name>
<url>http://nicolabs.xyz/people/nicobo</url>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/jenkins/script-realm-plugin.git</connection>

View file

@ -24,13 +24,19 @@
package hudson.plugins.script_realm;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.Launcher.LocalLauncher;
import hudson.model.Descriptor;
import hudson.security.AbstractPasswordBasedSecurityRealm;
import hudson.security.GroupDetails;
import hudson.security.SecurityRealm;
import hudson.tasks.MailAddressResolver;
import hudson.tasks.UserNameResolver;
import hudson.tasks.Mailer;
import hudson.util.QuotedStringTokenizer;
import hudson.util.StreamTaskListener;
import hudson.util.ListBoxModel;
import hudson.util.ListBoxModel.Option;
import java.io.IOException;
import java.io.OutputStream;
@ -59,16 +65,28 @@ import org.springframework.dao.DataAccessException;
/**
* @author Kohsuke Kawaguchi
* @author nicobo
*/
public class ScriptSecurityRealm extends AbstractPasswordBasedSecurityRealm {
private static final Logger LOGGER = Logger.getLogger(ScriptSecurityRealm.class.getName());
/** Strategy : call the global resolve method (let Jenkins choose) */
private static final String OPTION_RESOLVER_DEFAULTSTRATEGY = "*";
/** Strategy : don't resolve */
private static final String OPTION_RESOLVER_NONESTRATEGY = "";
public final String commandLine;
public final String groupsCommandLine;
public final String groupsDelimiter;
/** The name of the e-mail resolver to use */
public final String emailResolver;
/** The name of the full name resolver to user */
public final String nameResolver;
@DataBoundConstructor
public ScriptSecurityRealm(String commandLine, String groupsCommandLine, String groupsDelimiter) {
public ScriptSecurityRealm(String commandLine, String groupsCommandLine, String groupsDelimiter, String emailResolver, String nameResolver) {
this.commandLine = commandLine;
this.groupsCommandLine = groupsCommandLine;
if (StringUtils.isBlank(groupsDelimiter)) {
@ -76,9 +94,13 @@ public class ScriptSecurityRealm extends AbstractPasswordBasedSecurityRealm {
} else {
this.groupsDelimiter = groupsDelimiter;
}
this.emailResolver = emailResolver;
this.nameResolver = nameResolver;
LOGGER.log(Level.FINE, "Configured with : commandLine=[{0}] groupsCommandLine=[{1}] groupsDelimiter=[{2}] emailResolver=[{3}] nameResolver=[{4}]", new Object[]{commandLine,groupsCommandLine,groupsDelimiter,emailResolver,nameResolver});
}
protected UserDetails authenticate(String username, String password) throws AuthenticationException {
LOGGER.entering(ScriptSecurityRealm.class.getName(), "authenticate", new Object[]{username,password});
try {
StringWriter out = new StringWriter();
LocalLauncher launcher = new LoginScriptLauncher(new StreamTaskListener(out));
@ -88,11 +110,17 @@ public class ScriptSecurityRealm extends AbstractPasswordBasedSecurityRealm {
if (isWindows()) {
overrides.put("SystemRoot", System.getenv("SystemRoot"));
}
LOGGER.log(Level.FINE,"Executing command with U=[{0}], P=[{1}]", new Object[]{username,password});
if (launcher.launch().cmds(QuotedStringTokenizer.tokenize(commandLine)).stdout(new NullOutputStream()).envs(overrides).join() != 0) {
throw new BadCredentialsException(out.toString());
}
GrantedAuthority[] groups = loadGroups(username);
return new User(username, "", true, true, true, true, groups);
User user = new User(username, "", true, true, true, true, groups);
updateUserDetails(username);
return user;
} catch (InterruptedException e) {
throw new AuthenticationServiceException("Interrupted", e);
} catch (IOException e) {
@ -116,12 +144,52 @@ public class ScriptSecurityRealm extends AbstractPasswordBasedSecurityRealm {
@Extension
public static final class DescriptorImpl extends Descriptor<SecurityRealm> {
public String getDisplayName() {
return "Authenticate via custom script";
}
public String getDefaultEmailResolver() {
return OPTION_RESOLVER_DEFAULTSTRATEGY;
}
public String getDefaultNameResolver() {
return OPTION_RESOLVER_DEFAULTSTRATEGY;
}
public ListBoxModel doFillEmailResolverItems() {
ListBoxModel items = new ListBoxModel();
ExtensionList<MailAddressResolver> mars = MailAddressResolver.all();
items.add(new Option(Messages.ScriptSecurityRealm_EmailResolver_defaultstrategy_label(),OPTION_RESOLVER_DEFAULTSTRATEGY)); // This entry will use Jenkins's default behavior (calling all found resolvers)
if ( ! mars.isEmpty() ) {
items.add(new Option(Messages.ScriptSecurityRealm_EmailResolver_nonestrategy_label(),OPTION_RESOLVER_NONESTRATEGY)); // This entry will disable resolving if selected
// Adds all found e-mail resolvers as options so the user can select one of them
for (MailAddressResolver mar : mars) {
// class name is used both as label and value
items.add(mar.getClass().getCanonicalName(),mar.getClass().getName());
}
}
return items;
}
public ListBoxModel doFillNameResolverItems() {
ListBoxModel items = new ListBoxModel();
ExtensionList<UserNameResolver> unrs = UserNameResolver.all();
items.add(new Option(Messages.ScriptSecurityRealm_NameResolver_defaultstrategy_label(),OPTION_RESOLVER_DEFAULTSTRATEGY)); // This entry will use Jenkins's default behavior (calling all found resolvers)
if ( ! unrs.isEmpty() ) {
items.add(new Option(Messages.ScriptSecurityRealm_NameResolver_nonestrategy_label(),OPTION_RESOLVER_NONESTRATEGY)); // This entry will disable resolving if selected
// Adds all found name resolvers as options so the user can select one of them
for (UserNameResolver unr : unrs) {
// class name is used both as label and value
items.add(unr.getClass().getCanonicalName(),unr.getClass().getName());
}
}
return items;
}
}
protected GrantedAuthority[] loadGroups(String username) throws AuthenticationException {
LOGGER.log(Level.FINE,"Loading groups from command for {0}", new Object[]{username});
try {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(AUTHENTICATED_AUTHORITY);
@ -155,6 +223,74 @@ public class ScriptSecurityRealm extends AbstractPasswordBasedSecurityRealm {
}
}
/**
* Updates the display name and e-mail address of the user by calling the chosen resolvers.
* Most of the code comes from {@link hudson.security.LDAPSecurityRealm}.
*/
private void updateUserDetails(String username) {
hudson.model.User user = hudson.model.User.get(username);
if ( !OPTION_RESOLVER_NONESTRATEGY.equals(nameResolver) ) {
String fullname = null;
if ( OPTION_RESOLVER_DEFAULTSTRATEGY.equals(nameResolver) ) {
LOGGER.log(Level.FINE,"Calling any registered UserNameResolver for {0}",new Object[]{user});
fullname = UserNameResolver.resolve(user);
} else {
for (UserNameResolver unr : UserNameResolver.all()) {
if ( unr.getClass().getName().equals(nameResolver) ) {
LOGGER.log(Level.FINE,"Calling resolver {0} for {1}",new Object[]{nameResolver,user});
fullname = unr.findNameFor(user);
break;
}
LOGGER.log(Level.WARNING,"Resolver {0} not found : name not updated",new Object[]{nameResolver});
}
}
if ( StringUtils.isNotBlank(fullname) ) {
LOGGER.log(Level.FINE,"Setting user's name to {0}",new Object[]{fullname});
user.setFullName(fullname);
} else {
LOGGER.log(Level.FINE,"Null or empty user name : not updating it");
}
} else {
LOGGER.log(Level.FINE,"None strategy : not updating the user's name");
}
if ( !OPTION_RESOLVER_NONESTRATEGY.equals(emailResolver) ) {
Mailer.UserProperty existing = user.getProperty(Mailer.UserProperty.class);
if (existing==null || !existing.hasExplicitlyConfiguredAddress()) {
String email = null;
if ( OPTION_RESOLVER_DEFAULTSTRATEGY.equals(emailResolver) ) {
LOGGER.log(Level.FINE,"Calling any registered MailAddressResolver for {0}",new Object[]{user});
email = MailAddressResolver.resolve(user);
} else {
for (MailAddressResolver mar : MailAddressResolver.all()) {
if ( mar.getClass().getName().equals(emailResolver) ) {
LOGGER.log(Level.FINE,"Calling resolver {0} for {1}",new Object[]{emailResolver,user});
email = mar.findMailAddressFor(user);
break;
}
LOGGER.log(Level.WARNING,"Resolver {0} not found : e-mail not updated",new Object[]{emailResolver});
}
}
if ( StringUtils.isNotBlank(email) ) {
try {
LOGGER.log(Level.FINE,"Setting e-mail to {0}",new Object[]{email});
user.addProperty(new Mailer.UserProperty(email));
} catch (IOException e){
LOGGER.throwing(ScriptSecurityRealm.class.getCanonicalName(), "updateUserDetails", e);
}
} else {
LOGGER.log(Level.FINE,"Null or empty e-mail : not updating it");
}
} else {
LOGGER.log(Level.FINE,"An e-mail has already been set up by the user : not updating it");
}
} else {
LOGGER.log(Level.FINE,"None strategy : not updating the e-mail");
}
}
public boolean isWindows() {
String os = System.getProperty("os.name").toLowerCase();
return os.contains("win");

View file

@ -0,0 +1,5 @@
# Labels for the strategies in the drop-down list
ScriptSecurityRealm.EmailResolver.defaultstrategy.label=Default (try all existing resolvers)
ScriptSecurityRealm.EmailResolver.nonestrategy.label=None (do not resolve)
ScriptSecurityRealm.NameResolver.defaultstrategy.label=Default (try all existing resolvers)
ScriptSecurityRealm.NameResolver.nonestrategy.label=None (do not resolve)

View file

@ -31,4 +31,10 @@ THE SOFTWARE.
<f:entry title="${%Groups Delimiter}" help="/plugin/script-realm/help-groupsDelimiter.html">
<f:textbox field="groupsDelimiter" default="," />
</f:entry>
<f:entry field="emailResolver" title="${%Email resolver}" help="/plugin/script-realm/help-emailResolver.html">
<f:select default="${descriptor.defaultEmailResolver()}" />
</f:entry>
<f:entry field="nameResolver" title="${%Name resolver}" help="/plugin/script-realm/help-nameResolver.html">
<f:select default="${descriptor.defaultNameResolver()}" />
</f:entry>
</j:jelly>

View file

@ -3,11 +3,13 @@
a custom authentication scheme, but don't want to write your own plugin.
<p>
Each time the authentication is attemped (which is once per session),
Each time the authentication is attempted (which is once per session),
the specified script will be invoked with the username in the 'U' environment variable
and the password in the 'P' environment variable. If the script returns exit code 0,
the authentication is considered successful, and otherwise failure.
</p>
<p>
In case of the failure, the output from the process will be reported in the exception message.
</p>
</div>

View file

@ -1,3 +1,4 @@
<div>
This plugin adds authentication via user-defined script, in additon to the orignal script-realm, this one also supports groups.
This plugin adds authentication and groups resolving via user-defined script.
In addition to the original script-realm, this one also supports e-mail and name resolving through the use of existing plugins.
</div>

View file

@ -0,0 +1,5 @@
<div>
Choose how to resolve the e-mail of the authenticated user (you may want to install plugins to increase the available options).<br />
If none, no e-mail resolution will be attempted (it will stay as is).<br />
Be aware that not all options may work, as they may come from a plugin that follows its own configuration strategy.<br />
</div>

View file

@ -0,0 +1,5 @@
<div>
Choose how to resolve the full name of the authenticated user (you may want to install plugins to increase the available options).<br />
If none, no name resolution will be attempted (it will stay as is).<br />
Be aware that not all options may work, as they may come from a plugin that follows its own configuration strategy.<br />
</div>

View file

@ -28,7 +28,7 @@ public class ScriptSecurityRealmTest extends HudsonTestCase {
}
public void test1() {
UserDetails user = new ScriptSecurityRealm(trueScript.getAbsolutePath(), null, null).authenticate("test", "test");
UserDetails user = new ScriptSecurityRealm(trueScript.getAbsolutePath(), null, null, null, null).authenticate("test", "test");
System.out.println("**-->" + user);
assertTrue("user account not enabled", user.isEnabled());
assertTrue("user credentials expired", user.isCredentialsNonExpired());
@ -38,7 +38,7 @@ public class ScriptSecurityRealmTest extends HudsonTestCase {
public void test2() {
try {
new ScriptSecurityRealm(falseScript.getAbsolutePath(), null, null).authenticate("test", "test");
new ScriptSecurityRealm(falseScript.getAbsolutePath(), null, null, null, null).authenticate("test", "test");
fail();
} catch (AuthenticationException e) {
// as expected