You've already forked Mailu
mirror of
https://github.com/Mailu/Mailu.git
synced 2025-08-10 22:31:47 +02:00
Merge #2866
2866: Improve tokens (add ipranges) r=nextgens a=nextgens ## What type of PR? enhancement ## What does this PR do? Allow multiple IP addresses/networks to be set for tokens. ### Related issue(s) ## Prerequisites Before we can consider review and merge, please make sure the following list is done and checked. If an entry in not applicable, you can check it or remove it from the list. - [ ] In case of feature or enhancement: documentation updated accordingly - [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file. Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
This commit is contained in:
@@ -40,10 +40,13 @@ def check_credentials(user, password, ip, protocol=None, auth_port=None, source_
|
||||
return True
|
||||
if utils.is_app_token(password):
|
||||
for token in user.tokens:
|
||||
if (token.check_password(password) and
|
||||
(not token.ip or token.ip == ip)):
|
||||
if token.check_password(password):
|
||||
if not token.ip or utils.is_ip_in_subnet(ip, token.ip):
|
||||
app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: success: token-{token.id}: {token.comment or ""!r}')
|
||||
return True
|
||||
else:
|
||||
app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: failed: badip: token-{token.id}: {token.comment or ""!r}')
|
||||
return False # we can return directly here since the token is valid
|
||||
if user.check_password(password):
|
||||
app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: success: password')
|
||||
return True
|
||||
|
@@ -75,7 +75,7 @@ class CommaSeparatedList(db.TypeDecorator):
|
||||
""" Stores a list as a comma-separated string, compatible with Postfix.
|
||||
"""
|
||||
|
||||
impl = db.String(255)
|
||||
impl = db.String(4096)
|
||||
cache_ok = True
|
||||
python_type = list
|
||||
|
||||
@@ -732,7 +732,7 @@ class Token(Base):
|
||||
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))
|
||||
ip = db.Column(CommaSeparatedList, nullable=True, default=list)
|
||||
|
||||
def check_password(self, password):
|
||||
""" verifies password against stored hash
|
||||
|
@@ -5,6 +5,7 @@ from flask_babel import lazy_gettext as _
|
||||
import flask_login
|
||||
import flask_wtf
|
||||
import re
|
||||
import ipaddress
|
||||
|
||||
LOCALPART_REGEX = "^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*$"
|
||||
|
||||
@@ -152,10 +153,18 @@ class TokenForm(flask_wtf.FlaskForm):
|
||||
raw_password = fields.HiddenField([validators.DataRequired()])
|
||||
comment = fields.StringField(_('Comment'))
|
||||
ip = fields.StringField(
|
||||
_('Authorized IP'), [validators.Optional(), validators.IPAddress(ipv6=True)]
|
||||
_('Authorized IP'), [validators.Optional()]
|
||||
)
|
||||
submit = fields.SubmitField(_('Save'))
|
||||
|
||||
def validate_ip(form, field):
|
||||
if not field.data:
|
||||
return True
|
||||
try:
|
||||
for candidate in field.data.replace(' ','').split(','):
|
||||
ipaddress.ip_network(candidate, False)
|
||||
except:
|
||||
raise validators.ValidationError('Not a valid list of CIDRs')
|
||||
|
||||
class AliasForm(flask_wtf.FlaskForm):
|
||||
localpart = fields.StringField(_('Alias'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
|
||||
|
@@ -32,7 +32,7 @@
|
||||
</td>
|
||||
<td>{{ token.id }}</td>
|
||||
<td>{{ token.comment }}</td>
|
||||
<td>{{ token.ip or "any" }}</td>
|
||||
<td>{{ token.ip | join(', ') or "any" }}</td>
|
||||
<td data-sort="{{ token.created_at or '0000-00-00' }}">{{ token.created_at | format_date }}</td>
|
||||
<td data-sort="{{ token.updated_at or '0000-00-00' }}">{{ token.updated_at | format_date }}</td>
|
||||
</tr>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from mailu import models
|
||||
from mailu import models, utils
|
||||
from mailu.ui import ui, forms, access
|
||||
|
||||
from passlib import pwd
|
||||
@@ -27,11 +27,16 @@ def token_create(user_email):
|
||||
wtforms_components.read_only(form.displayed_password)
|
||||
if not form.raw_password.data:
|
||||
form.raw_password.data = pwd.genword(entropy=128, length=32, charset="hex")
|
||||
form.displayed_password.data = form.raw_password.data
|
||||
form.displayed_password.data = form.raw_password.data
|
||||
utils.formatCSVField(form.ip)
|
||||
if form.validate_on_submit():
|
||||
token = models.Token(user=user)
|
||||
token.set_password(form.raw_password.data)
|
||||
form.populate_obj(token)
|
||||
if form.ip.data:
|
||||
token.ip = form.ip.data.replace(' ','').split(',')
|
||||
else:
|
||||
del token.ip
|
||||
models.db.session.add(token)
|
||||
models.db.session.commit()
|
||||
flask.flash('Authentication token created')
|
||||
|
@@ -87,6 +87,16 @@ def is_exempt_from_ratelimits(ip):
|
||||
ip = ipaddress.ip_address(ip)
|
||||
return any(ip in cidr for cidr in app.config['AUTH_RATELIMIT_EXEMPTION'])
|
||||
|
||||
def is_ip_in_subnet(ip, subnets=[]):
|
||||
if isinstance(subnets, str):
|
||||
subnets = [subnets]
|
||||
ip = ipaddress.ip_address(ip)
|
||||
try:
|
||||
return any(ip in cidr for cidr in [ipaddress.ip_network(subnet, strict=False) for subnet in subnets])
|
||||
except:
|
||||
app.logger.debug(f'Unable to parse {subnets!r}, assuming {ip!r} is not in the set')
|
||||
return False
|
||||
|
||||
# Application translation
|
||||
babel = flask_babel.Babel()
|
||||
|
||||
@@ -521,6 +531,8 @@ def isBadOrPwned(form):
|
||||
return None
|
||||
|
||||
def formatCSVField(field):
|
||||
if not field.data:
|
||||
return
|
||||
if isinstance(field.data,str):
|
||||
data = field.data.replace(" ","").split(",")
|
||||
else:
|
||||
|
24
core/admin/migrations/versions/6b8f5e8caaa9_.py
Normal file
24
core/admin/migrations/versions/6b8f5e8caaa9_.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Enlarge token.ip
|
||||
|
||||
Revision ID: 6b8f5e8caaa9
|
||||
Revises: 7ac252f2bbbf
|
||||
Create Date: 2023-06-08 11:35:43.477708
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6b8f5e8caaa9'
|
||||
down_revision = '7ac252f2bbbf'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.alter_column('token', 'ip',
|
||||
existing_type=sa.String(),
|
||||
type=sa.VARCHAR(length=4096),
|
||||
existing_nullable=True)
|
||||
|
||||
def downgrade():
|
||||
pass
|
Reference in New Issue
Block a user