mirror of
https://github.com/Mailu/Mailu.git
synced 2025-03-17 20:57:54 +02:00
Merge pull request #292 from Mailu/feature-auth-tokens
Implement authentication tokens
This commit is contained in:
commit
cd90f0beb0
@ -35,7 +35,16 @@ def handle_authentication(headers):
|
||||
server, port = get_server(headers["Auth-Protocol"], True)
|
||||
user_email = urllib.parse.unquote(headers["Auth-User"])
|
||||
password = urllib.parse.unquote(headers["Auth-Pass"])
|
||||
ip = urllib.parse.unquote(headers["Client-Ip"])
|
||||
user = models.User.query.get(user_email)
|
||||
for token in user.tokens:
|
||||
if (token.check_password(password) and
|
||||
(not token.ip or token.ip == ip)):
|
||||
return {
|
||||
"Auth-Status": "OK",
|
||||
"Auth-Server": server,
|
||||
"Auth-Port": port
|
||||
}
|
||||
if user and user.check_password(password):
|
||||
return {
|
||||
"Auth-Status": "OK",
|
||||
|
@ -1,7 +1,7 @@
|
||||
from mailu import app, db, dkim, login_manager
|
||||
|
||||
from sqlalchemy.ext import declarative
|
||||
from passlib import context
|
||||
from passlib import context, hash
|
||||
from datetime import datetime
|
||||
|
||||
import re
|
||||
@ -249,6 +249,29 @@ class Alias(Base, Email):
|
||||
destination = db.Column(CommaSeparatedList, nullable=False, default=[])
|
||||
|
||||
|
||||
class Token(Base):
|
||||
""" A token is an application password for a given user.
|
||||
"""
|
||||
__tablename__ = "token"
|
||||
|
||||
id = db.Column(db.Integer(), primary_key=True)
|
||||
user_email = db.Column(db.String(255), db.ForeignKey(User.email),
|
||||
nullable=False)
|
||||
user = db.relationship(User,
|
||||
backref=db.backref('tokens', cascade='all, delete-orphan'))
|
||||
password = db.Column(db.String(255), nullable=False)
|
||||
ip = db.Column(db.String(255))
|
||||
|
||||
def check_password(self, password):
|
||||
return hash.sha256_crypt.verify(password, self.password)
|
||||
|
||||
def set_password(self, password):
|
||||
self.password = hash.sha256_crypt.using(rounds=1000).hash(password)
|
||||
|
||||
def __str__(self):
|
||||
return self.comment
|
||||
|
||||
|
||||
class Fetch(Base):
|
||||
""" A fetched account is a repote POP/IMAP account fetched into a local
|
||||
account.
|
||||
|
@ -104,6 +104,18 @@ class UserReplyForm(flask_wtf.FlaskForm):
|
||||
submit = fields.SubmitField(_('Update'))
|
||||
|
||||
|
||||
class TokenForm(flask_wtf.FlaskForm):
|
||||
displayed_password = fields.StringField(
|
||||
_('Your token (write it down, as it will never be displayed again)')
|
||||
)
|
||||
raw_password = fields.HiddenField([validators.DataRequired()])
|
||||
comment = fields.StringField(_('Comment'))
|
||||
ip = fields.StringField(
|
||||
_('Authorized IP'), [validators.Optional(), validators.IPAddress()]
|
||||
)
|
||||
submit = fields.SubmitField(_('Create'))
|
||||
|
||||
|
||||
class AliasForm(flask_wtf.FlaskForm):
|
||||
localpart = fields.StringField(_('Alias'), [validators.DataRequired()])
|
||||
wildcard = fields.BooleanField(
|
||||
|
@ -31,7 +31,12 @@
|
||||
<i class="fa fa-download"></i> <span>{% trans %}Fetched accounts{% endtrans %}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="{{ url_for('.token_list') }}">
|
||||
<i class="fa fa-ticket"></i> <span>{% trans %}Authentication tokens{% endtrans %}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="header">{% trans %}Administration{% endtrans %}</li>
|
||||
{% if current_user.global_admin %}
|
||||
<li>
|
||||
|
9
admin/mailu/ui/templates/token/create.html
Normal file
9
admin/mailu/ui/templates/token/create.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends "form.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Create an authentication token{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{{ user }}
|
||||
{% endblock %}
|
36
admin/mailu/ui/templates/token/list.html
Normal file
36
admin/mailu/ui/templates/token/list.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Authentication tokens{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{{ user }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_action %}
|
||||
<a class="btn btn-primary" href="{{ url_for('.token_create', user_email=user.email) }}">{% trans %}New token{% endtrans %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block box %}
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
<th>{% trans %}Comment{% endtrans %}</th>
|
||||
<th>{% trans %}Authorized IP{% endtrans %}</th>
|
||||
<th>{% trans %}Created{% endtrans %}</th>
|
||||
</tr>
|
||||
{% for token in user.tokens %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('.token_delete', token_id=token.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
<td>{{ token.comment }}</td>
|
||||
<td>{{ token.ip or "any" }}</td>
|
||||
<td>{{ token.created_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
@ -1 +1,4 @@
|
||||
__all__ = ['admins', 'aliases', 'alternatives', 'base', 'domains', 'fetches', 'managers', 'users', 'relays']
|
||||
__all__ = [
|
||||
'admins', 'aliases', 'alternatives', 'base', 'domains', 'fetches',
|
||||
'managers', 'users', 'relays', 'tokens'
|
||||
]
|
||||
|
53
admin/mailu/ui/views/tokens.py
Normal file
53
admin/mailu/ui/views/tokens.py
Normal file
@ -0,0 +1,53 @@
|
||||
from mailu import db, models
|
||||
from mailu.ui import ui, forms, access
|
||||
|
||||
from passlib import pwd
|
||||
|
||||
import flask
|
||||
import flask_login
|
||||
import wtforms_components
|
||||
|
||||
|
||||
@ui.route('/token/list', methods=['GET', 'POST'], defaults={'user_email': None})
|
||||
@ui.route('/token/list/<user_email>', methods=['GET'])
|
||||
@access.owner(models.User, 'user_email')
|
||||
def token_list(user_email):
|
||||
user_email = user_email or flask_login.current_user.email
|
||||
user = models.User.query.get(user_email) or flask.abort(404)
|
||||
return flask.render_template('token/list.html', user=user)
|
||||
|
||||
|
||||
@ui.route('/token/create', methods=['GET', 'POST'], defaults={'user_email': None})
|
||||
@ui.route('/token/create/<user_email>', methods=['GET', 'POST'])
|
||||
@access.owner(models.User, 'user_email')
|
||||
def token_create(user_email):
|
||||
user_email = user_email or flask_login.current_user.email
|
||||
user = models.User.query.get(user_email) or flask.abort(404)
|
||||
form = forms.TokenForm()
|
||||
wtforms_components.read_only(form.displayed_password)
|
||||
if not form.raw_password.data:
|
||||
form.raw_password.data = pwd.genword(entropy=128, charset="hex")
|
||||
form.displayed_password.data = form.raw_password.data
|
||||
if form.validate_on_submit():
|
||||
token = models.Token(user=user)
|
||||
token.set_password(form.raw_password.data)
|
||||
form.populate_obj(token)
|
||||
db.session.add(token)
|
||||
db.session.commit()
|
||||
flask.flash('Authentication token created')
|
||||
return flask.redirect(
|
||||
flask.url_for('.token_list', user_email=user.email))
|
||||
return flask.render_template('token/create.html', form=form)
|
||||
|
||||
|
||||
@ui.route('/token/delete/<token_id>', methods=['GET', 'POST'])
|
||||
@access.confirmation_required("delete an authentication token")
|
||||
@access.owner(models.Token, 'token_id')
|
||||
def token_delete(token_id):
|
||||
token = models.Token.query.get(token_id) or flask.abort(404)
|
||||
user = token.user
|
||||
db.session.delete(token)
|
||||
db.session.commit()
|
||||
flask.flash('Authentication token deleted')
|
||||
return flask.redirect(
|
||||
flask.url_for('.token_list', user_email=user.email))
|
32
admin/migrations/versions/9400a032eb1a_.py
Normal file
32
admin/migrations/versions/9400a032eb1a_.py
Normal file
@ -0,0 +1,32 @@
|
||||
""" Add authentication tokens
|
||||
|
||||
Revision ID: 9400a032eb1a
|
||||
Revises: 9c28df23f77e
|
||||
Create Date: 2017-10-29 14:31:58.032989
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '9400a032eb1a'
|
||||
down_revision = '9c28df23f77e'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table('token',
|
||||
sa.Column('created_at', sa.Date(), nullable=False),
|
||||
sa.Column('updated_at', sa.Date(), nullable=True),
|
||||
sa.Column('comment', sa.String(length=255), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_email', sa.String(length=255), nullable=False),
|
||||
sa.Column('password', sa.String(length=255), nullable=False),
|
||||
sa.Column('ip', sa.String(length=255), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_email'], ['user.email'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('token')
|
Loading…
x
Reference in New Issue
Block a user