reset_password.py 12 KB

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