diff options
author | B. Stack <bgstack15@gmail.com> | 2022-01-27 09:51:44 -0500 |
---|---|---|
committer | B. Stack <bgstack15@gmail.com> | 2022-01-27 09:51:44 -0500 |
commit | adfc6caaedefaeea913a7f5c4ad7781bb629ecba (patch) | |
tree | 4255fe3d57adaa4a0af8f9a93c0347afa70ea01f /bws.py | |
download | bws-master.tar.gz bws-master.tar.bz2 bws-master.zip |
Diffstat (limited to 'bws.py')
-rwxr-xr-x | bws.py | 745 |
1 files changed, 745 insertions, 0 deletions
@@ -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'] + ) |