From f8fbefba935920d9deeb58a0fa66f2a0daf2261b Mon Sep 17 00:00:00 2001 From: Alex Davies Date: Wed, 7 Apr 2021 09:31:52 -0300 Subject: [PATCH 01/10] Added .drone.yml and updated docker-compose file. --- .drone.yml | 9 +++++++++ docker-compose.dev.yaml | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .drone.yml diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..4d226a8 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,9 @@ +kind: pipeline +name: default + +steps: +- name: docker + image: plugins/docker + settings: + repo: glauth-ui + auto_tag: true diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index d532deb..cbd3e65 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -21,9 +21,9 @@ services: glauth: image: glauth/glauth container_name: glauth-dev - command: glauth -c /config/ + command: /app/glauth -c /config/config.cfg volumes: - - 'glauth:/config/config.cfg' + - 'glauth:/config/' volumes: glauth: From 6909b23edecafad4d1a3cb80960137e00c8c4def Mon Sep 17 00:00:00 2001 From: Alex Davies Date: Wed, 7 Apr 2021 10:19:31 -0300 Subject: [PATCH 02/10] Bump readme to test drone. --- README.md | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/README.md b/README.md index 2dd89f3..965b5da 100644 --- a/README.md +++ b/README.md @@ -27,50 +27,16 @@ Missing features: The best installation method atm is to build the docker image with the included Dockerfile. -1. Clone Repository ``` git clone https://github.com/sonicnkt/glauth-ui.git glauth-ui +docker-compose -f docker-compose.dev.yaml up -d --build #Buld and deploy with docker compose ``` -2. Run docker build -``` -cd glauth-ui -docker build -t glauthui:latest . - -``` -3. Create container - -`docker-compose.yaml` -``` -version: '3.8' -services: - glauthui: - image: glauthui:latest - container_name: glauthui - restart: unless-stopped - ports: - - 80:5000 - volumes: - # Mount Folder that contains DB and config file outside the container - - './docker-data:/home/ldap/db' - environment: - - SECRET_KEY=mysuperlongsecretkeythatnobodywillguess - # MAIL CONFIG - - MAIL_SERVER=mail.example.com - - MAIL_PORT=587 - - MAIL_USE_TLS=1 - - MAIL_USERNAME=username - - MAIL_PASSWORD=password - - MAIL_ADMIN=admin@example.com -``` -`docker-compose up #-d` On first startup (or if DB is empty) a sample database will be created with 2 users and 4 groups. Use the username "j_doe" and password "dogood" to login and have access to the administration interface. This should be run behind a reverse proxy like nginx that handles https! -4. Point glauth to the config.cfg created by glauth-ui - ---- ### Environment Variables: From 53fdf694cd215df68901dc8c0454e4473b378d81 Mon Sep 17 00:00:00 2001 From: Alex Davies Date: Wed, 7 Apr 2021 17:41:39 -0300 Subject: [PATCH 03/10] Initial work on forwardauth module --- .drone.yml | 6 ++- app/routes.py | 7 +++- examples/docker-compose.forwardauth.yaml | 52 ++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 examples/docker-compose.forwardauth.yaml diff --git a/.drone.yml b/.drone.yml index 4d226a8..d18b1ba 100644 --- a/.drone.yml +++ b/.drone.yml @@ -5,5 +5,9 @@ steps: - name: docker image: plugins/docker settings: - repo: glauth-ui + repo: traverseda/glauth-ui auto_tag: true + username: + from_secret: dockerhub_user + password: + from_secret: dockerhub_pass diff --git a/app/routes.py b/app/routes.py index e5a4c69..6c129b8 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,4 +1,5 @@ from flask import render_template, flash, redirect, url_for, request, abort +import flask from app import app, db from app.forms import LoginForm, EditProfileForm, ChangePasswordForm from app.forms import ResetPasswordRequestForm, ResetPasswordForm, NewAccountForm @@ -142,4 +143,8 @@ def new_account(token): flash('Your password has been set, please login.') return redirect(url_for('login')) fullname = '{}'.format(user.givenname + ' ' + user.surname) - return render_template('new_account.html', form=form, fullname=fullname) \ No newline at end of file + return render_template('new_account.html', form=form, fullname=fullname) + +@app.route('/forward_auth/', methods=['GET', 'POST']) +def forward_auth(): + return flask.Response(status=201) diff --git a/examples/docker-compose.forwardauth.yaml b/examples/docker-compose.forwardauth.yaml new file mode 100644 index 0000000..b1ce458 --- /dev/null +++ b/examples/docker-compose.forwardauth.yaml @@ -0,0 +1,52 @@ +version: '3.7' + +services: + traefik: + image: traefik + command: > + --log.level=DEBUG + --api.insecure=true + --providers.docker=true + --providers.docker.exposedbydefault=false + --entrypoints.web.address=:80 + ports: + - "80:80" + - "8080:8080" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + + whoami: + image: "traefik/whoami" + labels: + traefik.enable: true + traefik.http.routers.whoami.rule: Host(`whoami.localhost`) + traefik.http.services.whoami.loadbalancer.server.port: 80 + traefik.http.routers.whoami.entrypoints: web + traefik.http.routers.whoami.middlewares: traefik-forward-auth + + glauth-ui: + build: ../ + restart: unless-stopped + volumes: + # Mount Folder that contains DB and config file outside the container + - 'glauth:/home/ldap/db' + environment: + - SECRET_KEY=mysuperlongsecretkeythatnobodywillguess + labels: + traefik.enable: true + traefik.http.routers.auth.rule: Host(`auth.localhost`) + traefik.http.services.auth.loadbalancer.server.port: 5000 + traefik.http.routers.auth.entrypoints: web + traefik.http.middlewares.traefik-forward-auth.forwardauth.address: http://glauth-ui:5000/forward_auth/ + + glauth: + image: glauth/glauth + ports: + - "389:389" + command: /app/glauth -c /config/config.cfg + volumes: + - 'glauth:/config/' + +volumes: + glauth: + From 0727517cfb515477e8eddadb96f3e7d017682a0a Mon Sep 17 00:00:00 2001 From: Alex Davies Date: Sun, 18 Apr 2021 11:34:53 -0300 Subject: [PATCH 04/10] Can't read cookies from remote authserver --- app/__init__.py | 33 ++++++++++++++++--- app/forms.py | 4 +-- app/models.py | 25 ++++++++++++++- app/routes.py | 41 ++++++++++++++++++++---- app/static/htmx.min.js | 1 + app/templates/base.html | 4 +-- app/templates/forward_auth.html | 20 ++++++++++++ app/templates/login.html | 11 +++++-- config.py | 6 +++- examples/docker-compose.forwardauth.yaml | 4 ++- 10 files changed, 128 insertions(+), 21 deletions(-) create mode 100644 app/static/htmx.min.js create mode 100644 app/templates/forward_auth.html diff --git a/app/__init__.py b/app/__init__.py index fdb3c16..1f748a7 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,4 @@ -from flask import Flask +from flask import Flask, request import logging from logging.handlers import SMTPHandler, RotatingFileHandler # https://bootstrap-flask.readthedocs.io @@ -21,7 +21,6 @@ mail = Mail(app) bootstrap = Bootstrap(app) - if not app.debug: if app.config['MAIL_SERVER']: auth = None @@ -48,7 +47,7 @@ app.logger.addHandler(file_handler) app.logger.setLevel(logging.INFO) - app.logger.info('GLauth Management UI') + app.logger.info('GLauth Management UI') # Security - Generate GLAUTH compatible password hashs def generate_password_hash(password): @@ -65,8 +64,32 @@ def check_password_hash(hash, password): def createdbdata(): """Creating example db""" if models.User.query.count() == 0: - app.logger.info('No Data in DB, creating example dataset') + app.logger.info('No Data in DB, creating example dataset') click.echo('Creating Example DB') models.create_basic_db() else: - app.logger.info('Data in DB allready exists.') \ No newline at end of file + app.logger.info('Data in DB allready exists.') + +@app.after_request +def cors_headers(response): + """Automatically add CORS headers to every request. This + should be tightened up. + """ + response.headers["access-control-allow-origin"] = request.headers.get('origin') + response.headers.add('Access-Control-Allow-Headers', "hx-current-url,hx-request") + response.headers.add('Access-Control-Allow-Credentials', 'true') + return response + +import flask +class DomainSessionInterface(flask.sessions.SecureCookieSessionInterface): + def get_cookie_domain(self, app): + origin=request.headers.get('Origin',Config.SERVER_NAME).split("://")[-1] + if origin=="localhost": + origin="" + return origin + + def get_cookie_httponly(self, app): + return False + +app.session_interface = DomainSessionInterface() + diff --git a/app/forms.py b/app/forms.py index 9cc9fd9..0613188 100644 --- a/app/forms.py +++ b/app/forms.py @@ -1,5 +1,5 @@ from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, BooleanField, SubmitField +from wtforms import StringField, PasswordField, BooleanField, SubmitField, HiddenField from wtforms.validators import DataRequired, ValidationError, Email, EqualTo, Length from app.models import User from app import check_password_hash @@ -73,4 +73,4 @@ class ResetPasswordForm(FlaskForm): class ResetPasswordRequestForm(FlaskForm): email = StringField('Email', validators=[DataRequired(), Email()]) - submit = SubmitField('Request Password Reset') \ No newline at end of file + submit = SubmitField('Request Password Reset') diff --git a/app/models.py b/app/models.py index 266508d..f610b0d 100644 --- a/app/models.py +++ b/app/models.py @@ -63,6 +63,17 @@ class User(UserMixin, db.Model): othergroups = db.relationship('Group', secondary=othergroups_users, backref=db.backref('o_users', lazy='dynamic')) + def in_groups(self,*groups): + """Check is the user is in a group + """ + #ToDo: Does this work recursivly with nested groups? + if self.primarygroup.name in groups: + return True + for group in self.othergroups: + if group.name in allowed_groups: + return True + return False + @property def is_admin(self): # checks if the name of any group matches the configured ADMIN_GROUP name @@ -115,6 +126,18 @@ def verify_new_account_token(token): def load_user(id): return User.query.get(int(id)) +import base64 + +@login.request_loader +def http_basic_auth(request): + authstr = request.headers.get("Authorization") + if authstr: + authstr=authstr.removeprefix("Basic ").lstrip().rstrip() + username, password = base64.b64decode(authstr).split(":") + user = User.query.filter_by(username=username).first() + if user.check_password(password): + return user + return None def create_basic_db(): settings = Settings(debug=True, ldap_enabled=True, ldap_listen='0.0.0.0:389', basedn='dc=glauth-example,dc=com') @@ -140,4 +163,4 @@ def create_basic_db(): db.session.add(u1) db.session.add(u2) - db.session.commit() \ No newline at end of file + db.session.commit() diff --git a/app/routes.py b/app/routes.py index 6c129b8..75a4454 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,5 +1,4 @@ from flask import render_template, flash, redirect, url_for, request, abort -import flask from app import app, db from app.forms import LoginForm, EditProfileForm, ChangePasswordForm from app.forms import ResetPasswordRequestForm, ResetPasswordForm, NewAccountForm @@ -35,8 +34,8 @@ def testmail(): def login(): if current_user.is_authenticated: return redirect(url_for('index')) - - form = LoginForm() + + form = LoginForm() if form.validate_on_submit(): #flash('Login requested for user {} with pw {}, remember_me={}'.format( # form.username.data, form.password.data, form.remember_me.data)) @@ -51,6 +50,7 @@ def login(): flash('Account not eligable to login.') return redirect(url_for('login')) login_user(user, remember=form.remember_me.data) + next_page = request.args.get('next') if not next_page or url_parse(next_page).netloc != '': return redirect(url_for('index')) @@ -145,6 +145,35 @@ def new_account(token): fullname = '{}'.format(user.givenname + ' ' + user.surname) return render_template('new_account.html', form=form, fullname=fullname) -@app.route('/forward_auth/', methods=['GET', 'POST']) -def forward_auth(): - return flask.Response(status=201) +from flask_wtf import csrf + +@app.route('/forward_auth/', methods=['GET', 'POST'], subdomain="") +def forward_auth(subdomain): + protocol = request.headers.get('X-Forwarded-Proto') + host = request.headers.get('X-Forwarded-Host') + uri = request.headers.get('X-Forwarded-Uri') + origin = protocol+"://"+host+uri + method = request.headers.get('X-Forwarded-Method') + + #Whitelist based on IP address, wish there was some way to whitelist based on + # docker service name. Maybe traefik will do something about it. + #ToDo: If somone wants maybe add IP range whitelisting? I just did this because it + # was very easy to do, and someone might find it useful. + sourceIp=request.headers.get('X-Forwarded-For',None) + if sourceIp in request.args.getlist('ip'): + return "", 201 + + if current_user.is_anonymous: + return render_template('forward_auth.html'), 401 + loginpage = login(internal_redirect=origin) + if type(loginpage)==str: + return loginpage, 401 + return loginpage + + #Simple no DB based group lookup, configurable via client env variable + #Makes sure the user is in one of the groups passed as a `group` querystring arg. + allowed_groups = request.args.getlist('group') + if current_user.in_groups(*allowed_groups): + return "", 201 + + return abort(403) diff --git a/app/static/htmx.min.js b/app/static/htmx.min.js new file mode 100644 index 0000000..8ac2528 --- /dev/null +++ b/app/static/htmx.min.js @@ -0,0 +1 @@ +(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else{e.htmx=t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var v={onLoad:b,process:rt,on:z,off:V,trigger:lt,ajax:$t,find:S,findAll:E,closest:T,values:function(e,t){var r=Rt(e,t||"post");return r.values},remove:C,addClass:O,removeClass:A,toggleClass:L,takeClass:R,defineExtension:Kt,removeExtension:Qt,logAll:w,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:100,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,attributesToSettle:["class","style","width","height"],wsReconnectDelay:"full-jitter",disableSelector:"[hx-disable], [data-hx-disable]"},parseInterval:f,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){return new WebSocket(e,[])}};var t=["get","post","put","delete","patch"];var n=t.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function f(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}return parseFloat(e)||undefined}function l(e,t){return e.getAttribute&&e.getAttribute(t)}function a(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function I(e,t){return l(e,t)||l(e,"data-"+t)}function c(e){return e.parentElement}function M(){return document}function h(e,t){if(t(e)){return e}else if(c(e)){return h(c(e),t)}else{return null}}function k(e,t){var r=null;h(e,function(e){return r=I(e,t)});return r}function d(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function r(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=M().createDocumentFragment()}return i}function u(e){var t=r(e);switch(t){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i(""+e+"
",1);case"col":return i(""+e+"
",2);case"tr":return i(""+e+"
",2);case"td":case"th":return i(""+e+"
",3);case"script":return i("
"+e+"
",1);default:return i(e,0)}}function D(e){if(e){e()}}function o(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function s(e){return o(e,"Function")}function g(e){return o(e,"Object")}function F(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function p(e){var t=[];if(e){for(var r=0;r=0}function P(e){return M().body.contains(e)}function y(e){return e.trim().split(/\s+/)}function U(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function x(e){try{return JSON.parse(e)}catch(e){ut(e);return null}}function e(e){return Ut(M().body,function(){return eval(e)})}function b(t){var e=v.on("htmx:load",function(e){t(e.detail.elt)});return e}function w(){v.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function S(e,t){if(t){return e.querySelector(t)}else{return S(M(),e)}}function E(e,t){if(t){return e.querySelectorAll(t)}else{return E(M(),e)}}function C(e,t){e=N(e);if(t){setTimeout(function(){C(e)},t)}else{e.parentElement.removeChild(e)}}function O(e,t,r){e=N(e);if(r){setTimeout(function(){O(e,t)},r)}else{e.classList.add(t)}}function A(e,t,r){e=N(e);if(r){setTimeout(function(){A(e,t)},r)}else{e.classList.remove(t)}}function L(e,t){e=N(e);e.classList.toggle(t)}function R(e,t){e=N(e);X(e.parentElement.children,function(e){A(e,t)});O(e,t)}function T(e,t){e=N(e);if(e.closest){return e.closest(t)}else{do{if(e==null||d(e,t)){return e}}while(e=e&&c(e))}}function q(e,t){if(t.indexOf("closest ")===0){return[T(e,t.substr(8))]}else if(t.indexOf("find ")===0){return[S(e,t.substr(5))]}else{return M().querySelectorAll(t)}}function H(e,t){return q(e,t)[0]}function N(e){if(o(e,"String")){return S(e)}else{return e}}function j(e,t,r){if(s(t)){return{target:M().body,event:e,listener:t}}else{return{target:N(e),event:t,listener:r}}}function z(t,r,n){tr(function(){var e=j(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=s(r);return e?r:n}function V(t,r,n){tr(function(){var e=j(t,r,n);e.target.removeEventListener(e.event,e.listener)});return s(r)?r:n}function W(e){var t=h(e,function(e){return I(e,"hx-target")!==null});if(t){var r=I(t,"hx-target");if(r==="this"){return t}else{return H(e,r)}}else{var n=F(e);if(n.boosted){return M().body}else{return e}}}function _(e){var t=v.config.attributesToSettle;for(var r=0;r0){i=e.substr(0,e.indexOf(":"));n=e.substr(e.indexOf(":")+1,e.length)}else{i=e}var o=M().querySelector(n);if(o){var a;a=M().createDocumentFragment();a.appendChild(t);if(!$(i,o)){a=t}le(i,o,o,a,r)}else{t.parentNode.removeChild(t);ot(M().body,"htmx:oobErrorNoTarget",{content:t})}return e}function Z(e,r){X(E(e,"[hx-swap-oob], [data-hx-swap-oob]"),function(e){var t=I(e,"hx-swap-oob");if(t!=null){J(t,e,r)}})}function G(e){X(E(e,"[hx-preserve], [data-hx-preserve]"),function(e){var t=I(e,"id");var r=M().getElementById(t);if(r!=null){e.parentNode.replaceChild(r,e)}})}function Y(n,e,i){X(e.querySelectorAll("[id]"),function(e){if(e.id&&e.id.length>0){var t=n.querySelector(e.tagName+"[id='"+e.id+"']");if(t&&t!==n){var r=e.cloneNode();B(e,t);i.tasks.push(function(){B(e,r)})}}})}function K(e){return function(){rt(e);Ke(e);Q(e);lt(e,"htmx:load")}}function Q(e){var t="[autofocus]";var r=d(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function ee(e,t,r,n){Y(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(K(i))}}}function te(t){var e=F(t);if(e.webSocket){e.webSocket.close()}if(e.sseEventSource){e.sseEventSource.close()}if(e.listenerInfos){X(e.listenerInfos,function(e){if(t!==e.on){e.on.removeEventListener(e.trigger,e.listener)}})}if(t.children){X(t.children,function(e){te(e)})}}function re(e,t,r){if(e.tagName==="BODY"){return se(e,t)}else{var n=e.previousSibling;ee(c(e),e,t,r);if(n==null){var i=c(e).firstChild}else{var i=n.nextSibling}F(e).replacedWith=i;r.elts=[];while(i&&i!==e){if(i.nodeType===Node.ELEMENT_NODE){r.elts.push(i)}i=i.nextElementSibling}te(e);c(e).removeChild(e)}}function ne(e,t,r){return ee(e,e.firstChild,t,r)}function ie(e,t,r){return ee(c(e),e,t,r)}function oe(e,t,r){return ee(e,null,t,r)}function ae(e,t,r){return ee(c(e),e.nextSibling,t,r)}function se(e,t,r){var n=e.firstChild;ee(e,n,t,r);if(n){while(n.nextSibling){te(n.nextSibling);e.removeChild(n.nextSibling)}te(n);e.removeChild(n)}}function ue(e,t){var r=k(e,"hx-select");if(r){var n=M().createDocumentFragment();X(t.querySelectorAll(r),function(e){n.appendChild(e)});t=n}return t}function le(e,t,r,n,i){switch(e){case"none":return;case"outerHTML":re(r,n,i);return;case"afterbegin":ne(r,n,i);return;case"beforebegin":ie(r,n,i);return;case"beforeend":oe(r,n,i);return;case"afterend":ae(r,n,i);return;default:var o=er(t);for(var a=0;a([\s\S]+?)<\/title>/im;function ce(e){var t=fe.exec(e);if(t){return t[1]}}function he(e,t,r,n,i){var o=ce(n);if(o){var a=S("title");if(a){a.innerHTML=o}else{window.document.title=o}}var s=u(n);if(s){Z(s,i);s=ue(r,s);G(s);return le(e,r,t,s,i)}}function de(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=x(n);for(var o in i){if(i.hasOwnProperty(o)){var a=i[o];if(!g(a)){a={value:a}}lt(r,o,a)}}}else{lt(r,n,[])}}var ve=/\s/;var ge=/[\s,]/;var pe=/[_$a-zA-Z]/;var me=/[_$a-zA-Z0-9]/;var ye=['"',"'","/"];var xe=/[^\s]/;function be(e){var t=[];var r=0;while(r0){var a=t[0];if(a==="]"){n--;if(n===0){if(o===null){i=i+"true"}t.shift();i+=")})";try{var s=Ut(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){ot(M().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(a==="["){n++}if(we(a,o,r)){i+="(("+r+"."+a+") ? ("+r+"."+a+") : (window."+a+"))"}else{i=i+a}o=t.shift()}}}function Ee(e,t){var r="";while(e.length>0&&!e[0].match(t)){r+=e.shift()}return r}var Ce="input, textarea, select";function Oe(e){var t=I(e,"hx-trigger");var r=[];if(t){var n=be(t);do{Ee(n,xe);var i=n.length;var o=Ee(n,/[,\[\s]/);if(o!==""){if(o==="every"){var a={trigger:"every"};Ee(n,xe);a.pollInterval=f(Ee(n,ve));r.push(a)}else if(o.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:o.substr(4)})}else{var s={trigger:o};var u=Se(e,n,"event");if(u){s.eventFilter=u}while(n.length>0&&n[0]!==","){Ee(n,xe);var l=n.shift();if(l==="changed"){s.changed=true}else if(l==="once"){s.once=true}else if(l==="consume"){s.consume=true}else if(l==="delay"&&n[0]===":"){n.shift();s.delay=f(Ee(n,ge))}else if(l==="from"&&n[0]===":"){n.shift();s.from=Ee(n,ge)}else if(l==="target"&&n[0]===":"){n.shift();s.target=Ee(n,ge)}else if(l==="throttle"&&n[0]===":"){n.shift();s.throttle=f(Ee(n,ge))}else{ot(e,"htmx:syntax:error",{token:n.shift()})}}r.push(s)}}if(n.length===i){ot(e,"htmx:syntax:error",{token:n.shift()})}Ee(n,xe)}while(n[0]===","&&n.shift())}if(r.length>0){return r}else if(d(e,"form")){return[{trigger:"submit"}]}else if(d(e,Ce)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function Ae(e){F(e).cancelled=true}function Le(e,t,r,n){var i=F(e);i.timeout=setTimeout(function(){if(P(e)&&i.cancelled!==true){Jt(t,r,e);Le(e,t,I(e,"hx-"+t),n)}},n)}function Re(e){return location.hostname===e.hostname&&l(e,"href")&&l(e,"href").indexOf("#")!==0}function Te(t,r,e){if(t.tagName==="A"&&Re(t)||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=l(t,"href")}else{var o=l(t,"method");n=o?o.toLowerCase():"get";i=l(t,"action")}e.forEach(function(e){Ie(t,n,i,r,e,true)})}}function qe(e){return e.tagName==="FORM"||d(e,'input[type="submit"], button')&&T(e,"form")!==null||e.tagName==="A"&&e.href&&(e.getAttribute("href")==="#"||e.getAttribute("href").indexOf("#")!==0)}function He(e,t){return F(e).boosted&&e.tagName==="A"&&t.type==="click"&&t.ctrlKey}function Ne(e,t){var r=e.eventFilter;if(r){try{return r(t)!==true}catch(e){ot(M().body,"htmx:eventFilter:error",{error:e,source:r.source});return true}}return false}function Ie(n,i,o,e,a,s){var u=n;if(a.from){u=S(a.from)}var l=function(e){if(!P(n)){u.removeEventListener(a.trigger,l);return}if(He(n,e)){return}if(s||qe(n)){e.preventDefault()}if(Ne(a,e)){return}var t=F(e);if(t.handledFor==null){t.handledFor=[]}var r=F(n);if(t.handledFor.indexOf(n)<0){t.handledFor.push(n);if(a.consume){e.stopPropagation()}if(a.target&&e.target){if(!d(e.target,a.target)){return}}if(a.once){if(r.triggeredOnce){return}else{r.triggeredOnce=true}}if(a.changed){if(r.lastValue===n.value){return}else{r.lastValue=n.value}}if(r.delayed){clearTimeout(r.delayed)}if(r.throttle){return}if(a.throttle){r.throttle=setTimeout(function(){Jt(i,o,n,e);r.throttle=null},a.throttle)}else if(a.delay){r.delayed=setTimeout(function(){Jt(i,o,n,e)},a.delay)}else{Jt(i,o,n,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:a.trigger,listener:l,on:u});u.addEventListener(a.trigger,l)}var Me=false;var ke=null;function De(){if(!ke){ke=function(){Me=true};window.addEventListener("scroll",ke);setInterval(function(){if(Me){Me=false;X(M().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){Fe(e)})}},200)}}function Fe(e){var t=F(e);if(!t.revealed&&m(e)){t.revealed=true;Jt(t.verb,t.path,e)}}function Xe(e,t,r){var n=y(r);for(var i=0;i0){lt(l,"htmx:validation:halted",i);return}t.send(JSON.stringify(u));if(qe(l)){e.preventDefault()}})}else{ot(l,"htmx:noWebSocketSourceError")}}function ze(e){var t=v.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}ut('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function Ve(e,t,r){var n=y(r);for(var i=0;iv.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){ot(M().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function dt(e){var t=x(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){lt(M().body,"htmx:historyCacheMissLoad",i);var e=u(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=ct();var r=Ft(t);se(t,e,r);mt(r.tasks);ft=n;lt(M().body,"htmx:historyRestore",{path:n})}else{ot(M().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function xt(e){gt();e=e||location.pathname+location.search;var t=dt(e);if(t){var r=u(t.content);var n=ct();var i=Ft(n);se(n,r,i);mt(i.tasks);document.title=t.title;window.scrollTo(0,t.scroll);ft=e;lt(M().body,"htmx:historyRestore",{path:e})}else{if(v.config.refreshOnHistoryMiss){window.location.reload(true)}else{yt(e)}}}function bt(e){var t=k(e,"hx-push-url");return t&&t!=="false"||e.tagName==="A"&&F(e).boosted}function wt(e){var t=k(e,"hx-push-url");return t==="true"||t==="false"?null:t}function St(e){var t=k(e,"hx-indicator");if(t){var r=q(e,t)}else{r=[e]}X(r,function(e){e.classList["add"].call(e.classList,v.config.requestClass)});return r}function Et(e){X(e,function(e){e.classList["remove"].call(e.classList,v.config.requestClass)})}function Ct(e,t){for(var r=0;r=0}function kt(e){var t=k(e,"hx-swap");var r={swapStyle:F(e).boosted?"innerHTML":v.config.defaultSwapStyle,swapDelay:v.config.defaultSwapDelay,settleDelay:v.config.defaultSettleDelay};if(F(e).boosted&&!Mt(e)){r["show"]="top"}if(t){var n=y(t);if(n.length>0){r["swapStyle"]=n[0];for(var i=1;i0){lt(r,"htmx:validation:halted",E);D(o);c();return s}var C=t.split("#");var O=C[0];var A=C[1];if(e==="get"){var L=O;var R=Object.keys(S).length!==0;if(R){if(L.indexOf("?")<0){L+="?"}else{L+="&"}L+=qt(S);if(A){L+="#"+A}}g.open("GET",L,true)}else{g.open(e.toUpperCase(),t,true)}g.overrideMimeType("text/html");for(var T in p){if(p.hasOwnProperty(T)){var q=p[T];Wt(g,T,q)}}var H={xhr:g,target:l,requestConfig:E,pathInfo:{path:t,finalPath:L,anchor:A}};g.onload=function(){try{u(r,H)}catch(e){ot(r,"htmx:onLoadError",U({error:e},H));throw e}finally{Et(N);var e=r;if(!P(r)){e=F(l).replacedWith||l}lt(e,"htmx:afterRequest",H);lt(e,"htmx:afterOnLoad",H);D(o);c()}};g.onerror=function(){Et(N);var e=r;if(!P(r)){e=F(l).replacedWith||l}ot(e,"htmx:afterRequest",H);ot(e,"htmx:sendError",H);D(a);c()};g.onabort=function(){Et(N);var e=r;if(!P(r)){e=F(l).replacedWith||l}ot(e,"htmx:afterRequest",H);ot(e,"htmx:sendAbort",H);D(a);c()};if(!lt(r,"htmx:beforeRequest",H)){D(o);c();return s}var N=St(r);X(["loadstart","loadend","progress","abort"],function(t){X([g,g.upload],function(e){e.addEventListener(t,function(e){lt(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});lt(r,"htmx:beforeSend",H);g.send(e==="get"?null:Dt(g,r,S));return s}function Zt(a,s){var u=s.xhr;var l=s.target;if(!lt(a,"htmx:beforeOnLoad",s))return;if(Bt(u,/HX-Trigger:/i)){de(u,"HX-Trigger",a)}if(Bt(u,/HX-Push:/i)){var f=u.getResponseHeader("HX-Push")}if(Bt(u,/HX-Redirect:/i)){window.location.href=u.getResponseHeader("HX-Redirect");return}if(Bt(u,/HX-Refresh:/i)){if("true"===u.getResponseHeader("HX-Refresh")){location.reload();return}}var c=bt(a)||f;if(u.status>=200&&u.status<400){if(u.status===286){Ae(a)}if(u.status!==204){if(!lt(l,"htmx:beforeSwap",s))return;var h=u.response;st(a,function(e){h=e.transformResponse(h,u,a)});if(c){gt()}var d=kt(a);l.classList.add(v.config.swappingClass);var e=function(){try{var e=document.activeElement;var t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null};var r=Ft(l);he(d.swapStyle,l,a,h,r);if(t.elt&&!P(t.elt)&&t.elt.id){var n=document.getElementById(t.elt.id);if(n){if(t.start&&n.setSelectionRange){n.setSelectionRange(t.start,t.end)}n.focus()}}l.classList.remove(v.config.swappingClass);X(r.elts,function(e){if(e.classList){e.classList.add(v.config.settlingClass)}lt(e,"htmx:afterSwap",s)});if(s.pathInfo.anchor){location.hash=s.pathInfo.anchor}if(Bt(u,/HX-Trigger-After-Swap:/i)){var i=a;if(!P(a)){i=M().body}de(u,"HX-Trigger-After-Swap",i)}var o=function(){X(r.tasks,function(e){e.call()});X(r.elts,function(e){if(e.classList){e.classList.remove(v.config.settlingClass)}lt(e,"htmx:afterSettle",s)});if(c){var e=f||wt(a)||_t(u)||s.pathInfo.finalPath||s.pathInfo.path;pt(e);lt(M().body,"htmx:pushedIntoHistory",{path:e})}Xt(r.elts,d);if(Bt(u,/HX-Trigger-After-Settle:/i)){var t=a;if(!P(a)){t=M().body}de(u,"HX-Trigger-After-Settle",t)}};if(d.settleDelay>0){setTimeout(o,d.settleDelay)}else{o()}}catch(e){ot(a,"htmx:swapError",s);throw e}};if(d.swapDelay>0){setTimeout(e,d.swapDelay)}else{e()}}}else{ot(a,"htmx:responseError",U({error:"Response Status Error Code "+u.status+" from "+s.pathInfo.path},s))}}var Gt={};function Yt(){return{onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Kt(e,t){Gt[e]=U(Yt(),t)}function Qt(e){delete Gt[e]}function er(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=I(e,"hx-ext");if(t){X(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Gt[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return er(c(e),r,n)}function tr(e){if(M().readyState!=="loading"){e()}else{M().addEventListener("DOMContentLoaded",e)}}function rr(){if(v.config.includeIndicatorStyles!==false){M().head.insertAdjacentHTML("beforeend","")}}function nr(){var e=M().querySelector('meta[name="htmx-config"]');if(e){return x(e.content)}else{return null}}function ir(){var e=nr();if(e){v.config=U(v.config,e)}}tr(function(){ir();rr();var e=M().body;rt(e);window.onpopstate=function(e){if(e.state&&e.state.htmx){xt()}};setTimeout(function(){lt(e,"htmx:load",{})},0)});return v}()}); \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index f06fbd8..ec09969 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -5,7 +5,6 @@ - {% block styles %} {{ bootstrap.load_css() }} @@ -30,7 +29,7 @@ {% block scripts %} + {{ bootstrap.load_js() }} {% endblock %} diff --git a/app/templates/forward_auth.html b/app/templates/forward_auth.html new file mode 100644 index 0000000..9e8415d --- /dev/null +++ b/app/templates/forward_auth.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block content %} +
+ Loading login form -- Please ensure javascript is enabled +
+ + + + +{% endblock %} diff --git a/app/templates/login.html b/app/templates/login.html index 548440a..6f72846 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -5,9 +5,14 @@

Please sign in

- {{ render_form(form) }} + {{ render_form(form, action='//'+config.SERVER_NAME+'/login', + render_kw={ + 'hx-post':'//'+config.SERVER_NAME+'/login', + 'hx-target': 'body', + }) + }}
-

Forgot Your Password? Click to Reset It

+

Forgot Your Password? Click to Reset It

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/config.py b/config.py index 795fff4..3d6efc4 100644 --- a/config.py +++ b/config.py @@ -8,6 +8,10 @@ class Config(object): SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' ADMIN_GROUP = os.environ.get('ADMIN_GROUP') or 'glauth_admin' +# SESSION_COOKIE_DOMAIN = False + SESSION_COOKIE_NAME = "glauth-ui-session" + SESSION_COOKIE_SECURE = False + # MAIL Config MAIL_SERVER = os.environ.get('MAIL_SERVER') MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25) @@ -26,4 +30,4 @@ class Config(object): os.path.join(basedir, 'db', 'config.cfg') # FLASK ADMIN STUFF - FLASK_ADMIN_FLUID_LAYOUT = True \ No newline at end of file + FLASK_ADMIN_FLUID_LAYOUT = True diff --git a/examples/docker-compose.forwardauth.yaml b/examples/docker-compose.forwardauth.yaml index b1ce458..9f9e7c2 100644 --- a/examples/docker-compose.forwardauth.yaml +++ b/examples/docker-compose.forwardauth.yaml @@ -31,7 +31,9 @@ services: # Mount Folder that contains DB and config file outside the container - 'glauth:/home/ldap/db' environment: - - SECRET_KEY=mysuperlongsecretkeythatnobodywillguess + SECRET_KEY: mysuperlongsecretkeythatnobodywillguess + BASE_URL: auth.localhost + labels: traefik.enable: true traefik.http.routers.auth.rule: Host(`auth.localhost`) From b42412353224d301011a55c1cbe621c245e73ea0 Mon Sep 17 00:00:00 2001 From: Alex Davies Date: Sat, 1 May 2021 13:52:31 -0300 Subject: [PATCH 05/10] Added authorization header support, removed cookie support --- Dockerfile | 4 +-- app/models.py | 9 +++--- app/routes.py | 38 ++++++++++++++++++++++-- examples/docker-compose.forwardauth.yaml | 2 +- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0198236..938d3f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-alpine +FROM python:3.9-alpine RUN adduser -D ldap @@ -24,4 +24,4 @@ USER ldap VOLUME ["/home/ldap/db"] EXPOSE 5000 -ENTRYPOINT ["./boot.sh"] \ No newline at end of file +ENTRYPOINT ["./boot.sh"] diff --git a/app/models.py b/app/models.py index f610b0d..a12a586 100644 --- a/app/models.py +++ b/app/models.py @@ -63,12 +63,12 @@ class User(UserMixin, db.Model): othergroups = db.relationship('Group', secondary=othergroups_users, backref=db.backref('o_users', lazy='dynamic')) - def in_groups(self,*groups): + def in_groups(self,*allowed_groups): """Check is the user is in a group """ #ToDo: Does this work recursivly with nested groups? - if self.primarygroup.name in groups: - return True + #if self.primarygroup.name in groups: + # return True for group in self.othergroups: if group.name in allowed_groups: return True @@ -133,7 +133,8 @@ def http_basic_auth(request): authstr = request.headers.get("Authorization") if authstr: authstr=authstr.removeprefix("Basic ").lstrip().rstrip() - username, password = base64.b64decode(authstr).split(":") + authbytes = authstr.encode('utf-8') #We need to ensure it's bytes + username, password = base64.b64decode(authbytes).decode("utf-8").split(":") user = User.query.filter_by(username=username).first() if user.check_password(password): return user diff --git a/app/routes.py b/app/routes.py index 75a4454..b3aae24 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,4 +1,4 @@ -from flask import render_template, flash, redirect, url_for, request, abort +from flask import render_template, flash, redirect, url_for, request, abort, Response from app import app, db from app.forms import LoginForm, EditProfileForm, ChangePasswordForm from app.forms import ResetPasswordRequestForm, ResetPasswordForm, NewAccountForm @@ -145,10 +145,42 @@ def new_account(token): fullname = '{}'.format(user.givenname + ' ' + user.surname) return render_template('new_account.html', form=form, fullname=fullname) -from flask_wtf import csrf +import base64 -@app.route('/forward_auth/', methods=['GET', 'POST'], subdomain="") +@app.route('/forward_auth/header/', methods=['GET', 'POST'], subdomain="") def forward_auth(subdomain): + """The actual login is handled by flask_login + """ + protocol = request.headers.get('X-Forwarded-Proto') + host = request.headers.get('X-Forwarded-Host') + uri = request.headers.get('X-Forwarded-Uri') + origin = protocol+"://"+host+uri + method = request.headers.get('X-Forwarded-Method') + + #Whitelist based on IP address + sourceIp=request.headers.get('X-Forwarded-For',None) + if sourceIp in request.args.getlist('ip'): + return "", 201 + + if current_user.is_anonymous: + return Response( + f'Could not verify your access level for that {origin}.\n' + 'You have to login with proper credentials\n', 401, + {'WWW-Authenticate': 'Basic realm="Login Required"'}) + + allowed_groups = request.args.getlist('group') + if current_user.in_groups(*allowed_groups): + return "", 201 + + return abort(403) + +#@app.route('/forward_auth/cookie/', methods=['GET', 'POST'], subdomain="") +def forward_auth(subdomain): + """Unfortunatly implementing the CORS cookies in a clean way behind traefik is a bit beyond + me. There are things traefik could do to make this easier, like allow me to do a post + request to the auth server from behind the proxy, but alas. + """ + raise NotImplementedError protocol = request.headers.get('X-Forwarded-Proto') host = request.headers.get('X-Forwarded-Host') uri = request.headers.get('X-Forwarded-Uri') diff --git a/examples/docker-compose.forwardauth.yaml b/examples/docker-compose.forwardauth.yaml index 9f9e7c2..2f4795f 100644 --- a/examples/docker-compose.forwardauth.yaml +++ b/examples/docker-compose.forwardauth.yaml @@ -39,7 +39,7 @@ services: traefik.http.routers.auth.rule: Host(`auth.localhost`) traefik.http.services.auth.loadbalancer.server.port: 5000 traefik.http.routers.auth.entrypoints: web - traefik.http.middlewares.traefik-forward-auth.forwardauth.address: http://glauth-ui:5000/forward_auth/ + traefik.http.middlewares.traefik-forward-auth.forwardauth.address: http://glauth-ui:5000/forward_auth/header/ glauth: image: glauth/glauth From f6cb9e0489190eece363d64ece9ce8c78b85da9a Mon Sep 17 00:00:00 2001 From: Alex Davies Date: Sat, 1 May 2021 15:16:19 -0300 Subject: [PATCH 06/10] httpbasicauth now works --- app/models.py | 6 +++--- app/routes.py | 2 +- examples/docker-compose.forwardauth.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models.py b/app/models.py index a12a586..5bc11c0 100644 --- a/app/models.py +++ b/app/models.py @@ -66,9 +66,9 @@ class User(UserMixin, db.Model): def in_groups(self,*allowed_groups): """Check is the user is in a group """ - #ToDo: Does this work recursivly with nested groups? - #if self.primarygroup.name in groups: - # return True + primarygroup=Group.query.filter_by(unixid=self.primarygroup).first() + if primarygroup.name in allowed_groups: + return True for group in self.othergroups: if group.name in allowed_groups: return True diff --git a/app/routes.py b/app/routes.py index b3aae24..ac224d3 100644 --- a/app/routes.py +++ b/app/routes.py @@ -208,4 +208,4 @@ def forward_auth(subdomain): if current_user.in_groups(*allowed_groups): return "", 201 - return abort(403) + return abort(401) diff --git a/examples/docker-compose.forwardauth.yaml b/examples/docker-compose.forwardauth.yaml index 2f4795f..f64898e 100644 --- a/examples/docker-compose.forwardauth.yaml +++ b/examples/docker-compose.forwardauth.yaml @@ -39,7 +39,7 @@ services: traefik.http.routers.auth.rule: Host(`auth.localhost`) traefik.http.services.auth.loadbalancer.server.port: 5000 traefik.http.routers.auth.entrypoints: web - traefik.http.middlewares.traefik-forward-auth.forwardauth.address: http://glauth-ui:5000/forward_auth/header/ + traefik.http.middlewares.traefik-forward-auth.forwardauth.address: http://glauth-ui:5000/forward_auth/header/?group=glauth_admin&group=svcaccts glauth: image: glauth/glauth From e674f73a91de176a98e8858786e4c9f0bb03f3fb Mon Sep 17 00:00:00 2001 From: Alex Davies Date: Sat, 1 May 2021 16:14:30 -0300 Subject: [PATCH 07/10] Add support for cidr ip mask based whitelisting --- app/routes.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/routes.py b/app/routes.py index c1f49e8..5ccb8d5 100644 --- a/app/routes.py +++ b/app/routes.py @@ -146,6 +146,7 @@ def new_account(token): return render_template('new_account.html', title='Activate Account', form=form, fullname=fullname) import base64 +from ipaddress import ip_network, ip_address @app.route('/forward_auth/header/', methods=['GET', 'POST'], subdomain="") def forward_auth(subdomain): @@ -157,11 +158,17 @@ def forward_auth(subdomain): origin = protocol+"://"+host+uri method = request.headers.get('X-Forwarded-Method') - #Whitelist based on IP address + #whitelist based on IP address sourceIp=request.headers.get('X-Forwarded-For',None) if sourceIp in request.args.getlist('ip'): return "", 201 + #Whitelist based on CIDR netmask + for net in request.args.getlist('network'): + net = ip_network(net) + if sourceIp and ip_address(sourceIp) in net: + return "", 201 + if current_user.is_anonymous: return Response( f'Could not verify your access level for that {origin}.\n' From df2afdc576b876bfd99f532c8e2356d128077c2f Mon Sep 17 00:00:00 2001 From: Alex Davies Date: Sat, 1 May 2021 17:22:50 -0300 Subject: [PATCH 08/10] Minor docs change --- examples/docker-compose.forwardauth.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/docker-compose.forwardauth.yaml b/examples/docker-compose.forwardauth.yaml index f64898e..3ef928b 100644 --- a/examples/docker-compose.forwardauth.yaml +++ b/examples/docker-compose.forwardauth.yaml @@ -22,7 +22,8 @@ services: traefik.http.routers.whoami.rule: Host(`whoami.localhost`) traefik.http.services.whoami.loadbalancer.server.port: 80 traefik.http.routers.whoami.entrypoints: web - traefik.http.routers.whoami.middlewares: traefik-forward-auth + traefik.http.routers.whoami.middlewares: whoami + traefik.http.middlewares.whoami.forwardauth.address: http://glauth-ui:5000/forward_auth/header/?group=glauth_admin&group=svcaccts glauth-ui: build: ../ @@ -39,7 +40,6 @@ services: traefik.http.routers.auth.rule: Host(`auth.localhost`) traefik.http.services.auth.loadbalancer.server.port: 5000 traefik.http.routers.auth.entrypoints: web - traefik.http.middlewares.traefik-forward-auth.forwardauth.address: http://glauth-ui:5000/forward_auth/header/?group=glauth_admin&group=svcaccts glauth: image: glauth/glauth From ea2ae56a38006e6e78a373c28397ac3534d8cd34 Mon Sep 17 00:00:00 2001 From: Alex Davies Date: Tue, 4 May 2021 08:28:30 -0300 Subject: [PATCH 09/10] Clear out non header auth stuff --- app/__init__.py | 22 ------------ app/routes.py | 44 +++--------------------- app/templates/forward_auth.html | 20 ----------- app/templates/login.html | 9 ++--- config.py | 4 --- examples/docker-compose.forwardauth.yaml | 1 - 6 files changed, 6 insertions(+), 94 deletions(-) delete mode 100644 app/templates/forward_auth.html diff --git a/app/__init__.py b/app/__init__.py index 2b11688..1d589eb 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -77,26 +77,4 @@ def createdbdata(): else: app.logger.info('Data in DB allready exists.') -@app.after_request -def cors_headers(response): - """Automatically add CORS headers to every request. This - should be tightened up. - """ - response.headers["access-control-allow-origin"] = request.headers.get('origin') - response.headers.add('Access-Control-Allow-Headers', "hx-current-url,hx-request") - response.headers.add('Access-Control-Allow-Credentials', 'true') - return response - -import flask -class DomainSessionInterface(flask.sessions.SecureCookieSessionInterface): - def get_cookie_domain(self, app): - origin=request.headers.get('Origin',Config.SERVER_NAME).split("://")[-1] - if origin=="localhost": - origin="" - return origin - - def get_cookie_httponly(self, app): - return False - -app.session_interface = DomainSessionInterface() diff --git a/app/routes.py b/app/routes.py index 5ccb8d5..975bf8b 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,6 +1,6 @@ from flask import render_template, flash, redirect, url_for, request, abort, Response from app import app, db -from app.forms import LoginForm, EditProfileForm, ChangePasswordForm +from app.forms import LoginForm, EditProfileForm, ChangePasswordForm from app.forms import ResetPasswordRequestForm, ResetPasswordForm, NewAccountForm from app.forms import TestMailForm from flask_login import current_user, login_user, logout_user, login_required @@ -52,7 +52,7 @@ def login(): login_user(user, remember=form.remember_me.data) next_page = request.args.get('next') - if not next_page or url_parse(next_page).netloc != '': + if not next_page or url_parse(next_page).netloc != '': return redirect(url_for('index')) return redirect(next_page) return render_template('login.html', title='Sign In', form=form) @@ -148,8 +148,8 @@ def new_account(token): import base64 from ipaddress import ip_network, ip_address -@app.route('/forward_auth/header/', methods=['GET', 'POST'], subdomain="") -def forward_auth(subdomain): +@app.route('/forward_auth/header/', methods=['GET', 'POST']) +def forward_auth(): """The actual login is handled by flask_login """ protocol = request.headers.get('X-Forwarded-Proto') @@ -179,40 +179,4 @@ def forward_auth(subdomain): if current_user.in_groups(*allowed_groups): return "", 201 - return abort(403) - -#@app.route('/forward_auth/cookie/', methods=['GET', 'POST'], subdomain="") -def forward_auth(subdomain): - """Unfortunatly implementing the CORS cookies in a clean way behind traefik is a bit beyond - me. There are things traefik could do to make this easier, like allow me to do a post - request to the auth server from behind the proxy, but alas. - """ - raise NotImplementedError - protocol = request.headers.get('X-Forwarded-Proto') - host = request.headers.get('X-Forwarded-Host') - uri = request.headers.get('X-Forwarded-Uri') - origin = protocol+"://"+host+uri - method = request.headers.get('X-Forwarded-Method') - - #Whitelist based on IP address, wish there was some way to whitelist based on - # docker service name. Maybe traefik will do something about it. - #ToDo: If somone wants maybe add IP range whitelisting? I just did this because it - # was very easy to do, and someone might find it useful. - sourceIp=request.headers.get('X-Forwarded-For',None) - if sourceIp in request.args.getlist('ip'): - return "", 201 - - if current_user.is_anonymous: - return render_template('forward_auth.html'), 401 - loginpage = login(internal_redirect=origin) - if type(loginpage)==str: - return loginpage, 401 - return loginpage - - #Simple no DB based group lookup, configurable via client env variable - #Makes sure the user is in one of the groups passed as a `group` querystring arg. - allowed_groups = request.args.getlist('group') - if current_user.in_groups(*allowed_groups): - return "", 201 - return abort(401) diff --git a/app/templates/forward_auth.html b/app/templates/forward_auth.html deleted file mode 100644 index 9e8415d..0000000 --- a/app/templates/forward_auth.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
- Loading login form -- Please ensure javascript is enabled -
- - - - -{% endblock %} diff --git a/app/templates/login.html b/app/templates/login.html index 6f72846..9d96969 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -5,14 +5,9 @@

Please sign in

- {{ render_form(form, action='//'+config.SERVER_NAME+'/login', - render_kw={ - 'hx-post':'//'+config.SERVER_NAME+'/login', - 'hx-target': 'body', - }) - }} + {{ render_form(form) }}
-

Forgot Your Password? Click to Reset It

+

Forgot Your Password? Click to Reset It

{% endblock %} diff --git a/config.py b/config.py index 3d6efc4..131135d 100644 --- a/config.py +++ b/config.py @@ -8,10 +8,6 @@ class Config(object): SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' ADMIN_GROUP = os.environ.get('ADMIN_GROUP') or 'glauth_admin' -# SESSION_COOKIE_DOMAIN = False - SESSION_COOKIE_NAME = "glauth-ui-session" - SESSION_COOKIE_SECURE = False - # MAIL Config MAIL_SERVER = os.environ.get('MAIL_SERVER') MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25) diff --git a/examples/docker-compose.forwardauth.yaml b/examples/docker-compose.forwardauth.yaml index 3ef928b..d5cfd8b 100644 --- a/examples/docker-compose.forwardauth.yaml +++ b/examples/docker-compose.forwardauth.yaml @@ -33,7 +33,6 @@ services: - 'glauth:/home/ldap/db' environment: SECRET_KEY: mysuperlongsecretkeythatnobodywillguess - BASE_URL: auth.localhost labels: traefik.enable: true From ed9f4cfc322edf4512cf89ec1a65b64f165e29f7 Mon Sep 17 00:00:00 2001 From: Alex Davies Date: Tue, 4 May 2021 09:04:56 -0300 Subject: [PATCH 10/10] Finally re-ask for password if unauthorized --- app/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes.py b/app/routes.py index 62a9f19..1fe9f1d 100644 --- a/app/routes.py +++ b/app/routes.py @@ -185,4 +185,4 @@ def forward_auth(): if current_user.in_groups(*allowed_groups): return "", 201 - return abort(401) + return abort(403)