From 2b0f9fe362a5f699f2c3ef52206f6f7039f84816 Mon Sep 17 00:00:00 2001
From: Pavel Březina <pbrezina@redhat.com>
Date: Mar 28 2017 09:28:17 +0000
Subject: add faillock support


---

diff --git a/authconfig.py b/authconfig.py
index ca7cfe5..9d891c9 100755
--- a/authconfig.py
+++ b/authconfig.py
@@ -323,6 +323,13 @@ class Authconfig:
 			help=_("require at least one other character in a password"))
 		parser.add_option("--disablereqother", action="store_true",
 			help=_("do not require other characters in a password"))
+		
+		parser.add_option("--enablefaillock", action="store_true",
+			help=_("enable account locking in case of too many consecutive authentication failures"))
+		parser.add_option("--disablefaillock", action="store_true",
+			help=_("disable account locking on too many consecutive authentication failures"))
+		parser.add_option("--faillockargs", metavar=_("<options>"),
+			help=_("the pam_faillock module options"))
 
 		parser.add_option("--nostart", action="store_true",
 			help=_("do not start/stop portmap, ypbind, and nscd"))
@@ -423,7 +430,8 @@ class Authconfig:
                         "reqlower":"passReqLower",
                         "requpper":"passReqUpper",
                         "reqdigit":"passReqDigit",
-                        "reqother":"passReqOther"}
+                        "reqother":"passReqOther",
+                        "faillock":"enableFaillock"}
 
 		string_settings = {"passalgo":"passwordAlgorithm",
 			"ldapserver":"ldapServer",
@@ -447,7 +455,8 @@ class Authconfig:
                         "passminlen":"passMinLen",
                         "passminclass":"passMinClass",
                         "passmaxrepeat":"passMaxRepeat",
-                        "passmaxclassrepeat":"passMaxClassRepeat"}
+                        "passmaxclassrepeat":"passMaxClassRepeat",
+                        "faillockargs":"faillockArgs"}
 
 		for opt, aival in bool_settings.items():
 			if getattr(self.options, "enable"+opt):
diff --git a/authinfo.py b/authinfo.py
index 40f302c..7ef7453 100644
--- a/authinfo.py
+++ b/authinfo.py
@@ -461,6 +461,8 @@ pam_modules[STANDARD] = [
 	 "ldap",		argv_ldap_auth],
 	[False, AUTH,		LOGIC_SUFFICIENT,
 	 "winbind",		argv_winbind_auth],
+	[False,  AUTH,		LOGIC_REQUIRED,
+	 "faillock",	["authfail"]],
 	[True,  AUTH,		LOGIC_REQUIRED,
 	 "deny",		[]],
 # Account management is tricky.  Because we've implicitly committed to
@@ -480,6 +482,8 @@ pam_modules[STANDARD] = [
 # successful call to pam_permit at the end as a requirement.
 	[False, ACCOUNT,	LOGIC_REQUIRED,
 	 "access",		[]],
+	[False, ACCOUNT,	LOGIC_REQUIRED,
+	 "faillock",	[]],
 	[True,  ACCOUNT,	LOGIC_REQUIRED,
 	 "unix",		[]],
 	[False,  ACCOUNT,	LOGIC_SUFFICIENT,
@@ -568,10 +572,14 @@ pam_modules[PASSWORD_ONLY] = [
 	 "ldap",		argv_ldap_auth],
 	[False, AUTH,		LOGIC_SUFFICIENT,
 	 "winbind",		argv_winbind_auth],
+	[False,  AUTH,		LOGIC_REQUIRED,
+	 "faillock",	["authfail"]],
 	[True,  AUTH,		LOGIC_REQUIRED,
 	 "deny",		[]],
 	[False, ACCOUNT,	LOGIC_REQUIRED,
 	 "access",		[]],
+	[False, ACCOUNT,	LOGIC_REQUIRED,
+	 "faillock",	[]],
 	[True,  ACCOUNT,	LOGIC_REQUIRED,
 	 "unix",		[]],
 	[False,  ACCOUNT,	LOGIC_SUFFICIENT,
@@ -633,10 +641,14 @@ pam_modules[FINGERPRINT] = [
 	 "deny",		[]],
 	[False,  AUTH,		LOGIC_SUFFICIENT,
 	 "fprintd",		[]],
+	[False,  AUTH,		LOGIC_REQUIRED,
+	 "faillock",	["authfail"]],
 	[True,  AUTH,		LOGIC_REQUIRED,
 	 "deny",		[]],
 	[False, ACCOUNT,	LOGIC_REQUIRED,
 	 "access",		[]],
+	[False, ACCOUNT,		LOGIC_REQUIRED,
+	 "faillock",	[]],
 	[True,  ACCOUNT,	LOGIC_REQUIRED,
 	 "unix",		[]],
 	[False,  ACCOUNT,	LOGIC_SUFFICIENT,
@@ -688,10 +700,14 @@ pam_modules[SMARTCARD] = [
 	 "krb5",		argv_krb5_sc_auth],
 	[False, AUTH,		LOGIC_SUFFICIENT,
 	 "permit",		[]],
+	[False,  AUTH,		LOGIC_REQUIRED,
+	 "faillock",	["authfail"]],
 	[True,  AUTH,		LOGIC_REQUIRED,
 	 "deny",		[]],
 	[False, ACCOUNT,	LOGIC_REQUIRED,
 	 "access",		[]],
+	[False, ACCOUNT,	LOGIC_REQUIRED,
+	 "faillock",	[]],
 	[True,  ACCOUNT,	LOGIC_REQUIRED,
 	 "unix",		[]],
 	[False,  ACCOUNT,	LOGIC_SUFFICIENT,
@@ -1302,6 +1318,10 @@ class AuthInfo:
 		self.passReqDigit = None
 		self.passReqOther = None
 
+		# Faillock
+		self.enableFaillock = None
+		self.faillockArgs = "deny=4 unlock_time=1200"
+
 		# Not really options.
 		self.joinUser = ""
 		self.joinPassword = ""
@@ -1358,6 +1378,7 @@ class AuthInfo:
 		("enableLDAP", "b"), ("enableMDNS", "b"), ("enableMyhostname", "b"),
 		("enableSSSD", "b"), ("preferDNSinHosts", "b"), ("implicitSSSD", "b")]),
 	SaveGroup(self.writePAM, None, [("pwqualityArgs", "c"), ("passwdqcArgs", "c"),
+		("faillockArgs", "c"), ("enableFaillock", "b"),
 		("localuserArgs", "c"), ("pamAccessArgs", "c"), ("enablePAMAccess", "b"),
 		("mkhomedirArgs", "c"), ("enableMkHomeDir", "b"), ("algoRounds", "c"),
 		("passwordAlgorithm", "i"), ("enableShadow", "b"), ("enableNIS", "b"),
@@ -1374,6 +1395,7 @@ class AuthInfo:
 		("enableEcryptfs", "b"), ("enableSmartcard", "b"), ("forceSmartcard", "b"),
 		("enableWinbindAuth", "b"), ("enableWinbind", "b"), ("winbindKrb5", "b"),
 		("enablePWQuality", "b"), ("enablePasswdQC", "b"),
+		("enableFaillock", "b"), ("faillockArgs", "c"),
 		("enableLocAuthorize", "b"), ("enablePAMAccess", "b"), ("enableCacheCreds", "b"),
 		("enableMkHomeDir", "b"), ("enableSysNetAuth", "b"), ("enableFprintd", "b"),
 		("enableSSSD", "b"), ("enableSSSDAuth", "b"), ("enableForceLegacy", "b")]),
@@ -2124,6 +2146,12 @@ class AuthInfo:
 			if stack == "auth":
 				if module.startswith("pam_unix"):
 					self.setParam("enableNullOk", args.find("nullok") >= 0, ref)
+				if module.startswith("pam_faillock"):
+					self.setParam("enableFaillock", True, ref)
+					if args:
+						args.replace('authfail', '').strip()
+						self.setParam("faillockArgs", args, ref)
+					continue
 			if stack == "account":
 				if module.startswith("pam_unix"):
 					self.setParam("brokenShadow", args.find("broken_shadow") >= 0, ref)
@@ -2158,6 +2186,11 @@ class AuthInfo:
 			except ValueError:
 				pass
 			try:
+				self.enableFaillock = shv.getBoolValue("USEFAILLOCK")
+				self.faillockArgs = shv.getValue("FAILLOCKARGS")
+			except ValueError:
+				pass
+			try:
 				self.enableEcryptfs = shv.getBoolValue("USEECRYPTFS")
 			except ValueError:
 				pass
@@ -3676,6 +3709,8 @@ class AuthInfo:
 					if (self.forceBrokenShadow or self.enableLDAPAuth or
 						self.enableKerberos or self.enableWinbindAuth):
 						output += " broken_shadow"
+			if name == "faillock" and stack == "auth":
+				args = " ".join(module[ARGV]) + " " + self.faillockArgs
 			if args:
 				output += " " + args
 		output += "\n"
@@ -3740,6 +3775,7 @@ class AuthInfo:
 				prevmodule = module
 				if (module[MANDATORY] or
 					(self.enablePWQuality and module[NAME] == "pwquality") or
+					(self.enableFaillock and module[NAME] == "faillock") or
 					(self.enableEcryptfs and module[NAME] == "ecryptfs") or
 					((self.enableKerberos and not self.implicitSSSDAuth)and module[NAME] == "krb5" and
 						not module[ARGV] == argv_krb5_sc_auth) or
@@ -3798,6 +3834,8 @@ class AuthInfo:
 			return False
 
 		shv.setBoolValue("USEPWQUALITY", self.enablePWQuality)
+		shv.setBoolValue("USEFAILLOCK", self.enableFaillock)
+		shv.setValue("FAILLOCKARGS", self.faillockArgs)
 		shv.setBoolValue("USELDAP", self.enableLDAP)
 		shv.setBoolValue("USENIS", self.enableNIS)
 		shv.setBoolValue("USEECRYPTFS", self.enableEcryptfs)
@@ -4030,6 +4068,8 @@ class AuthInfo:
 			self.passwdqcArgs))
 		print("pam_access is %s (%s)" % (formatBool(self.enablePAMAccess),
 			self.pamAccessArgs))
+		print("pam_faillock is %s (%s)" % (formatBool(self.enableFaillock),
+			self.faillockArgs))
 		print("pam_mkhomedir or pam_oddjob_mkhomedir is %s (%s)" % (formatBool(self.enableMkHomeDir),
 			self.mkhomedirArgs))
 		print("Always authorize local users is %s (%s)" % (formatBool(self.enableLocAuthorize),

