diff --git a/pom.xml b/pom.xml index 4444639..7243a86 100644 --- a/pom.xml +++ b/pom.xml @@ -6,11 +6,11 @@ 1.566 - script-realm + script-realm-modular 1.6-SNAPSHOT hpi - Security Realm by custom script - Supports authentication by executing a custom script, to resolve groups for a user, a second script can be defined. + Security Realm by custom script using modular resolvers + Supports authentication by custom script, groups resolving for a user, and triggers e-mail and name resolvers. https://wiki.jenkins-ci.org/display/JENKINS/Script+Security+Realm UTF-8 @@ -33,12 +33,25 @@ + + + + org.jenkins-ci.plugins + mailer + 1.8 + + imod Dominik Bartholdi - + + nicobo + Nicolas BONARDELLE + http://nicolabs.xyz/people/nicobo + scm:git:git://github.com/jenkins/script-realm-plugin.git diff --git a/src/main/java/hudson/plugins/script_realm/ScriptSecurityRealm.java b/src/main/java/hudson/plugins/script_realm/ScriptSecurityRealm.java index 76084a7..9a70544 100644 --- a/src/main/java/hudson/plugins/script_realm/ScriptSecurityRealm.java +++ b/src/main/java/hudson/plugins/script_realm/ScriptSecurityRealm.java @@ -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 { + 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 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 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 authorities = new ArrayList(); 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"); diff --git a/src/main/resources/hudson/plugins/script_realm/Messages.properties b/src/main/resources/hudson/plugins/script_realm/Messages.properties new file mode 100644 index 0000000..ab26f0c --- /dev/null +++ b/src/main/resources/hudson/plugins/script_realm/Messages.properties @@ -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) \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/script_realm/ScriptSecurityRealm/config.jelly b/src/main/resources/hudson/plugins/script_realm/ScriptSecurityRealm/config.jelly index fd6550d..4baf225 100644 --- a/src/main/resources/hudson/plugins/script_realm/ScriptSecurityRealm/config.jelly +++ b/src/main/resources/hudson/plugins/script_realm/ScriptSecurityRealm/config.jelly @@ -31,4 +31,10 @@ THE SOFTWARE. + + + + + + \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/script_realm/ScriptSecurityRealm/help.html b/src/main/resources/hudson/plugins/script_realm/ScriptSecurityRealm/help.html index ddd2ccd..490d959 100644 --- a/src/main/resources/hudson/plugins/script_realm/ScriptSecurityRealm/help.html +++ b/src/main/resources/hudson/plugins/script_realm/ScriptSecurityRealm/help.html @@ -3,11 +3,13 @@ a custom authentication scheme, but don't want to write your own plugin.

- 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. +

In case of the failure, the output from the process will be reported in the exception message. +

\ No newline at end of file diff --git a/src/main/resources/index.jelly b/src/main/resources/index.jelly index 01e653c..aac3d84 100644 --- a/src/main/resources/index.jelly +++ b/src/main/resources/index.jelly @@ -1,3 +1,4 @@
- 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.
\ No newline at end of file diff --git a/src/main/webapp/help-emailResolver.html b/src/main/webapp/help-emailResolver.html new file mode 100644 index 0000000..cb87410 --- /dev/null +++ b/src/main/webapp/help-emailResolver.html @@ -0,0 +1,5 @@ +
+ Choose how to resolve the e-mail of the authenticated user (you may want to install plugins to increase the available options).
+ If none, no e-mail resolution will be attempted (it will stay as is).
+ Be aware that not all options may work, as they may come from a plugin that follows its own configuration strategy.
+
\ No newline at end of file diff --git a/src/main/webapp/help-nameResolver.html b/src/main/webapp/help-nameResolver.html new file mode 100644 index 0000000..b4ec87d --- /dev/null +++ b/src/main/webapp/help-nameResolver.html @@ -0,0 +1,5 @@ +
+ Choose how to resolve the full name of the authenticated user (you may want to install plugins to increase the available options).
+ If none, no name resolution will be attempted (it will stay as is).
+ Be aware that not all options may work, as they may come from a plugin that follows its own configuration strategy.
+
\ No newline at end of file diff --git a/src/test/java/hudson/plugins/script_realm/ScriptSecurityRealmTest.java b/src/test/java/hudson/plugins/script_realm/ScriptSecurityRealmTest.java index 9e402e0..bd7d72c 100644 --- a/src/test/java/hudson/plugins/script_realm/ScriptSecurityRealmTest.java +++ b/src/test/java/hudson/plugins/script_realm/ScriptSecurityRealmTest.java @@ -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