Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forward auth #6

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
13 changes: 13 additions & 0 deletions .drone.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
kind: pipeline
name: default

steps:
- name: docker
image: plugins/docker
settings:
repo: traverseda/glauth-ui
auto_tag: true
username:
from_secret: dockerhub_user
password:
from_secret: dockerhub_pass
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.8-alpine
FROM python:3.9-alpine

RUN adduser -D ldap

Expand All @@ -24,4 +24,4 @@ USER ldap
VOLUME ["/home/ldap/db"]

EXPOSE 5000
ENTRYPOINT ["./boot.sh"]
ENTRYPOINT ["./boot.sh"]
36 changes: 1 addition & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,50 +26,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
- [email protected]
```
`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:
Expand Down
9 changes: 5 additions & 4 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,7 +22,6 @@
mail = Mail(app)
bootstrap = Bootstrap(app)


if not app.debug:
if app.config['MAIL_SERVER']:
auth = None
Expand Down Expand Up @@ -72,8 +71,10 @@ 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.')
app.logger.info('Data in DB allready exists.')


2 changes: 1 addition & 1 deletion app/forms.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
26 changes: 25 additions & 1 deletion app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ class User(UserMixin, db.Model):
othergroups = db.relationship('Group', secondary=othergroups_users,
backref=db.backref('o_users', lazy='dynamic'))

def in_groups(self,*allowed_groups):
"""Check is the user is in a group
"""
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
return False

@property
def is_admin(self):
# checks if the name of any group matches the configured ADMIN_GROUP name
Expand Down Expand Up @@ -117,6 +128,19 @@ 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()
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
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')
Expand All @@ -142,4 +166,4 @@ def create_basic_db():
db.session.add(u1)
db.session.add(u2)

db.session.commit()
db.session.commit()
49 changes: 43 additions & 6 deletions app/routes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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 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
Expand Down Expand Up @@ -34,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():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
Expand All @@ -48,8 +48,9 @@ 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 != '':
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)
Expand Down Expand Up @@ -148,4 +149,40 @@ 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', title='Activate Account', form=form, fullname=fullname)
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'])
def forward_auth():
"""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

#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'
'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)
1 change: 1 addition & 0 deletions app/static/htmx.min.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions app/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

{% block styles %}
<!-- Bootstrap CSS -->
{{ bootstrap.load_css() }}
Expand All @@ -30,7 +29,7 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item"> <!-- active -->
<a class="nav-link" href="{{ url_for('index') }}">Home</a>
<a class="nav-link" href="{{ url_for('index',_external=True) }}">Home</a>
</li>
{% if not current_user.is_anonymous %}
<li class="nav-item dropdown">
Expand Down Expand Up @@ -86,6 +85,7 @@
{% endblock %}
</div>
{% block scripts %}
<script src="{{ url_for('static', filename='htmx.min.js') }}"></script>
<!-- Optional JavaScript -->
{{ bootstrap.load_js() }}
{% endblock %}
Expand Down
2 changes: 1 addition & 1 deletion app/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ <h2>Please sign in</h2>
<p>Forgot Your Password? <a href="{{ url_for('reset_password_request') }}">Click to Reset It</a></p>
</div>
</div>
{% endblock %}
{% endblock %}
4 changes: 2 additions & 2 deletions docker-compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
53 changes: 53 additions & 0 deletions examples/docker-compose.forwardauth.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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: whoami
traefik.http.middlewares.whoami.forwardauth.address: http://glauth-ui:5000/forward_auth/header/?group=glauth_admin&group=svcaccts

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

glauth:
image: glauth/glauth
ports:
- "389:389"
command: /app/glauth -c /config/config.cfg
volumes:
- 'glauth:/config/'

volumes:
glauth: