aboutsummaryrefslogtreecommitdiff
path: root/bws.py
diff options
context:
space:
mode:
authorB. Stack <bgstack15@gmail.com>2022-01-27 09:51:44 -0500
committerB. Stack <bgstack15@gmail.com>2022-01-27 09:51:44 -0500
commitadfc6caaedefaeea913a7f5c4ad7781bb629ecba (patch)
tree4255fe3d57adaa4a0af8f9a93c0347afa70ea01f /bws.py
downloadbws-master.tar.gz
bws-master.tar.bz2
bws-master.zip
initial commitHEADmaster
Diffstat (limited to 'bws.py')
-rwxr-xr-xbws.py745
1 files changed, 745 insertions, 0 deletions
diff --git a/bws.py b/bws.py
new file mode 100755
index 0000000..a40fdb5
--- /dev/null
+++ b/bws.py
@@ -0,0 +1,745 @@
+#!/usr/bin/env python
+# Startdate: 2021-06-17
+# goals:
+# References:
+# for session_app:
+# https://code.tutsplus.com/tutorials/flask-authentication-with-ldap--cms-23101
+# https://www.techlifediary.com/python-web-development-tutorial-using-flask-session-cookies/
+# delete cookie https://stackoverflow.com/a/14386413/3569534
+# timeout sessions https://stackoverflow.com/a/11785722/3569534
+# future: https://code.tutsplus.com/tutorials/flask-authentication-with-ldap--cms-23101
+# better timeout session: https://stackoverflow.com/a/49891626/3569534
+# store "formdata" in session for changing the basic auth to form data for the ldap login https://stackoverflow.com/a/56904875/3569534
+# modify url from urlparse https://stackoverflow.com/a/21629125/3569534
+# preserve POST with code 307 https://stackoverflow.com/a/15480983/3569534
+# for bws:
+# https://stackoverflow.com/questions/37259740/passing-variables-from-flask-to-javascript
+# https://stackoverflow.com/questions/43690222/overloading-less-than-in-python/45493522#45493522
+# https://stackoverflow.com/questions/21787496/converting-epoch-time-with-milliseconds-to-datetime
+# for javascript:
+# https://javascript.info/xmlhttprequest
+# https://stackoverflow.com/questions/38370854/making-a-div-flash-just-once/38370873#38370873
+# https://stackoverflow.com/questions/38370854/making-a-div-flash-just-once/38371229#38371229
+# https://stackoverflow.com/questions/8674618/adding-options-to-select-with-javascript/8674667#8674667
+# https://stackoverflow.com/questions/33758595/html-form-run-javascript-on-enter/33758792#33758792
+# https://stackoverflow.com/questions/647282/is-there-an-onselect-event-or-equivalent-for-html-select/12404521#12404521
+# get all options from select-options http://www.mredkj.com/tutorials/tutorial007.html
+# https://stackoverflow.com/questions/1628826/how-to-add-an-onchange-event-to-a-select-box-via-javascript/24829023#24829023
+# https://stackoverflow.com/questions/8267857/how-to-wait-until-array-is-filled-asynchronous/8267923#8267923
+# https://stackoverflow.com/questions/9713058/send-post-data-using-xmlhttprequest/9713078#9713078
+# Improve:
+# Run:
+# FLASK_APP=bws.py FLASK_DEBUG=1 flask run --host 0.0.0.0
+# Dependencies:
+# apt-get install python3-flask
+
+from flask import Flask, Response, redirect, url_for, render_template, request, _request_ctx_stack as stack, make_response, session, jsonify, escape
+#from flask_kerberos import init_kerberos, requires_authentication, _unauthorized, _forbidden, _gssapi_authenticate
+from functools import wraps
+from urllib.parse import urlparse
+import binascii, datetime, os, json
+# Custom libraries for Bookmark Web Service
+import session_ldap
+import bws_db
+
+app = Flask(__name__)
+if 'BWS_CONFIG' in os.environ:
+ conf_file=os.environ['BWS_CONFIG']
+else:
+ conf_file="bws.cfg"
+app.config.from_pyfile(conf_file, silent=False)
+#app.config.from_object(__name__)
+if 'DEBUG' in app.config and app.config['DEBUG']:
+ app.debug=True
+secret_key_value = os.urandom(24)
+secret_key_value_hex_encoded = binascii.hexlify(secret_key_value)
+app.config['SECRET_KEY'] = secret_key_value_hex_encoded
+app.permanent_session_lifetime=datetime.timedelta(minutes=app.config['SESSION_DURATION_MINUTES'])
+app.config['SESSION_TYPE']='filesystem'
+app.config['database'] = 'bws.sqlite1'
+
+## WRAPPER FUNCTIONS
+def requires_session(function):
+ '''
+ Requires a valid session, provided by cookie!
+ '''
+ @wraps(function)
+ def decorated(*args, **kwargs):
+ if not session:
+ return Response(f"Not logged in! Try <a href=\"{url_for('login')}\">logging in</a>.",401)
+ #return Response("requires session",401)
+ else:
+ if 'user' not in session:
+ return Response("User is not in this session.",401)
+ s_user = session['user']
+ s_groups = session['groups']
+ c_user = request.cookies.get('user')
+ print(f"session user: {s_user}")
+ print(f"cookie user: {c_user}")
+ if session['user'] != c_user:
+ return Response(f"Not logged in! Try <a href=\"{url_for('login')}\">logging in</a>.",401)
+ #return Response("Wrong user for this session!.",401)
+ # otherwise, everything is good!
+ return function(s_user, s_groups, *args,**kwargs)
+ # catch-all
+ return Response("requires session",401)
+ return decorated
+
+# Goal: function that determines if loggedin_uid = b.uid or if loggedin_uid is in group admins
+# This adds a parameter, allowed, to the function call
+def requires_bid_access(function):
+ """
+ Requires that loggedin_uid = b.uid or that loggedin_uid is in admin group
+ """
+ @wraps(function)
+ def decorated(*args, **kwargs):
+ print(f"DEBUG(bws) requires_bid_access: kwargs={kwargs}")
+ user, groups = args # magic
+ blank = bws_db.Bookmark(app.config['database'],_from_db=True)
+ loggedin_uid = bws_db.get_uid_from_any(app.config['database'],user)
+ buid = -1
+ bid = kwargs['bid']
+ allowed = True # until we prove otherwise
+ if "new" == bid:
+ b = blank
+ else:
+ # get Bookmark object
+ print(f"DEBUG(bws): will ask for bid {bid}.")
+ b = bws_db.get_bookmark_by_id(app.config['database'],bid)
+ buid = b.uid
+ print(f"Got bookmark {b}")
+ busername = "" # if not empty, then the loggedinuser is admin
+ if loggedin_uid != buid and (-1 != buid and "-1" != buid):
+ allowed = False
+ if app.config['ADMIN_GROUP'] in groups:
+ busername = bws_db.get_user_name_from_uid(app.config['database'],buid)
+ # Then we allow it, with a warning
+ allowed = True
+ print(f"DEBUG(bws) requires_bid_access: loggedin_uid = {loggedin_uid} and bookmark uid = {buid}")
+ # Yes, we are omitting **kwargs because to this decorator, kwargs is only bid which is already being passed somehow and we don't want to double-send it.
+ #return function(loggedin_uid, busername, allowed, *args)
+ # args is, [user,groups,<empty?>]
+ return function(*args,allowed,bid,loggedin_uid,busername,b)
+ return decorated
+
+def requires_authn_ldap(function):
+ '''
+ Require that the wrapped view function only be called by users
+ authenticated with ldap. The view function will have the authenticated
+ users principal passed to it as its first argument.
+ :param function: flask view function
+ :type function: function
+ :returns: decorated function
+ :rtype: function
+ '''
+ @wraps(function)
+ def decorated(*args, **kwargs):
+ # formdata is in session if we are coming from login_basic()
+ form = session.get('formdata', None)
+ if form:
+ #print(f"DEBUG(bws): requires_authn_ldap form={form}")
+ session.pop('formdata')
+ if 'username' in form:
+ username = form['username']
+ if 'password' in form:
+ pw = form['password']
+ else:
+ # then we are coming from the form with POST data
+ if 'username' not in request.form or 'password' not in request.form:
+ return _unauthorized_ldap()
+ username = request.form['username']
+ pw = request.form['password']
+ #print(f"DEBUG(bws): requires_authn_ldap with username={username} and pw={pw}")
+ # learn dn of user
+ this_uri = get_next_ldap_server(app)
+ this_user = session_ldap.list_matching_users(
+ server_uri=this_uri,
+ bind_dn=app.config['LDAP_BIND_DN'],
+ bind_pw=app.config['LDAP_BIND_PASSWORD'],
+ user_base=app.config['LDAP_USER_BASE'],
+ username=username,
+ user_match_attrib=app.config['LDAP_USER_MATCH_ATTRIB']
+ )
+ # list_matching_users always returns list, so if it contains <> 1 we are in trouble
+ if len(this_user) != 1:
+ print(f"WARNING: cannot determine unique user for {app.config['LDAP_USER_MATCH_ATTRIB']}={username} which returned {this_user}")
+ return _unauthorized_ldap()
+ this_user = this_user[0]
+ print(f"DEBUG(bws): requires_authn_ldap: found in ldap the username {this_user}")
+ ll = ldap_login(this_user,pw)
+ if ll:
+ shortuser = session_ldap.get_ldap_username_attrib_from_dn(
+ authenticated_user=ll,
+ user_match_attrib=app.config['LDAP_USER_DISPLAY_ATTRIB']
+ )
+ groups = session_ldap.get_ldap_user_groups(
+ connection=ll,
+ user_dn=this_user,
+ user_attrib_memberof=app.config['LDAP_USER_ATTRIB_MEMBEROF'],
+ group_name_attrib=app.config['LDAP_GROUP_DISPLAY_ATTRIB'],
+ group_base=app.config['LDAP_GROUP_BASE']
+ )
+ print(f"DEBUG(bws): user {shortuser} has groups {groups}")
+ return function(shortuser, groups ,*args, **kwargs)
+ else:
+ return _unauthorized_ldap()
+ return decorated
+
+def _unauthorized_ldap():
+ return Response(f'<meta http-equiv="Refresh" content="4; url={url_for("login")}">Unauthorized! Invalid ldap credentials... returning to login form', 401)
+
+def render_template_extra(templatefile,*args, **kwargs):
+ """Render template with extra values passed, for nav and info"""
+ s_user = session['user']
+ c_user = request.cookies.get('user')
+ groups = session['groups']
+ cookie=request.cookies
+ s_cn = ""
+ if 'computername' in session:
+ s_cn = session['computername']
+ s_t = "Bookmark Web Service"
+ if 'title' in session:
+ s_t = session['title']
+ #print(f"DEBUG(bws): render_template_extra title is \"{s_t}\"")
+ return render_template(templatefile,
+ # for nav
+ settings_url = url_for("settings"),
+ logout_url = url_for("logout"),
+ # for info
+ s_user=s_user,
+ cookie = cookie,
+ session_computername = s_cn,
+ session_title = s_t,
+ groups=groups,
+ *args,
+ **kwargs
+ )
+
+## BARE FUNCTIONS
+def convert_timestamp_from_epoch_to_useful(ts):
+ """
+ Convert epoch timestamps from varying specificity to a useful, to-the-seconds string without timezone, i.e., "YYYY-MM-DDTHH:MM:SS".
+ """
+ out_ts = ""
+ try:
+ out_ts = datetime.datetime.fromtimestamp(ts/1.0).strftime('%Y-%m-%dT%H:%M:%S')
+ except Exception as e:
+ #print(f"DEBUG(bws) convert_timestamp: woops, {e}, trying 1000")
+ try:
+ out_ts = datetime.datetime.fromtimestamp(ts/1000.0).strftime('%Y-%m-%dT%H:%M:%S')
+ except Exception as e:
+ #print(f"DEBUG(bws) convert_timestamp: woops, {e}, trying 1000000")
+ try:
+ out_ts = datetime.datetime.fromtimestamp(ts/1000000.0).strftime('%Y-%m-%dT%H:%M:%S')
+ except Exception as e:
+ #print(f"DEBUG(bws) convert_timestamp: woops, {e}, just using \"{ts}\" directly)")
+ out_ts = ts
+ return out_ts
+
+## ROUTE FUNCTIONS
+
+@app.route("/")
+def index():
+ if session:
+ print("This user is already logged in!")
+ return redirect(url_for("view"))
+ return render_template('index.html')
+
+@app.route("/view/")
+@requires_session
+def view(user=None,groups=None):
+ return render_template_extra('view.html')
+
+# This will be useful for setting the timestamp on the /edit/ page.
+@app.route("/internal/now/")
+def now():
+ return datetime.datetime.now().isoformat(timespec="seconds"),200
+
+@app.route("/edit/",defaults={'bid':-1},methods=['GET','POST'])
+@app.route("/edit/<bid>/",methods=['GET','POST'])
+@requires_session
+@requires_bid_access
+def edit(user=None,groups=None,allowed=False,bid="",loggedin_uid=-1,busername="",b=None):
+ #print(f"DEBUG(bws) edit: locals()={locals()}")
+ print(f"DEBUG(bws) edit: user={user}")
+ print(f"DEBUG(bws) edit: groups={groups}")
+ print(f"DEBUG(bws) edit: allowed={allowed}")
+ print(f"DEBUG(bws) edit: bid={bid}")
+ print(f"DEBUG(bws) edit: loggedin_uid={loggedin_uid}")
+ print(f"DEBUG(bws) edit: busername={busername}")
+ print(f"DEBUG(bws) edit: b={b}")
+ # Common things for both POST and GET
+ ts = datetime.datetime.now().isoformat(timespec='seconds'),
+ if "" == bid or -1 == bid or "-1" == bid:
+ try:
+ return redirect(request.referer)
+ except:
+ return "<script type='text/javascript'>function func3(){history.back(-1)};func3();</script>",302
+ admin_message = ""
+ if busername != "":
+ # Then we allow it, with a warning
+ admin_message = f"Using admin access to edit bookmark for user \"{busername}\"."
+ if "GET" == request.method:
+ if allowed:
+ print(f"DEBUG: for rendering edit.html, aoint={b.always_open_in_new_tab}")
+ return render_template_extra("edit.html",
+ bid = b.bid,
+ url = b.url,
+ title = b.title,
+ tags = b.tags,
+ ts = b.timestamp,
+ notes = b.notes,
+ iid = b.iid,
+ order_id = b.order_id,
+ always_open_in_new_tab = b.always_open_in_new_tab,
+ admin_message = admin_message,
+ allowed = allowed
+ )
+ else:
+ # not allowed, from above logic
+ return f"<meta http-equiv=\"Refresh\" content=\"2; url={url_for('view')}\">Invalid bookmark.<p>Returning momentarily...",401
+ elif "POST" == request.method:
+ f = request.form
+ # n is the new object dictionary, with the default values. Required fields are not listed here. Checkboxes are not listed here.
+ n = {
+ "title": "",
+ "tags": [],
+ "notes": "",
+ "iid": -1,
+ "timestamp": ts,
+ "order_id": -1
+ }
+ if 'draganddrop' in request.headers:
+ if "new" != bid:
+ return "Bad request",400
+ else:
+ # process the draganddrop stuff here
+ print(f"DEBUG(bws) edit/new/ d&d")
+ #for i in f:
+ # print(f"DEBUG(bws) edit/new/ d&d: Form[\"{i}\"]: \"{f[i]}\"")
+ if 'type' not in f:
+ return """Bad request: needs 'type' matching of these supported types: ['text/x-moz-place','text/x-moz-url','text/html']""",400
+ if 'string' not in f:
+ return "Bad request: no contents of 'string'!",400
+ ftype = f['type']
+ fstring = f['string']
+ if "text/x-moz-place" == ftype:
+ # item is a dict: title, id, itemGuid, instanceID, parent, parentGuid, dateAdded,lastModified,type,uri.
+ newitem = json.loads(fstring)
+ print(f"DEBUG(bws) edit/new/ d&d newitem={newitem}")
+ try: ftitle = newitem['title']
+ except: ftitle = n['title']
+ try: furl = newitem['uri']
+ except: return "Invalid request! No uri in text/x-moz-place object.",400
+ try: ftags = newitem['tags']
+ except: ftags = n['tags']
+ new_ftags = []
+ for i in ftags:
+ if str(i) != "":
+ new_ftags.append(i)
+ ftags = new_ftags
+ try: fnotes = newitem['comment']
+ except: fnotes = n['notes']
+ fts = convert_timestamp_from_epoch_to_useful(newitem['dateAdded'])
+ try: forder_id = newitem['id']
+ except: forder_id = 0
+ faoint = False
+ try:
+ fusername = f['username']
+ fuid = bws_db.get_uid_from_any(app.config['database'],fusername)
+ except:
+ fuid = loggedin_uid
+ b = bws_db.Bookmark(
+ source = app.config['database'],
+ url = furl,
+ title = ftitle,
+ uid = fuid,
+ tags = ftags,
+ notes = fnotes,
+ iid = 0,
+ timestamp = fts,
+ order_id = forder_id,
+ always_open_in_new_tab = faoint
+ )
+ if -1 == b.bid or "-1" == b.bid:
+ return "Something failed on server side.",500
+ else:
+ return f"Added bookmark id {b.bid}",201 # will get displayed in the drop box for 1.5 seconds by the javascript
+ return "OK",200
+ elif "text/x-moz-url" == ftype:
+ print(f"DEBUG(bws) still need support to parse text/x-moz-url \"{fstring}\"")
+ return "OK",200
+ elif "text/html" == ftype:
+ print(f"DEBUG(bws) still need support to parse text/html \"{fstring}\"")
+ else:
+ print(f"DEBUG(bws) e/n/ d&d: Unsupported type as of yet: \"{ftype}\" string \"{fstring}\"")
+ return f"Unsupported type as of yet: \"{ftype}\" string \"{fstring}\"",400
+ else: # not drag-and-drop
+ print(f"DEBUG(BWS): Got a POST for /edit/")
+ for i in f:
+ print(f"DEBUG(bws): Form[\"{i}\"]: \"{f[i]}\"")
+ # validate required fields
+ for word in ['url']:
+ if word not in f:
+ return f"Bad request: needs attribute \"{word}\".",400
+ # checkboxes are stupid, and will appear in the list as false, if the checkbox is checked. If the checkbox is unchecked, the attribute simply will be absent in the POST.
+ try: ftitle = f['title']
+ except: ftitle = n['title']
+ try: ftags = f['tags'].split(','),
+ except: ftags = n['tags']
+ if tuple == type(ftags): ftags=ftags[0]
+ new_ftags = []
+ for i in ftags:
+ if str(i) != "":
+ new_ftags.append(i)
+ ftags=new_ftags
+ try: fnotes = f['notes']
+ except: fnotes = n['notes']
+ try: fts = f['timestamp']
+ except: fts = n['timestamp']
+ try:
+ forder_id = f['order_id']
+ print(f"DEBUG(bws) edit: forder_id=f['order_id']={forder_id}")
+ except:
+ forder_id = n['order_id']
+ print(f"DEBUG(bws) edit: forder_id=n['order_id']={forder_id}")
+ faoint = False
+ if 'always_open_in_new_tab' in f:
+ print(f"DEBUG(bws): always_open_in_new_tab was in the form, so setting it to True")
+ faoint = True
+ print(f"DEBUG(bws): ftags is {ftags}")
+ if "new" == bid:
+ if "" == fts: fts = n['timestamp']
+ print(f"DEBUG(bws): making new bookmark")
+ b = bws_db.Bookmark(
+ source = app.config['database'],
+ url = f['url'],
+ title = ftitle,
+ uid = loggedin_uid,
+ tags = ftags,
+ notes = fnotes,
+ iid = 0,
+ timestamp = fts,
+ order_id = forder_id,
+ always_open_in_new_tab = faoint
+ )
+ if -1 == b.bid or "-1" == b.bid:
+ return "Something failed on server side.",500
+ else:
+ #return f"The server returned bid {b.bid}",201
+ return f"Added bookmark id {b.bid}!<p><meta http-equiv=\"Refresh\" content=\"2; url={url_for('view')}\">Returning momentarily...",201
+ else:
+ if allowed:
+ # POST, allowed, but not new, so just update.
+ message = ""
+ print(f"DEBUG(bws) edit: updating bookmark {b.bid}")
+ if f['url'] != b.url:
+ b.url = f['url']
+ message += "<li>url</li>"
+ if ftitle != b.title:
+ b.title = ftitle
+ message += "<li>title</li>"
+ if str(ftags) != str(b.tags):
+ b.tags = ftags
+ message += "<li>tags</li>"
+ if fnotes != b.notes:
+ b.notes = fnotes
+ message += "<li>notes</li>"
+ if str(fts) != str(b.timestamp):
+ b.timestamp = fts
+ message += "<li>timestamp</li>"
+ if str(forder_id) != str(b.order_id):
+ b.order_id = forder_id
+ message += "<li>order_id</li>"
+ if str(faoint) != str(b.always_open_in_new_tab):
+ b.always_open_in_new_tab = faoint
+ message += "<li>always_open_in_new_tab</li>"
+ if "" != message:
+ b.update()
+ message = "Bookmark attributes updated:<ul>" + message + "</ul>"
+ message += f"<meta http-equiv=\"Refresh\" content=\"2; url={url_for('view')}\">Returning momentarily..."
+ return message,200
+ else:
+ # no changes
+ #message += "No changes"
+ message += f"<meta http-equiv=\"Refresh\" content=\"2; url={url_for('view')}\">Returning momentarily..."
+ return message,200
+ else:
+ # POST, not allowed
+ return f"Bad request.<p><meta http-equiv=\"Refresh\" content=\"2; url={url_for('view')}\">Returning momentarily...",401
+ return f"Bad request.<p><meta http-equiv=\"Refresh\" content=\"2; url={url_for('view')}\">Returning momentarily...",400
+
+# because html forms do not support http DELETE
+@app.route("/delete/",defaults={'bid':-1},methods=['POST'])
+@app.route("/delete/<bid>/",methods=['GET','POST'])
+@requires_session
+@requires_bid_access
+def delete(user=None,groups=None,allowed=False,bid="",loggedin_uid=-1,busername="",b=None):
+ print(f"DEBUG(bws) delete: got request to delete {bid} from loggedin_uid {user}, with allowed={allowed}")
+ if allowed:
+ text = str(b)
+ response = b.delete()
+ if 1 == response:
+ return f"Deleted {escape(text)}.<p><meta http-equiv=\"Refresh\" content=\"2; url={url_for('view')}\">Returning momentarily...",200
+ else:
+ return f"Got some other response, {response}.",500
+ else:
+ return f"Bad request.<p><meta http-equiv=\"Refresh\" content=\"2; url={url_for('view')}\">Returning momentarily...",401
+
+#############################
+# xhr requests
+
+@app.route("/view/list/<username>/")
+@requires_session
+def view_list_user(user=None,groups=None,username=""):
+ if "users" == username:
+ if 'Accept' in request.headers and 'application/json' not in request.headers['Accept']:
+ return "Only json is implemented for /view/list/users/",400
+ user_list = bws_db.get_all_users(app.config['database'])
+ print(f"DEBUG(bws) view_list_user: sending json user_list")
+ return jsonify(json.loads(json.dumps(user_list,cls=bws_db.BookmarkEncoder)))
+ else:
+ print(f"Request, user {user} in groups {groups}")
+ if user == username:
+ #print(f"User requesting his own bookmarks")
+ pass
+ elif app.config['ADMIN_GROUP'] in groups:
+ #print(f"Admin user requesting bookmarks for user {username}")
+ pass
+ else:
+ return Response(f"Wrong user",401)
+ a = bws_db.get_all_bookmarks(app.config['database'],username)
+ if 'Accept' in request.headers and 'application/json' in request.headers['Accept']:
+ print(f"DEBUG(bws): Returning user {username} bookmarks as json")
+ # I do not know how to jsonify with cls=bws_db.BookmarkEncoder, so I just used
+ # this back-and-forth thing
+ return jsonify(
+ json.loads(json.dumps(a,cls=bws_db.BookmarkEncoder)),
+ edit_url = url_for('edit')
+ )
+ else:
+ print(f"DEBUG(bws) view_list_user: Returning user {username} bookmarks as html")
+ return render_template("view_list.html",
+ bm_list = a
+ )
+
+@app.route("/view/set/<item>/<value>/", methods=['POST'])
+@requires_session
+def set_computername(user=None,groups=None,item="",value=""):
+ print(f"DEBUG(bws): Setting session {session} item {item} to value \"{value}\".")
+ if item in ['computername','title']:
+ session[item] = value
+ return "OK",200
+ else:
+ return "bad item",400
+
+## LOGIN FUNCTIONS
+@app.route("/login/new")
+@app.route("/login/new/")
+def login_new():
+ """Force the user to authenticate again. This is not about creating a new user."""
+ return redirect(url_for("login", new=""))
+
+@app.route("/login/", methods=['POST','GET'])
+def login(user="None"):
+ if request.method == "GET":
+ if 'user' in session and request.cookies.get('user') == session['user'] and (not 'new' in request.args):
+ return redirect(url_for("view"))
+ auth_header = request.headers.get("Authorization")
+ if auth_header:
+ print(f"DEBUG(bws): auth_header provided, but kerberos was removed... WHAT DO I DO NOW?!")
+ #if "negotiate" in auth_header:
+ # # assume we are already trying to log in with kerberos
+ # return redirect(url_for("login_kerberos"))
+ # default, show login form
+ return redirect(url_for("login_form"))
+ elif request.method == "POST":
+ if request.authorization:
+ return redirect(url_for("login_basic"),code=307)
+ return handle_login_ldap_from_non_ldap(request)
+
+def get_next_ldap_server(app):
+ # on first ldap_login attempt, cache this lookup result:
+ if 'LDAP_HOSTS' not in app.config:
+ this_domain = urlparse(app.config['LDAP_URI']).hostname
+ app.config['LDAP_HOSTS'] = session_ldap.list_ldap_servers_for_domain(this_domain)
+ else:
+ # rotate them! So every ldap_login attempt will use the next ldap server in the list.
+ this_list = app.config['LDAP_HOSTS']
+ a = this_list[0]
+ this_list.append(a)
+ this_list.pop(0)
+ app.config['LDAP_HOSTS'] = this_list
+ # construct a new, full uri.
+ this_netloc = app.config['LDAP_HOSTS'][0]
+ up = urlparse(app.config['LDAP_URI'])
+ if up.port:
+ this_netloc += f":{up.port}"
+ this_uri = up._replace(netloc=this_netloc).geturl()
+ return this_uri
+
+def ldap_login(username,password):
+ #print(f"DEBUG(bws): Trying user {username} with pw '{password}'")
+ this_uri = get_next_ldap_server(app)
+ # Perform the ldap interactions
+ user = session_ldap.authenticated_user(
+ server_uri=this_uri,
+ user_dn=username,
+ password=password
+ )
+ if user:
+ return user
+ else:
+ return False
+ return False
+
+@app.route("/login/ldap", methods=['POST','GET'])
+@app.route("/login/ldap/", methods=['POST','GET'])
+@requires_authn_ldap
+def login_ldap(user,groups=[]):
+ resp = Response(f'<meta http-equiv="Refresh" content="1; url={url_for("view")}">success with ldap')
+ resp.set_cookie('type',"ldap")
+ resp = login_generic(session,resp,user,groups)
+ return resp
+
+def login_generic(session,resp,user,groups=[]):
+ resp.set_cookie('user',user)
+ end_time = datetime.datetime.now(datetime.timezone.utc) + app.permanent_session_lifetime
+ end_time_str = datetime.datetime.strftime(end_time,"%FT%TZ")
+ resp.set_cookie('timestamp',end_time_str)
+ session.permanent = True
+ session['user']=user
+ session['end_time'] = end_time_str
+ session['groups'] = groups
+ session.modified = True
+ return resp
+
+@app.route("/login/form", methods=['POST','GET'])
+@app.route("/login/form/", methods=['POST','GET'])
+def login_form():
+ if request.method == "GET":
+ options = {
+ "ldap": "ldap",
+ "other": "other"
+ }
+ return render_template("login_form.html",
+ login_url = url_for("login")
+ )
+ #options=options
+ else:
+ # assume it is a POST
+ return redirect(url_for("login_ldap"), code=307)
+
+def handle_login_ldap_from_non_ldap(request):
+ # set default logintype for user
+ logintype = "ldap"
+ # redirect to whichever option was chosen in the drop-down
+ if 'logintype' in request.form:
+ logintype = request.form['logintype']
+ if "ldap" == logintype:
+ return redirect(url_for("login_ldap"), code=307)
+ else:
+ return f"Authentication method {logintype} not supported yet.",400
+
+@app.route("/logout")
+@app.route("/logout/")
+def logout():
+ resp = Response(f'<meta http-equiv="Refresh" content="1; url={url_for("index")}">logged out')
+ # not documented but is found on the Internet in a few random places:
+ session.clear()
+ resp.set_cookie('user','',expires=0)
+ resp.set_cookie('type','',expires=0)
+ resp.set_cookie('session','',expires=0)
+ resp.set_cookie('timestamp','',expires=0)
+ return resp
+
+@app.route("/login/basic",methods=['POST','GET'])
+@app.route("/login/basic/",methods=['POST','GET'])
+def login_basic():
+ if not request.authorization:
+ return Response(f"Please provide username and password.",401,{'WWW-Authenticate': 'Basic'})
+ if 'username' not in request.authorization:
+ return Response(f"No username provided.",401)
+ if 'password' not in request.authorization:
+ return Response(f"No password provided.",401)
+ username = request.authorization.username
+ pw = request.authorization.password
+ form={'username':username,'password':pw}
+ session['formdata'] = form
+ session.modified = True
+ return redirect(url_for("login_ldap"),code=307)
+
+@app.route("/settings/", methods=['GET','POST'])
+@requires_session
+def settings(user,groups):
+ print(f"DEBUG(bws): visit settings page as user {user}")
+ print(f"DEBUG(bws): with groups {groups}")
+ #if "admins" not in groups:
+ #return Response(f'<h1>Not Found</h1><p>What you were looking for is just not there.<p><a href="{ url_for("index") }">Start over</a>', 404)
+ #return Response(f'<h1>Not Found</h1><p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>', 404)
+ # return Response(f'<h1>Not Authorized</h1><p>You are not authorized to access this page.</p>', 403)
+ if False:
+ a = False
+ else:
+ if request.method == "GET":
+ s_cn = ""
+ if 'computername' in session:
+ s_cn = session['computername']
+ s_t = "Bookmark Web Service"
+ if 'title' in session:
+ s_t = session['title']
+ return render_template(
+ 'settings.html',
+ ldap_uri=app.config['LDAP_URI'],
+ session_computername = s_cn,
+ session_title = s_t
+ )
+ elif request.method == "POST":
+ form = request.form
+ print(f"Form: {form}")
+ message = ""
+ if 'computername' not in form and 'title' not in form:
+ return Response("Invalid input.", 400)
+ else:
+ s_cn = ""
+ if 'computername' in session:
+ s_cn = session['computername']
+ s_t = ""
+ if 'title' in session:
+ s_t = session['title']
+ if 'computername' in form and form['computername'] != s_cn:
+ session['computername'] = form['computername']
+ message += "<li>computer name</li>"
+ session.modified = True
+ if 'title' in form and form['title'] != s_t:
+ session['title'] = form['title']
+ message += "<li>Tab title</li>"
+ session.modified = True
+ if "" != message:
+ message = "Settings updated:<ul>" + message + "</ul>"
+ #message += f"<form action='{url_for('view')}' method='get'><input type='submit' value='Return to bookmarks'/></form>"
+ pp = url_for("view")
+ message += f"<meta http-equiv=\"Refresh\" content=\"2; url={url_for('view')}\">Returning momentarily..."
+ message += f"""
+<script type="text/javascript">
+console.log("Setting item to ");
+console.log("{session['computername']}");
+localStorage.setItem('computername',"{session['computername']}");
+localStorage.setItem('title',"{session['title']}");
+</script>
+"""
+ return Response(message, 200)
+
+## This bumps the session lifetime to two minutes farther out from each web request with this session.
+#@app.before_request
+#def make_session_permanent():
+# session.permanent = True
+# session['end_time'] = datetime.datetime.now()+app.permanent_session_lifetime
+
+if __name__ == '__main__':
+ print("should listen to ",app.config['LISTEN_HOST'])
+ app.run(
+ host=app.config['LISTEN_HOST'],
+ port=app.config['LISTEN_PORT'],
+ debug=app.config['DEBUG']
+ )
bgstack15