From 2659e7430ff27adf324abad961bfe461599f1618 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Tue, 23 Jul 2013 17:48:55 -0400
Subject: [PATCH] Added PAMUserService for local Linux/Unix/MacOSX account authentication

---
 src/site/siteindex.mkd                        |    7 +
 src/site/setup_authentication.mkd             |    8 ++
 .classpath                                    |    1 
 src/main/java/com/gitblit/Constants.java      |    2 
 releases.moxie                                |    3 
 build.xml                                     |    2 
 src/main/distrib/data/gitblit.properties      |   16 ++++
 src/main/java/com/gitblit/PAMUserService.java |  142 +++++++++++++++++++++++++++++++++++
 src/site/features.mkd                         |    1 
 build.moxie                                   |    1 
 gitblit.iml                                   |   11 ++
 11 files changed, 190 insertions(+), 4 deletions(-)

diff --git a/.classpath b/.classpath
index 179bfdf..bfdaad2 100644
--- a/.classpath
+++ b/.classpath
@@ -45,6 +45,7 @@
 	<classpathentry kind="lib" path="ext/platform-3.5.0.jar" sourcepath="ext/src/platform-3.5.0.jar" />
 	<classpathentry kind="lib" path="ext/jna-3.5.0.jar" sourcepath="ext/src/jna-3.5.0.jar" />
 	<classpathentry kind="lib" path="ext/guava-13.0.1.jar" sourcepath="ext/src/guava-13.0.1.jar" />
+	<classpathentry kind="lib" path="ext/libpam4j-1.7.jar" sourcepath="ext/src/libpam4j-1.7.jar" />
 	<classpathentry kind="lib" path="ext/junit-4.11.jar" sourcepath="ext/src/junit-4.11.jar" />
 	<classpathentry kind="lib" path="ext/hamcrest-core-1.3.jar" sourcepath="ext/src/hamcrest-core-1.3.jar" />
 	<classpathentry kind="lib" path="ext/selenium-java-2.28.0.jar" sourcepath="ext/src/selenium-java-2.28.0.jar" />
diff --git a/build.moxie b/build.moxie
index d15d916..f417d3c 100644
--- a/build.moxie
+++ b/build.moxie
@@ -150,6 +150,7 @@
 - compile 'com.force.api:force-partner-api:24.0.0' :war
 - compile 'org.freemarker:freemarker:2.3.19' :war
 - compile 'com.github.dblock.waffle:waffle-jna:1.5' :war
+- compile 'org.kohsuke:libpam4j:1.7' :war
 - test 'junit'
 # Dependencies for Selenium web page testing
 - test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar
diff --git a/build.xml b/build.xml
index 4939f7f..d42750d 100644
--- a/build.xml
+++ b/build.xml
@@ -302,6 +302,7 @@
 			<class name="com.gitblit.RedmineUserService" />
 			<class name="com.gitblit.SalesforceUserService" />
 			<class name="com.gitblit.WindowsUserService" />
+			<class name="com.gitblit.PAMUserService" />
 		</mx:genjar>
 
 		<!-- Build the WAR file -->
@@ -421,6 +422,7 @@
 			<class name="com.gitblit.RedmineUserService" />
 			<class name="com.gitblit.SalesforceUserService" />
 			<class name="com.gitblit.WindowsUserService" />
+			<class name="com.gitblit.PAMUserService" />
 		</mx:genjar>
 
 		<!-- Build Express Zip file -->
diff --git a/gitblit.iml b/gitblit.iml
index 85ba779..c4ba270 100644
--- a/gitblit.iml
+++ b/gitblit.iml
@@ -468,6 +468,17 @@
         </SOURCES>
       </library>
     </orderEntry>
+    <orderEntry type="module-library">
+      <library name="libpam4j-1.7.jar">
+        <CLASSES>
+          <root url="jar://$MODULE_DIR$/ext/libpam4j-1.7.jar!/" />
+        </CLASSES>
+        <JAVADOC />
+        <SOURCES>
+          <root url="jar://$MODULE_DIR$/ext/src/libpam4j-1.7.jar!/" />
+        </SOURCES>
+      </library>
+    </orderEntry>
     <orderEntry type="module-library" scope="TEST">
       <library name="junit-4.11.jar">
         <CLASSES>
diff --git a/releases.moxie b/releases.moxie
index 17894a5..2f601eb 100644
--- a/releases.moxie
+++ b/releases.moxie
@@ -32,9 +32,12 @@
     additions:
 	- Added optional browser-side page caching using Last-Modified and Cache-Control for the dashboard, activity, project, and several repository pages
 	- Added a GET_USER request type for the RPC mechanism (issue-275)
+	- Added PAMUserService to authenticate against a local Linux/Unix/MacOSX server
     dependencyChanges: ~
 	settings:
 	- { name: 'web.pageCacheExpires', defaultValue: 0 }
+	- { name: 'realm.pam.backingUserService', defaultValue: 'users.conf' }
+	- { name: 'realm.pam.serviceName', defaultValue: 'system-auth' }
     contributors:
 	- Rainer Alföldi 
 	- Liyu Wang
diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties
index 1ee6d80..7f2f8b4 100644
--- a/src/main/distrib/data/gitblit.properties
+++ b/src/main/distrib/data/gitblit.properties
@@ -501,6 +501,7 @@
 #    com.gitblit.RedmineUserService
 #    com.gitblit.SalesforceUserService
 #    com.gitblit.WindowsUserService
+#    com.gitblit.PAMUserService
 #
 # Any custom user service implementation must have a public default constructor.
 #
@@ -1212,6 +1213,21 @@
 # SINCE 1.3.0
 realm.windows.defaultDomain =
 
+# The PAMUserService must be backed by another user service for standard user
+# and team management.
+# default: users.conf
+#
+# RESTART REQUIRED
+# BASEFOLDER
+# SINCE 1.3.1
+realm.pam.backingUserService = ${baseFolder}/users.conf
+
+# The PAM service name for authentication.
+# default: system-auth
+#
+# SINCE 1.3.1
+realm.pam.serviceName = system-auth
+
 # The SalesforceUserService must be backed by another user service for standard user
 # and team management.
 # default: users.conf
diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
index 67f9d65..c180baf 100644
--- a/src/main/java/com/gitblit/Constants.java
+++ b/src/main/java/com/gitblit/Constants.java
@@ -480,7 +480,7 @@
 	}
 	
 	public static enum AccountType {
-		LOCAL, EXTERNAL, LDAP, REDMINE, SALESFORCE, WINDOWS;
+		LOCAL, EXTERNAL, LDAP, REDMINE, SALESFORCE, WINDOWS, PAM;
 		
 		public boolean isLocal() {
 			return this == LOCAL;
diff --git a/src/main/java/com/gitblit/PAMUserService.java b/src/main/java/com/gitblit/PAMUserService.java
new file mode 100644
index 0000000..692b0f4
--- /dev/null
+++ b/src/main/java/com/gitblit/PAMUserService.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * 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.gitblit;
+
+import java.io.File;
+
+import org.jvnet.libpam.PAM;
+import org.jvnet.libpam.PAMException;
+import org.jvnet.libpam.impl.CLibrary;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants.AccountType;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Implementation of a PAM user service for Linux/Unix/MacOSX.
+ * 
+ * @author James Moger
+ */
+public class PAMUserService extends GitblitUserService {
+
+    private final Logger logger = LoggerFactory.getLogger(PAMUserService.class);
+
+    private IStoredSettings settings;
+    
+    public PAMUserService() {
+        super();
+    }
+
+    @Override
+    public void setup(IStoredSettings settings) {
+        this.settings = settings;
+
+        String file = settings.getString(Keys.realm.pam.backingUserService, "${baseFolder}/users.conf");
+        File realmFile = GitBlit.getFileOrFolder(file);
+
+        serviceImpl = createUserService(realmFile);
+        logger.info("PAM User Service backed by " + serviceImpl.toString());
+        
+        // Try to identify the passwd database
+        String [] files = { "/etc/shadow", "/etc/master.passwd" };
+		File passwdFile = null;
+		for (String name : files) {
+			File f = new File(name);
+			if (f.exists()) {
+				passwdFile = f;
+				break;
+			}
+		}
+		if (passwdFile == null) {
+			logger.error("PAM User Service could not find a passwd database!");
+		} else if (!passwdFile.canRead()) {
+			logger.error("PAM User Service can not read passwd database {}! PAM authentications may fail!", passwdFile);
+		}
+    }
+    
+    @Override
+    public boolean supportsCredentialChanges() {
+        return false;
+    }
+
+    @Override
+    public boolean supportsDisplayNameChanges() {
+        return true;
+    }
+
+    @Override
+    public boolean supportsEmailAddressChanges() {
+        return true;
+    }
+
+    @Override
+    public boolean supportsTeamMembershipChanges() {
+        return true;
+    }
+    
+	 @Override
+	protected AccountType getAccountType() {
+		return AccountType.PAM;
+	}
+
+    @Override
+    public UserModel authenticate(String username, char[] password) {
+		if (isLocalAccount(username)) {
+			// local account, bypass PAM authentication
+			return super.authenticate(username, password);
+		}
+		
+		if (CLibrary.libc.getpwnam(username) == null) {
+			logger.warn("Can not get PAM passwd for " + username);
+			return null;
+		}
+
+		PAM pam = null;
+		try {
+			String serviceName = settings.getString(Keys.realm.pam.serviceName, "system-auth");
+			pam = new PAM(serviceName);
+			pam.authenticate(username, new String(password));
+		} catch (PAMException e) {
+			logger.error(e.getMessage());
+			return null;
+		} finally {
+			pam.dispose();
+		}
+
+        UserModel user = getUserModel(username);
+        if (user == null)	// create user object for new authenticated user
+        	user = new UserModel(username.toLowerCase());
+
+        // create a user cookie
+        if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {
+        	user.cookie = StringUtils.getSHA1(user.username + new String(password));
+        }
+
+        // update user attributes from UnixUser
+        user.accountType = getAccountType();
+        user.password = Constants.EXTERNAL_ACCOUNT;
+
+        // TODO consider mapping PAM groups to teams
+
+        // push the changes to the backing user service
+        super.updateUserModel(user);
+        
+        return user;
+    }
+}
diff --git a/src/site/features.mkd b/src/site/features.mkd
index b9b44a5..d5bf410 100644
--- a/src/site/features.mkd
+++ b/src/site/features.mkd
@@ -37,6 +37,7 @@
 - Redmine authentication
 - Salesforce.com authentication
 - Windows authentication
+- PAM authentication
 - Gravatar integration
 - Git-notes display support
 - Submodule support
diff --git a/src/site/setup_authentication.mkd b/src/site/setup_authentication.mkd
index 7f60618..0ec07fa 100644
--- a/src/site/setup_authentication.mkd
+++ b/src/site/setup_authentication.mkd
@@ -6,6 +6,7 @@
 
 * LDAP authentication
 * Windows authentication
+* PAM authentication
 * Redmine auhentication
 * Salesforce.com authentication
 * Servlet container authentication
@@ -83,6 +84,13 @@
     realm.userService = com.gitblit.WindowsUserService
     realm.windows.defaultDomain =
 
+### PAM Authentication
+
+PAM authentication is based on the use of libpam4j and JNA.  To use this service, your Gitblit server must be installed on a Linux/Unix/MacOSX machine and the user that Gitblit runs-as must have root permissions.
+
+    realm.userService = com.gitblit.PAMUserService
+    realm.pam.serviceName = system-auth
+
 ### Redmine Authentication
 
 You may authenticate your users against a Redmine installation as long as your Redmine install has properly enabled [API authentication](http://www.redmine.org/projects/redmine/wiki/Rest_Api#Authentication).  This user service only supports user authentication; it does not support team creation based on Redmine groups.  Redmine administrators will also be Gitblit administrators.
diff --git a/src/site/siteindex.mkd b/src/site/siteindex.mkd
index f72e9c1..1b6dcdf 100644
--- a/src/site/siteindex.mkd
+++ b/src/site/siteindex.mkd
@@ -68,9 +68,10 @@
 - Groovy push hook scripts
 - Pluggable user service mechanism
     - LDAP authentication with optional LDAP-controlled Team memberships
-	- Redmine authentication
-	- SalesForce.com authentication
-	- Windows authentication
+    - Redmine authentication
+    - SalesForce.com authentication
+    - Windows authentication
+    - PAM authentication
     - Custom authentication, authorization, and user management
 - Rich RSS feeds
 - JSON-based RPC mechanism

--
Gitblit v1.9.1