reset_password.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. #!/usr/bin/python
  2. #
  3. # Copyright (c) 2017-2019 Joe Clarke <jclarke@cisco.com>
  4. # All rights reserved.
  5. #
  6. # Redistribution and use in source and binary forms, with or without
  7. # modification, are permitted provided that the following conditions
  8. # are met:
  9. # 1. Redistributions of source code must retain the above copyright
  10. # notice, this list of conditions and the following disclaimer.
  11. # 2. Redistributions in binary form must reproduce the above copyright
  12. # notice, this list of conditions and the following disclaimer in the
  13. # documentation and/or other materials provided with the distribution.
  14. #
  15. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  16. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  17. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  18. # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  19. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  20. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  21. # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  22. # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  23. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  24. # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  25. # SUCH DAMAGE.
  26. from __future__ import print_function
  27. from builtins import bytes
  28. from pyad import *
  29. import re
  30. from functools import wraps
  31. from flask import request, Response, session, redirect
  32. from flask import Flask
  33. import pythoncom
  34. import CLEUCreds
  35. import socket
  36. from cleu.config import Config as C
  37. LOG_EMERG = 0 # system is unusable
  38. LOG_ALERT = 1 # action must be taken immediately
  39. LOG_CRIT = 2 # critical conditions
  40. LOG_ERR = 3 # error conditions
  41. LOG_WARNING = 4 # warning conditions
  42. LOG_NOTICE = 5 # normal but significant condition
  43. LOG_INFO = 6 # informational
  44. LOG_DEBUG = 7 # debug-level messages
  45. # facility codes
  46. LOG_KERN = 0 # kernel messages
  47. LOG_USER = 1 # random user-level messages
  48. LOG_MAIL = 2 # mail system
  49. LOG_DAEMON = 3 # system daemons
  50. LOG_AUTH = 4 # security/authorization messages
  51. LOG_SYSLOG = 5 # messages generated internally by syslogd
  52. LOG_LPR = 6 # line printer subsystem
  53. LOG_NEWS = 7 # network news subsystem
  54. LOG_UUCP = 8 # UUCP subsystem
  55. LOG_CRON = 9 # clock daemon
  56. LOG_AUTHPRIV = 10 # security/authorization messages (private)
  57. LOG_FTP = 11 # FTP daemon
  58. # other codes through 15 reserved for system use
  59. LOG_LOCAL0 = 16 # reserved for local use
  60. LOG_LOCAL1 = 17 # reserved for local use
  61. LOG_LOCAL2 = 18 # reserved for local use
  62. LOG_LOCAL3 = 19 # reserved for local use
  63. LOG_LOCAL4 = 20 # reserved for local use
  64. LOG_LOCAL5 = 21 # reserved for local use
  65. LOG_LOCAL6 = 22 # reserved for local use
  66. LOG_LOCAL7 = 23 # reserved for local use
  67. AD_DC = "dc1-ad." + C.AD_DOMAIN
  68. app = Flask("CLEU Password Reset")
  69. def send_syslog(msg, facility=LOG_LOCAL7, severity=LOG_NOTICE, host="localhost", port=514):
  70. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  71. if severity is not None and facility is not None:
  72. data = "<%d>%s" % (severity + facility * 8, msg)
  73. else:
  74. data = msg
  75. sock.sendto(bytes(data, "utf-8"), (host, port))
  76. sock.close()
  77. def query_user(username, password, target_user):
  78. global AD_DC
  79. try:
  80. adcontainer.ADContainer.from_dn(C.AD_DN_BASE, options={"ldap_server": AD_DC, "username": username, "password": password})
  81. except Exception as e:
  82. print(e)
  83. return None
  84. try:
  85. q = adquery.ADQuery(options={"ldap_server": AD_DC, "username": username, "password": password})
  86. q.execute_query(
  87. attributes=["distinguishedName"],
  88. where_clause="sAMAccountName='{}'".format(target_user),
  89. base_dn=C.AD_DN_BASE,
  90. options={"ldap_server": AD_DC, "username": username, "password": password},
  91. )
  92. for row in q.get_results():
  93. return row["distinguishedName"]
  94. except Exception as e:
  95. print(e)
  96. return None
  97. def check_auth(username, password):
  98. pythoncom.CoInitialize()
  99. if username == C.VPN_USER or username == C.VPN_USER + "@" + C.AD_DOMAIN:
  100. return False
  101. if "dn" in session:
  102. return True
  103. if not re.search(r"@{}$".format(C.AD_DOMAIN), username):
  104. username += "@{}".format(C.AD_DOMAIN)
  105. target_username = username.replace("@{}".format(C.AD_DOMAIN), "")
  106. try:
  107. dn = query_user(username, password, target_username)
  108. if dn is not None:
  109. session["dn"] = dn
  110. session["target_username"] = target_username
  111. session["first_time"] = False
  112. return True
  113. else:
  114. try:
  115. dn = None
  116. dn = query_user(CLEUCreds.AD_ADMIN, CLEUCreds.AD_PASSWORD, target_username)
  117. if dn is None:
  118. return False
  119. adu = aduser.ADUser.from_dn(
  120. dn, options={"ldap_server": AD_DC, "username": CLEUCreds.AD_ADMIN, "password": CLEUCreds.AD_PASSWORD}
  121. )
  122. obj = adu.get_attribute("pwdLastSet", False)
  123. if password == CLEUCreds.DEFAULT_USER_PASSWORD and int(obj.highpart) == 0 and int(obj.lowpart) == 0:
  124. session["dn"] = dn
  125. session["target_username"] = target_username
  126. session["first_time"] = True
  127. return True
  128. except Exception as ie:
  129. print(ie)
  130. return False
  131. except Exception as e:
  132. print(e)
  133. return False
  134. return False
  135. def authenticate():
  136. if "loggedout" in session:
  137. del session["loggedout"]
  138. return Response(
  139. "Failed to verify credentials for password reset.\n" "You have to login with proper credentials.",
  140. 401,
  141. {"WWW-Authenticate": 'Basic realm="CLEU Password Reset; !!!ENTER YOUR AD USERNAME AND PASSWORD!!!"'},
  142. )
  143. def requires_auth(f):
  144. @wraps(f)
  145. def decorated(*args, **kwargs):
  146. auth = request.authorization
  147. if session.get("loggedout", False) or not auth or not check_auth(auth.username, auth.password):
  148. return authenticate()
  149. return f(*args, **kwargs)
  150. return decorated
  151. @app.route("/logout", methods=["GET"])
  152. def logout():
  153. session.clear()
  154. session["loggedout"] = True
  155. return redirect("/", code=302)
  156. @app.route("/reset-password", methods=["POST"])
  157. @requires_auth
  158. def reset_password():
  159. new_pw = request.form.get("new_pass")
  160. new_pw_confirm = request.form.get("new_pass_confirm")
  161. vpnuser = request.form.get("vpnuser")
  162. if new_pw.strip() == "" or new_pw_confirm.strip() == "":
  163. return Response(
  164. """
  165. <html>
  166. <head>
  167. <title>Bad Password</title>
  168. </head>
  169. <body>
  170. <p>You must specify a new password.</p>
  171. </body>
  172. </html>""",
  173. mimetype="text/html",
  174. )
  175. if new_pw != new_pw_confirm:
  176. return Response(
  177. """
  178. <html>
  179. <head>
  180. <title>Bad Password</title>
  181. </head>
  182. <body>
  183. <p>Passwords did not match</p>
  184. </body>
  185. </html>""",
  186. mimetype="text/html",
  187. )
  188. adu = aduser.ADUser.from_dn(
  189. session["dn"], options={"ldap_server": AD_DC, "username": CLEUCreds.AD_ADMIN, "password": CLEUCreds.AD_PASSWORD}
  190. )
  191. try:
  192. adu.set_password(new_pw)
  193. except Exception as e:
  194. return Response(
  195. """
  196. <html>
  197. <head>
  198. <title>Failed to Reset Password</title>
  199. </head>
  200. <body>
  201. <h1>Password Reset Failed!</h1>
  202. <p>{}</p>
  203. </body>
  204. </html>""".format(
  205. e
  206. ),
  207. mimetype="text/html",
  208. )
  209. adu.grant_password_lease()
  210. del session["dn"]
  211. resp = """
  212. <html>
  213. <head>
  214. <title>Password Changed Successfully!</title>
  215. </head>
  216. <body>
  217. <h1>Password Changed Successfully!</h1>"""
  218. if session.get("first_time", False):
  219. send_syslog(
  220. "PRINT-LABEL: requesting to print label for userid {}".format(session["target_username"]),
  221. None,
  222. None,
  223. C.TOOL,
  224. )
  225. resp += "\n<h1>Please go see Dave Shen to get your barcode label.</h1>"
  226. if vpnuser and vpnuser == "true":
  227. resp += "\n<h1>Please disconnect your VPN and connect again with your AD credentials.</h1>"
  228. else:
  229. resp += "\n<h1>Please close this browser window.</h1>"
  230. resp += "\n<script>setTimeout(function() { window.location = '/logout'; }, 60000);</script>"
  231. resp += """
  232. </body>
  233. </html>"""
  234. return Response(resp, mimetype="text/html")
  235. @app.route("/")
  236. @requires_auth
  237. def get_main():
  238. page = """
  239. <html>
  240. <head>
  241. <title>Password Reset Form</title>
  242. <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  243. <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/datatables/1.10.12/css/dataTables.bootstrap.min.css" integrity="sha256-7MXHrlaY+rYR1p4jeLI23tgiUamQVym2FWmiUjksFDc=" crossorigin="anonymous" />
  244. <meta name="viewport" content="width=device-width, initial-scale=1">
  245. <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
  246. <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/datatables/1.10.12/js/jquery.dataTables.min.js" integrity="sha256-TX6POJQ2u5/aJmHTJ/XUL5vWCbuOw0AQdgUEzk4vYMc=" crossorigin="anonymous"></script>
  247. <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/datatables/1.10.12/js/dataTables.bootstrap.min.js" integrity="sha256-90YqnHom4j8OhcEQgyUI2IhmGYTBO54Adcf3YDZU9xM=" crossorigin="anonymous"></script>
  248. <script>
  249. function verify() {
  250. if (!$('#new_pass').val().trim()) {
  251. alert('Please specify a new password.');
  252. return false;
  253. }
  254. if (!$('#new_pass_confirm').val().trim()) {
  255. alert('Please confirm the new password.');
  256. return false;
  257. }
  258. if ($('#new_pass_confirm').val().trim() != $('#new_pass').val().trim()) {
  259. alert('Passwords do not match.');
  260. return false;
  261. }
  262. return true;
  263. }
  264. </script>
  265. </head>
  266. <body>
  267. <div class="container" role="main" style="width: 100%;">
  268. <div class="page-header">
  269. <h3>Password Reset Form</h3>
  270. </div>
  271. <div class="row">
  272. <div class="col-sm-8">
  273. <form method="POST" onSubmit="return verify();" action="/reset-password">
  274. <div class="form-group">
  275. <label for="new_pass">New Password:</label>
  276. <input type="password" name="new_pass" id="new_pass" class="form-control" placeholder="New Password">
  277. </div>
  278. <div class="form-group">
  279. <label for="new_pass_confirm">Confirm New Password:</label>
  280. <input type="password" name="new_pass_confirm" id="new_pass_confirm" class="form-control" placeholder="Confirm New Password">
  281. </div>
  282. <div class="form-group">
  283. <input type="submit" name="submit" value="Reset My Password!" class="btn btn-primary">
  284. <input type="reset" name="reset" value="Start Over" class="btn btn-default">
  285. </div>"""
  286. page += '\n<input type="hidden" name="vpnuser" value="%s"/>' % (request.args.get("vpnuser"))
  287. page += """
  288. </form>
  289. </div>
  290. </div>
  291. </div>
  292. </body>
  293. </html>"""
  294. return Response(page, mimetype="text/html")
  295. if __name__ == "__main__":
  296. app.secret_key = CLEUCreds.AD_PASSWORD
  297. app.run(host="10.100.252.25", port=8443, threaded=True, ssl_context=("chain.pem", "privkey.pem"))