You've already forked Mailu
mirror of
https://github.com/Mailu/Mailu.git
synced 2025-08-10 22:31:47 +02:00
Allow multiple IP addresses/networks to be set for tokens
This commit is contained in:
@@ -39,10 +39,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:
|
||||
|
@@ -178,8 +178,8 @@ After saving the application token it is not possible anymore to view the unique
|
||||
|
||||
The comment field can be used to enter a description for the authentication token. For example the name of the application the application token is created for.
|
||||
|
||||
In the Authorized IP field a white listed IP address can be entered. When an IP address is entered, then the application token can only be used when the IP address of the client matches with this IP address.
|
||||
When no IP address is entered, there is no restriction on IP address. It is not possible to enter multiple IP addresses.
|
||||
In the Authorized IP field a comma separated list of white listed IP addresses or networks can be entered. When the field is set, the application token can only be used when the IP address of the client matches what is in the field.
|
||||
When no IP address is entered, there is no restriction on IP address.
|
||||
|
||||
|
||||
Announcement
|
||||
|
1
towncrier/newsfragments/2852.misc
Normal file
1
towncrier/newsfragments/2852.misc
Normal file
@@ -0,0 +1 @@
|
||||
Allow a list of subnets rather than just ip addresses for tokens
|
Reference in New Issue
Block a user