[Update] merge

pull/2461/head
ibuler 2019-03-04 11:36:10 +08:00
commit a5fc04e0ce
76 changed files with 1836 additions and 976 deletions

View File

@ -0,0 +1,158 @@
# Generated by Django 2.1.7 on 2019-02-28 10:16
import assets.models.asset
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
replaces = [('assets', '0002_auto_20180105_1807'), ('assets', '0003_auto_20180109_2331'), ('assets', '0004_auto_20180125_1218'), ('assets', '0005_auto_20180126_1637'), ('assets', '0006_auto_20180130_1502'), ('assets', '0007_auto_20180225_1815'), ('assets', '0008_auto_20180306_1804'), ('assets', '0009_auto_20180307_1212')]
dependencies = [
('assets', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='adminuser',
options={'ordering': ['name'], 'verbose_name': 'Admin user'},
),
migrations.AlterModelOptions(
name='asset',
options={'verbose_name': 'Asset'},
),
migrations.AlterModelOptions(
name='assetgroup',
options={'ordering': ['name'], 'verbose_name': 'Asset group'},
),
migrations.AlterModelOptions(
name='cluster',
options={'ordering': ['name'], 'verbose_name': 'Cluster'},
),
migrations.AlterModelOptions(
name='systemuser',
options={'ordering': ['name'], 'verbose_name': 'System user'},
),
migrations.RemoveField(
model_name='asset',
name='cluster',
),
migrations.AlterField(
model_name='assetgroup',
name='created_by',
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'),
),
migrations.CreateModel(
name='Label',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('value', models.CharField(max_length=128, verbose_name='Value')),
('category', models.CharField(choices=[('S', 'System'), ('U', 'User')], default='U', max_length=128, verbose_name='Category')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
options={
'db_table': 'assets_label',
},
),
migrations.AlterUniqueTogether(
name='label',
unique_together={('name', 'value')},
),
migrations.AddField(
model_name='asset',
name='labels',
field=models.ManyToManyField(blank=True, related_name='assets', to='assets.Label', verbose_name='Labels'),
),
migrations.RemoveField(
model_name='asset',
name='cabinet_no',
),
migrations.RemoveField(
model_name='asset',
name='cabinet_pos',
),
migrations.RemoveField(
model_name='asset',
name='env',
),
migrations.RemoveField(
model_name='asset',
name='remote_card_ip',
),
migrations.RemoveField(
model_name='asset',
name='status',
),
migrations.RemoveField(
model_name='asset',
name='type',
),
migrations.CreateModel(
name='Node',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('key', models.CharField(max_length=64, unique=True, verbose_name='Key')),
('value', models.CharField(max_length=128, verbose_name='Value')),
('child_mark', models.IntegerField(default=0)),
('date_create', models.DateTimeField(auto_now_add=True)),
],
),
migrations.RemoveField(
model_name='asset',
name='groups',
),
migrations.RemoveField(
model_name='systemuser',
name='cluster',
),
migrations.AlterField(
model_name='asset',
name='admin_user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.AdminUser', verbose_name='Admin user'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AddField(
model_name='asset',
name='nodes',
field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'),
),
migrations.AddField(
model_name='systemuser',
name='nodes',
field=models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes'),
),
migrations.AlterField(
model_name='adminuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='systemuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
]

View File

@ -0,0 +1,220 @@
# Generated by Django 2.1.7 on 2019-02-28 10:16
import assets.models.utils
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import uuid
# Functions from the following migrations need manual copying.
# Move them and any dependencies into this file, then update the
# RunPython operations to refer to the local versions:
# assets.migrations.0017_auto_20180702_1415
def migrate_win_to_ssh_protocol(apps, schema_editor):
asset_model = apps.get_model("assets", "Asset")
db_alias = schema_editor.connection.alias
asset_model.objects.using(db_alias).filter(platform__startswith='Win').update(protocol='rdp')
class Migration(migrations.Migration):
replaces = [('assets', '0010_auto_20180307_1749'), ('assets', '0011_auto_20180326_0957'), ('assets', '0012_auto_20180404_1302'), ('assets', '0013_auto_20180411_1135'), ('assets', '0014_auto_20180427_1245'), ('assets', '0015_auto_20180510_1235'), ('assets', '0016_auto_20180511_1203'), ('assets', '0017_auto_20180702_1415'), ('assets', '0018_auto_20180807_1116'), ('assets', '0019_auto_20180816_1320')]
dependencies = [
('assets', '0009_auto_20180307_1212'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, unique=True, verbose_name='Value'),
),
migrations.CreateModel(
name='Domain',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
),
migrations.CreateModel(
name='Gateway',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('username', models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('ip', models.GenericIPAddressField(db_index=True, verbose_name='IP')),
('port', models.IntegerField(default=22, verbose_name='Port')),
('protocol', models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol')),
('comment', models.CharField(blank=True, max_length=128, null=True, verbose_name='Comment')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Domain', verbose_name='Domain')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='asset',
name='domain',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.Domain', verbose_name='Domain'),
),
migrations.AddField(
model_name='systemuser',
name='assets',
field=models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets'),
),
migrations.AlterField(
model_name='systemuser',
name='sudo',
field=models.TextField(default='/bin/whoami', verbose_name='Sudo'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, verbose_name='Value'),
),
migrations.AddField(
model_name='asset',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=128, verbose_name='Protocol'),
),
migrations.AddField(
model_name='systemuser',
name='login_mode',
field=models.CharField(choices=[('auto', 'Automatic login'), ('manual', 'Manually login')], default='auto', max_length=10, verbose_name='Login mode'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Windows2016', 'Windows(2016)'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.RunPython(
code=migrate_win_to_ssh_protocol,
),
migrations.AddField(
model_name='adminuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='asset',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='domain',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='gateway',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='label',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='node',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='systemuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AlterField(
model_name='adminuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='asset',
name='hostname',
field=models.CharField(max_length=128, verbose_name='Hostname'),
),
migrations.AlterField(
model_name='gateway',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='systemuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='adminuser',
unique_together={('name', 'org_id')},
),
migrations.AddField(
model_name='asset',
name='cpu_vcpus',
field=models.IntegerField(null=True, verbose_name='CPU vcpus'),
),
migrations.AlterUniqueTogether(
name='asset',
unique_together={('org_id', 'hostname')},
),
migrations.AlterUniqueTogether(
name='gateway',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='systemuser',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='label',
unique_together={('name', 'value', 'org_id')},
),
]

View File

@ -1,4 +1,2 @@
# -*- coding: utf-8 -*-
#
from users.models import LoginLog

View File

@ -38,14 +38,4 @@ class Migration(migrations.Migration):
('datetime', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='UserLoginLog',
fields=[
],
options={
'proxy': True,
'indexes': [],
},
bases=('users.loginlog',),
),
]

View File

@ -0,0 +1,39 @@
# Generated by Django 2.1.7 on 2019-02-28 09:15
from django.db import migrations, models, connection
import django.utils.timezone
import uuid
class Migration(migrations.Migration):
dependencies = [
('audits', '0004_operatelog_passwordchangelog_userloginlog'),
]
operations = [
migrations.CreateModel(
name='UserLoginLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('username', models.CharField(max_length=128, verbose_name='Username')),
('type', models.CharField(choices=[('W', 'Web'), ('T', 'Terminal')], max_length=2, verbose_name='Login type')),
('ip', models.GenericIPAddressField(verbose_name='Login ip')),
('city', models.CharField(blank=True, max_length=254, null=True, verbose_name='Login city')),
('user_agent', models.CharField(blank=True, max_length=254, null=True, verbose_name='User agent')),
('mfa', models.SmallIntegerField(choices=[(0, 'Disabled'), (1, 'Enabled'), (2, '-')], default=2, verbose_name='MFA')),
('reason', models.SmallIntegerField(choices=[(0, '-'), (1, 'Username/password check failed'), (2, 'MFA authentication failed'), (3, 'Username does not exist'), (4, 'Password expired')], default=0, verbose_name='Reason')),
('status', models.BooleanField(choices=[(True, 'Success'), (False, 'Failed')], default=True, max_length=2, verbose_name='Status')),
('datetime', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date login')),
],
options={
'ordering': ['-datetime', 'username'],
},
),
]
drop_table_sql = "DROP TABLE audits_userloginlog"
rename_table_sql = "RENAME TABLE users_loginlog TO audits_userloginlog"
table_exist = 'users_loginlog' in connection.introspection.table_names()
if table_exist:
operations.append(migrations.RunSQL(drop_table_sql))
operations.append(migrations.RunSQL(rename_table_sql))

View File

@ -2,9 +2,9 @@ import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from orgs.mixins import OrgModelMixin
from .hands import LoginLog
__all__ = [
'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog',
@ -55,6 +55,50 @@ class PasswordChangeLog(models.Model):
return "{} change {}'s password".format(self.change_by, self.user)
class UserLoginLog(LoginLog):
class UserLoginLog(models.Model):
LOGIN_TYPE_CHOICE = (
('W', 'Web'),
('T', 'Terminal'),
)
MFA_DISABLED = 0
MFA_ENABLED = 1
MFA_UNKNOWN = 2
MFA_CHOICE = (
(MFA_DISABLED, _('Disabled')),
(MFA_ENABLED, _('Enabled')),
(MFA_UNKNOWN, _('-')),
)
REASON_NOTHING = 0
REASON_PASSWORD = 1
REASON_MFA = 2
REASON_NOT_EXIST = 3
REASON_PASSWORD_EXPIRED = 4
REASON_CHOICE = (
(REASON_NOTHING, _('-')),
(REASON_PASSWORD, _('Username/password check failed')),
(REASON_MFA, _('MFA authentication failed')),
(REASON_NOT_EXIST, _("Username does not exist")),
(REASON_PASSWORD_EXPIRED, _("Password expired")),
)
STATUS_CHOICE = (
(True, _('Success')),
(False, _('Failed'))
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
username = models.CharField(max_length=128, verbose_name=_('Username'))
type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type'))
ip = models.GenericIPAddressField(verbose_name=_('Login ip'))
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city'))
user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent'))
mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA'))
reason = models.SmallIntegerField(default=0, choices=REASON_CHOICE, verbose_name=_('Reason'))
status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status'))
datetime = models.DateTimeField(default=timezone.now, verbose_name=_('Date login'))
class Meta:
proxy = True
ordering = ['-datetime', 'username']

21
apps/audits/tasks.py Normal file
View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
#
import datetime
from django.utils import timezone
from django.conf import settings
from celery import shared_task
from ops.celery.decorator import register_as_period_task
from .models import UserLoginLog
@register_as_period_task(interval=3600*24)
@shared_task
def clean_login_log_period():
now = timezone.now()
try:
days = int(settings.LOGIN_LOG_KEEP_DAYS)
except ValueError:
days = 90
expired_day = now - datetime.timedelta(days=days)
UserLoginLog.objects.filter(datetime__lt=expired_day).delete()

View File

@ -8,7 +8,6 @@ from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org
from ops.views import CommandExecutionListView as UserCommandExecutionListView
from users.models import User
from .models import FTPLog, OperateLog, PasswordChangeLog, UserLoginLog

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
#
from .auth import *

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
import uuid
from django.core.cache import cache
@ -14,16 +15,21 @@ from rest_framework.views import APIView
from common.utils import get_logger, get_request_ip
from common.permissions import IsOrgAdminOrAppUser
from orgs.mixins import RootOrgViewMixin
from ..serializers import UserSerializer
from ..tasks import write_login_log_async
from ..models import User, LoginLog
from ..utils import check_user_valid, check_otp_code, \
increase_login_failed_count, is_block_login, \
clean_failed_count
from ..hands import Asset, SystemUser
from users.serializers import UserSerializer
from users.models import User
from assets.models import Asset, SystemUser
from audits.models import UserLoginLog as LoginLog
from users.utils import (
check_user_valid, check_otp_code, increase_login_failed_count,
is_block_login, clean_failed_count
)
from ..signals import post_auth_success, post_auth_failed
logger = get_logger(__name__)
__all__ = [
'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi',
]
class UserAuthApi(RootOrgViewMixin, APIView):
@ -46,37 +52,22 @@ class UserAuthApi(RootOrgViewMixin, APIView):
username = request.data.get('username', '')
exist = User.objects.filter(username=username).first()
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
data = {
'username': username,
'mfa': LoginLog.MFA_UNKNOWN,
'reason': reason,
'status': False
}
self.write_login_log(request, data)
self.send_auth_signal(success=False, username=username, reason=reason)
increase_login_failed_count(username, ip)
return Response({'msg': msg}, status=401)
if user.password_has_expired:
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_PASSWORD_EXPIRED,
'status': False
}
self.write_login_log(request, data)
self.send_auth_signal(
success=False, username=username,
reason=LoginLog.REASON_PASSWORD_EXPIRED
)
msg = _("The user {} password has expired, please update.".format(
user.username))
logger.info(msg)
return Response({'msg': msg}, status=401)
if not user.otp_enabled:
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(request, data)
self.send_auth_signal(success=True, user=user)
# 登陆成功,清除原来的缓存计数
clean_failed_count(username, ip)
token = user.create_bearer_token(request)
@ -91,7 +82,7 @@ class UserAuthApi(RootOrgViewMixin, APIView):
'code': 101,
'msg': _('Please carry seed value and '
'conduct MFA secondary certification'),
'otp_url': reverse('api-users:user-otp-auth'),
'otp_url': reverse('api-auth:user-otp-auth'),
'seed': seed,
'user': self.serializer_class(user).data
}, status=300
@ -108,22 +99,14 @@ class UserAuthApi(RootOrgViewMixin, APIView):
)
return user, msg
@staticmethod
def write_login_log(request, data):
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
user_agent = request.data.get('HTTP_USER_AGENT', '')
if not login_ip:
login_ip = get_request_ip(request)
tmp_data = {
'ip': login_ip,
'type': login_type,
'user_agent': user_agent,
}
data.update(tmp_data)
write_login_log_async.delay(**data)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
class UserConnectionTokenApi(RootOrgViewMixin, APIView):
@ -167,29 +150,6 @@ class UserConnectionTokenApi(RootOrgViewMixin, APIView):
return super().get_permissions()
class UserToken(APIView):
permission_classes = (AllowAny,)
def post(self, request):
if not request.user.is_authenticated:
username = request.data.get('username', '')
email = request.data.get('email', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
user, msg = check_user_valid(
username=username, email=email,
password=password, public_key=public_key)
else:
user = request.user
msg = None
if user:
token = user.create_bearer_token(request)
return Response({'Token': token, 'Keyword': 'Bearer'}, status=200)
else:
return Response({'error': msg}, status=406)
class UserOtpAuthApi(RootOrgViewMixin, APIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
@ -197,52 +157,25 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView):
def post(self, request):
otp_code = request.data.get('otp_code', '')
seed = request.data.get('seed', '')
user = cache.get(seed, None)
if not user:
return Response(
{'msg': _('Please verify the user name and password first')},
status=401
)
if not check_otp_code(user.otp_secret_key, otp_code):
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_MFA,
'status': False
}
self.write_login_log(request, data)
self.send_auth_signal(success=False, username=user.username, reason=LoginLog.REASON_MFA)
return Response({'msg': _('MFA certification failed')}, status=401)
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(request, data)
self.send_auth_signal(success=True, user=user)
token = user.create_bearer_token(request)
return Response(
{
'token': token,
'user': self.serializer_class(user).data
}
)
data = {'token': token, 'user': self.serializer_class(user).data}
return Response(data)
@staticmethod
def write_login_log(request, data):
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
user_agent = request.data.get('HTTP_USER_AGENT', '')
if not login_ip:
login_ip = get_request_ip(request)
tmp_data = {
'ip': login_ip,
'type': login_type,
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)

View File

@ -8,13 +8,13 @@ from django.core.cache import cache
from django.conf import settings
from django.utils.translation import ugettext as _
from django.utils.six import text_type
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth import get_user_model
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import authentication, exceptions
from rest_framework.authentication import CSRFCheck
from common.utils import get_object_or_none, make_signature, http_to_unixtime
from .models import User, AccessKey, PrivateToken
from ..models import AccessKey, PrivateToken
def get_request_date_header(request):
@ -42,7 +42,6 @@ class AccessKeyAuthentication(authentication.BaseAuthentication):
失败
"""
keyword = 'Sign'
model = AccessKey
def authenticate(self, request):
auth = authentication.get_authorization_header(request).split()
@ -109,7 +108,7 @@ class AccessKeyAuthentication(authentication.BaseAuthentication):
class AccessTokenAuthentication(authentication.BaseAuthentication):
keyword = 'Bearer'
model = User
model = get_user_model()
expiration = settings.TOKEN_EXPIRATION or 3600
def authenticate(self, request):
@ -133,10 +132,9 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(token)
@staticmethod
def authenticate_credentials(token):
def authenticate_credentials(self, token):
user_id = cache.get(token)
user = get_object_or_none(User, id=user_id)
user = get_object_or_none(self.model, id=user_id)
if not user:
msg = _('Invalid token or cache refreshed.')

View File

@ -16,6 +16,8 @@ class LDAPAuthorizationBackend(LDAPBackend):
"""
def authenticate(self, request=None, username=None, password=None, **kwargs):
if not username:
return None
ldap_user = LDAPUser(self, username=username.strip(), request=request)
user = self.authenticate_ldap_user(ldap_user, password)
return user

View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
#
from .backends import *
from .middleware import *
from .utils import *

View File

@ -4,16 +4,19 @@
from django.contrib.auth import get_user_model
from django.conf import settings
from . import client
from common.utils import get_logger
from authentication.openid.models import OIDT_ACCESS_TOKEN
from .utils import new_client
from .models import OIDT_ACCESS_TOKEN
UserModel = get_user_model()
logger = get_logger(__file__)
client = new_client()
BACKEND_OPENID_AUTH_CODE = \
'authentication.openid.backends.OpenIDAuthorizationCodeBackend'
__all__ = [
'OpenIDAuthorizationCodeBackend', 'OpenIDAuthorizationPasswordBackend',
]
class BaseOpenIDAuthorizationBackend(object):
@ -72,7 +75,6 @@ class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend):
if not settings.AUTH_OPENID:
return None
elif not username:
return None

View File

@ -6,12 +6,13 @@ from django.contrib.auth import logout
from django.utils.deprecation import MiddlewareMixin
from django.contrib.auth import BACKEND_SESSION_KEY
from . import client
from common.utils import get_logger
from .backends import BACKEND_OPENID_AUTH_CODE
from authentication.openid.models import OIDT_ACCESS_TOKEN
from .utils import new_client
from .models import OIDT_ACCESS_TOKEN
BACKEND_OPENID_AUTH_CODE = 'OpenIDAuthorizationCodeBackend'
logger = get_logger(__file__)
__all__ = ['OpenIDAuthenticationMiddleware']
class OpenIDAuthenticationMiddleware(MiddlewareMixin):
@ -20,22 +21,22 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin):
"""
def process_request(self, request):
# Don't need openid auth if AUTH_OPENID is False
if not settings.AUTH_OPENID:
return
# Don't need check single logout if user not authenticated
if not request.user.is_authenticated:
return
elif request.session[BACKEND_SESSION_KEY] != BACKEND_OPENID_AUTH_CODE:
elif request.session[BACKEND_SESSION_KEY].endswith(
BACKEND_OPENID_AUTH_CODE):
return
# Check openid user single logout or not with access_token
client = new_client()
try:
client.openid_connect_client.userinfo(
token=request.session.get(OIDT_ACCESS_TOKEN))
token=request.session.get(OIDT_ACCESS_TOKEN)
)
except Exception as e:
logout(request)

View File

@ -5,7 +5,8 @@ from django.db import transaction
from django.contrib.auth import get_user_model
from keycloak.realm import KeycloakRealm
from keycloak.keycloak_openid import KeycloakOpenID
from ..signals import post_create_openid_user
from .signals import post_create_openid_user
OIDT_ACCESS_TOKEN = 'oidt_access_token'
@ -38,10 +39,6 @@ class Client(object):
self.openid_connect_client = self.new_openid_connect_client()
def new_realm(self):
"""
:param authentication.openid.models.Realm realm:
:return keycloak.realm.Realm:
"""
return KeycloakRealm(
server_url=self.server_url,
realm_name=self.realm_name,
@ -76,7 +73,7 @@ class Client(object):
:param str username: authentication username
:param str password: authentication password
:return: authentication.models.OpenIDTokenProfile
:return: OpenIDTokenProfile
"""
token_response = self.openid_client.token(
username=username, password=password
@ -93,7 +90,7 @@ class Client(object):
:param str code: authentication code
:param str redirect_uri:
:rtype: authentication.models.OpenIDTokenProfile
:rtype: OpenIDTokenProfile
"""
token_response = self.openid_connect_client.authorization_code(
@ -114,7 +111,7 @@ class Client(object):
- refresh_expires_in
:param dict token_response:
:rtype: authentication.openid.models.OpenIDTokenProfile
:rtype: OpenIDTokenProfile
"""
userinfo = self.openid_connect_client.userinfo(

View File

@ -0,0 +1,5 @@
from django.dispatch import Signal
post_create_openid_user = Signal(providing_args=('user',))
post_openid_login_success = Signal(providing_args=('user', 'request'))

View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
#
from django.urls import path
from . import views
urlpatterns = [
path('login/', views.OpenIDLoginView.as_view(), name='openid-login'),
path('login/complete/', views.OpenIDLoginCompleteView.as_view(),
name='openid-login-complete'),
]

View File

@ -4,6 +4,8 @@
from django.conf import settings
from .models import Client
__all__ = ['new_client']
def new_client():
"""
@ -15,6 +17,3 @@ def new_client():
client_id=settings.AUTH_OPENID_CLIENT_ID,
client_secret=settings.AUTH_OPENID_CLIENT_SECRET
)
client = new_client()

View File

@ -3,7 +3,6 @@
import logging
from django.urls import reverse
from django.conf import settings
from django.core.cache import cache
from django.views.generic.base import RedirectView
@ -14,43 +13,35 @@ from django.http.response import (
HttpResponseRedirect
)
from . import client
from .utils import new_client
from .models import Nonce
from users.models import LoginLog
from users.tasks import write_login_log_async
from common.utils import get_request_ip
from .signals import post_openid_login_success
logger = logging.getLogger(__name__)
client = new_client()
__all__ = ['OpenIDLoginView', 'OpenIDLoginCompleteView']
def get_base_site_url():
return settings.BASE_SITE_URL
class LoginView(RedirectView):
class OpenIDLoginView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
redirect_uri = settings.BASE_SITE_URL + str(settings.LOGIN_COMPLETE_URL)
nonce = Nonce(
redirect_uri=get_base_site_url() + reverse(
"authentication:openid-login-complete"),
redirect_uri=redirect_uri,
next_path=self.request.GET.get('next')
)
cache.set(str(nonce.state), nonce, 24*3600)
self.request.session['openid_state'] = str(nonce.state)
authorization_url = client.openid_connect_client.\
authorization_url(
redirect_uri=nonce.redirect_uri, scope='code',
state=str(nonce.state)
)
return authorization_url
class LoginCompleteView(RedirectView):
class OpenIDLoginCompleteView(RedirectView):
def get(self, request, *args, **kwargs):
if 'error' in request.GET:
@ -79,24 +70,8 @@ class LoginCompleteView(RedirectView):
return HttpResponseBadRequest()
login(self.request, user)
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(data)
post_openid_login_success.send(
sender=self.__class__, user=user, request=self.request
)
return HttpResponseRedirect(nonce.next_path or '/')
def write_login_log(self, data):
login_ip = get_request_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
tmp_data = {
'ip': login_ip,
'type': 'W',
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
#

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
#
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.utils.translation import gettext_lazy as _
from captcha.fields import CaptchaField
class UserLoginForm(AuthenticationForm):
username = forms.CharField(label=_('Username'), max_length=100)
password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput,
max_length=128, strip=False
)
def confirm_login_allowed(self, user):
if not user.is_staff:
raise forms.ValidationError(
self.error_messages['inactive'],
code='inactive',)
class UserLoginCaptchaForm(UserLoginForm):
captcha = CaptchaField()
class UserCheckOtpCodeForm(forms.Form):
otp_code = forms.CharField(label=_('MFA code'), max_length=6)

View File

@ -0,0 +1,51 @@
# Generated by Django 2.1.7 on 2019-02-28 08:07
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AccessKey',
fields=[
('id',
models.UUIDField(default=uuid.uuid4, editable=False,
primary_key=True, serialize=False,
verbose_name='AccessKeyID')),
('secret',
models.UUIDField(default=uuid.uuid4, editable=False,
verbose_name='AccessKeySecret')),
('user', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='access_keys',
to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
),
migrations.CreateModel(
name='PrivateToken',
fields=[
('key',
models.CharField(max_length=40, primary_key=True,
serialize=False, verbose_name='Key')),
('created', models.DateTimeField(auto_now_add=True,
verbose_name='Created')),
('user', models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name='auth_token',
to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Private Token',
},
),
]

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
#

View File

@ -1 +1,33 @@
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token
from django.conf import settings
class AccessKey(models.Model):
id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True,
default=uuid.uuid4, editable=False)
secret = models.UUIDField(verbose_name='AccessKeySecret',
default=uuid.uuid4, editable=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User',
on_delete=models.CASCADE, related_name='access_keys')
def get_id(self):
return str(self.id)
def get_secret(self):
return str(self.secret)
def get_full_value(self):
return '{}:{}'.format(self.id, self.secret)
def __str__(self):
return str(self.id)
class PrivateToken(Token):
"""Inherit from auth token, otherwise migration is boring"""
class Meta:
verbose_name = _('Private Token')

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from .models import AccessKey
__all__ = ['AccessKeySerializer']
class AccessKeySerializer(serializers.ModelSerializer):
class Meta:
model = AccessKey
fields = ['id', 'secret']
read_only_fields = ['id', 'secret']

View File

@ -1,4 +1,5 @@
from django.dispatch import Signal
post_create_openid_user = Signal(providing_args=('user',))
post_auth_success = Signal(providing_args=('user', 'request'))
post_auth_failed = Signal(providing_args=('username', 'request', 'reason'))

View File

@ -2,9 +2,16 @@ from django.http.request import QueryDict
from django.conf import settings
from django.dispatch import receiver
from django.contrib.auth.signals import user_logged_out
from django.utils import timezone
from django_auth_ldap.backend import populate_user
from .openid import client
from .signals import post_create_openid_user
from common.utils import get_request_ip
from .backends.openid import new_client
from .backends.openid.signals import (
post_create_openid_user, post_openid_login_success
)
from .tasks import write_login_log_async
from .signals import post_auth_success, post_auth_failed
@receiver(user_logged_out)
@ -17,6 +24,7 @@ def on_user_logged_out(sender, request, user, **kwargs):
'redirect_uri': settings.BASE_SITE_URL
})
client = new_client()
openid_logout_url = "%s?%s" % (
client.openid_connect_client.get_url(
name='end_session_endpoint'),
@ -33,8 +41,46 @@ def on_post_create_openid_user(sender, user=None, **kwargs):
user.save()
@receiver(post_openid_login_success)
def on_openid_login_success(sender, user=None, request=None, **kwargs):
post_auth_success.send(sender=sender, user=user, request=request)
@receiver(populate_user)
def on_ldap_create_user(sender, user, ldap_user, **kwargs):
if user and user.name != 'admin':
user.source = user.SOURCE_LDAP
user.save()
def generate_data(username, request):
if not request.user.is_anonymous and request.user.is_app:
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
user_agent = request.data.get('HTTP_USER_AGENT', '')
else:
login_ip = get_request_ip(request)
user_agent = request.META.get('HTTP_USER_AGENT', '')
login_type = 'W'
data = {
'username': username,
'ip': login_ip,
'type': login_type,
'user_agent': user_agent,
'datetime': timezone.now()
}
return data
@receiver(post_auth_success)
def on_user_auth_success(sender, user, request, **kwargs):
data = generate_data(user.username, request)
data.update({'mfa': int(user.otp_enabled), 'status': True})
write_login_log_async.delay(**data)
@receiver(post_auth_failed)
def on_user_auth_failed(sender, username, request, reason, **kwargs):
data = generate_data(username, request)
data.update({'reason': reason, 'status': False})
write_login_log_async.delay(**data)

View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
#
from celery import shared_task
from .utils import write_login_log
@shared_task
def write_login_log_async(*args, **kwargs):
write_login_log(*args, **kwargs)

View File

@ -1 +1,20 @@
# coding:utf-8
#
from __future__ import absolute_import
from django.urls import path
from .. import api
app_name = 'authentication'
urlpatterns = [
# path('token/', api.UserToken.as_view(), name='user-token'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'),
path('connection-token/',
api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
]

View File

@ -1,16 +1,20 @@
# coding:utf-8
#
from django.urls import path
from authentication.openid import views
from __future__ import absolute_import
from django.urls import path, include
from .. import views
app_name = 'authentication'
urlpatterns = [
# openid
path('openid/login/', views.LoginView.as_view(), name='openid-login'),
path('openid/login/complete/', views.LoginCompleteView.as_view(),
name='openid-login-complete'),
path('openid/', include(('authentication.backends.openid.urls', 'authentication'), namespace='openid')),
# other
# login
path('login/', views.UserLoginView.as_view(), name='login'),
path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'),
path('logout/', views.UserLogoutView.as_view(), name='logout'),
]

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext as _
from common.utils import get_ip_city, validate_ip
def write_login_log(*args, **kwargs):
from audits.models import UserLoginLog
default_city = _("Unknown")
ip = kwargs.get('ip', '')
if not (ip and validate_ip(ip)):
ip = ip[:15]
city = default_city
else:
city = get_ip_city(ip) or default_city
kwargs.update({'ip': ip, 'city': city})
UserLoginLog.objects.create(**kwargs)

View File

@ -1 +0,0 @@

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
#
from .login import *

View File

@ -0,0 +1,208 @@
# ~*~ coding: utf-8 ~*~
#
from __future__ import unicode_literals
import os
from django.core.cache import cache
from django.contrib.auth import login as auth_login, logout as auth_logout
from django.http import HttpResponse
from django.shortcuts import reverse, redirect
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView
from django.conf import settings
from common.utils import get_request_ip
from users.models import User
from audits.models import UserLoginLog as LoginLog
from users.utils import (
check_otp_code, is_block_login, clean_failed_count, get_user_or_tmp_user,
set_tmp_user_to_cache, increase_login_failed_count,
redirect_user_first_login_or_index,
)
from ..signals import post_auth_success, post_auth_failed
from .. import forms
__all__ = [
'UserLoginView', 'UserLoginOtpView', 'UserLogoutView',
]
@method_decorator(sensitive_post_parameters(), name='dispatch')
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(never_cache, name='dispatch')
class UserLoginView(FormView):
form_class = forms.UserLoginForm
form_class_captcha = forms.UserLoginCaptchaForm
redirect_field_name = 'next'
key_prefix_captcha = "_LOGIN_INVALID_{}"
def get_template_names(self):
template_name = 'users/login.html'
if not settings.XPACK_ENABLED:
return template_name
from xpack.plugins.license.models import License
if not License.has_valid_license():
return template_name
template_name = 'users/new_login.html'
return template_name
def get(self, request, *args, **kwargs):
if request.user.is_staff:
return redirect(redirect_user_first_login_or_index(
request, self.redirect_field_name)
)
request.session.set_test_cookie()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
# limit login authentication
ip = get_request_ip(request)
username = self.request.POST.get('username')
if is_block_login(username, ip):
return self.render_to_response(self.get_context_data(block_login=True))
return super().post(request, *args, **kwargs)
def form_valid(self, form):
if not self.request.session.test_cookie_worked():
return HttpResponse(_("Please enable cookies and try again."))
user = form.get_user()
# user password expired
if user.password_has_expired:
reason = LoginLog.REASON_PASSWORD_EXPIRED
self.send_auth_signal(success=False, username=user.username, reason=reason)
return self.render_to_response(self.get_context_data(password_expired=True))
set_tmp_user_to_cache(self.request, user)
username = form.cleaned_data.get('username')
ip = get_request_ip(self.request)
# 登陆成功,清除缓存计数
clean_failed_count(username, ip)
return redirect(self.get_success_url())
def form_invalid(self, form):
# write login failed log
username = form.cleaned_data.get('username')
exist = User.objects.filter(username=username).first()
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
# limit user login failed count
ip = get_request_ip(self.request)
increase_login_failed_count(username, ip)
# show captcha
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
self.send_auth_signal(success=False, username=username, reason=reason)
old_form = form
form = self.form_class_captcha(data=form.data)
form._errors = old_form.errors
return super().form_invalid(form)
def get_form_class(self):
ip = get_request_ip(self.request)
if cache.get(self.key_prefix_captcha.format(ip)):
return self.form_class_captcha
else:
return self.form_class
def get_success_url(self):
user = get_user_or_tmp_user(self.request)
if user.otp_enabled and user.otp_secret_key:
# 1,2,mfa_setting & T
return reverse('authentication:login-otp')
elif user.otp_enabled and not user.otp_secret_key:
# 1,2,mfa_setting & F
return reverse('users:user-otp-enable-authentication')
elif not user.otp_enabled:
# 0 & T,F
auth_login(self.request, user)
self.send_auth_signal(success=True, user=user)
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def get_context_data(self, **kwargs):
context = {
'demo_mode': os.environ.get("DEMO_MODE"),
'AUTH_OPENID': settings.AUTH_OPENID,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
class UserLoginOtpView(FormView):
template_name = 'users/login_otp.html'
form_class = forms.UserCheckOtpCodeForm
redirect_field_name = 'next'
def form_valid(self, form):
user = get_user_or_tmp_user(self.request)
otp_code = form.cleaned_data.get('otp_code')
otp_secret_key = user.otp_secret_key
if check_otp_code(otp_secret_key, otp_code):
auth_login(self.request, user)
self.send_auth_signal(success=True, user=user)
return redirect(self.get_success_url())
else:
self.send_auth_signal(
success=False, username=user.username,
reason=LoginLog.REASON_MFA
)
form.add_error(
'otp_code', _('MFA code invalid, or ntp sync server time')
)
return super().form_invalid(form)
def get_success_url(self):
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
@method_decorator(never_cache, name='dispatch')
class UserLogoutView(TemplateView):
template_name = 'flash_message_standalone.html'
def get(self, request, *args, **kwargs):
auth_logout(request)
next_uri = request.COOKIES.get("next")
if next_uri:
return redirect(next_uri)
response = super().get(request, *args, **kwargs)
return response
def get_context_data(self, **kwargs):
context = {
'title': _('Logout success'),
'messages': _('Logout success, return login page'),
'interval': 1,
'redirect_url': reverse('authentication:login'),
'auto_redirect': True,
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
#
from .common import *
from .django import *
from .encode import *
from .http import *
from .ipip import *

View File

@ -1,104 +1,18 @@
# -*- coding: utf-8 -*-
#
import re
import sys
from collections import OrderedDict
from six import string_types
import base64
import os
from itertools import chain
import logging
import datetime
import time
import hashlib
from email.utils import formatdate
import calendar
import threading
from io import StringIO
import uuid
from functools import wraps
import copy
import paramiko
import sshpubkeys
from itsdangerous import TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer, \
BadSignature, SignatureExpired
from django.shortcuts import reverse as dj_reverse
from django.conf import settings
from django.utils import timezone
import ipaddress
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
def reverse(view_name, urlconf=None, args=None, kwargs=None,
current_app=None, external=False):
url = dj_reverse(view_name, urlconf=urlconf, args=args,
kwargs=kwargs, current_app=current_app)
if external:
site_url = settings.SITE_URL
url = site_url.strip('/') + url
return url
def get_object_or_none(model, **kwargs):
try:
obj = model.objects.get(**kwargs)
except model.DoesNotExist:
return None
return obj
class Singleton(type):
def __init__(cls, *args, **kwargs):
cls.__instance = None
super().__init__(*args, **kwargs)
def __call__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super().__call__(*args, **kwargs)
return cls.__instance
else:
return cls.__instance
class Signer(metaclass=Singleton):
"""用来加密,解密,和基于时间戳的方式验证token"""
def __init__(self, secret_key=None):
self.secret_key = secret_key
def sign(self, value):
s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256')
return s.dumps(value).decode()
def unsign(self, value):
if value is None:
return value
s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256')
try:
return s.loads(value)
except BadSignature:
return {}
def sign_t(self, value, expires_in=3600):
s = TimedJSONWebSignatureSerializer(self.secret_key, expires_in=expires_in)
return str(s.dumps(value), encoding="utf8")
def unsign_t(self, value):
s = TimedJSONWebSignatureSerializer(self.secret_key)
try:
return s.loads(value)
except (BadSignature, SignatureExpired):
return {}
def date_expired_default():
try:
years = int(settings.DEFAULT_EXPIRED_YEARS)
except TypeError:
years = 70
return timezone.now() + timezone.timedelta(days=365*years)
ipip_db = None
def combine_seq(s1, s2, callback=None):
@ -146,88 +60,6 @@ def timesince(dt, since='', default="just now"):
return default
def ssh_key_string_to_obj(text, password=None):
key = None
try:
key = paramiko.RSAKey.from_private_key(StringIO(text), password=password)
except paramiko.SSHException:
pass
try:
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password)
except paramiko.SSHException:
pass
return key
def ssh_pubkey_gen(private_key=None, username='jumpserver', hostname='localhost', password=None):
if isinstance(private_key, bytes):
private_key = private_key.decode("utf-8")
if isinstance(private_key, string_types):
private_key = ssh_key_string_to_obj(private_key, password=password)
if not isinstance(private_key, (paramiko.RSAKey, paramiko.DSSKey)):
raise IOError('Invalid private key')
public_key = "%(key_type)s %(key_content)s %(username)s@%(hostname)s" % {
'key_type': private_key.get_name(),
'key_content': private_key.get_base64(),
'username': username,
'hostname': hostname,
}
return public_key
def ssh_key_gen(length=2048, type='rsa', password=None, username='jumpserver', hostname=None):
"""Generate user ssh private and public key
Use paramiko RSAKey generate it.
:return private key str and public key str
"""
if hostname is None:
hostname = os.uname()[1]
f = StringIO()
try:
if type == 'rsa':
private_key_obj = paramiko.RSAKey.generate(length)
elif type == 'dsa':
private_key_obj = paramiko.DSSKey.generate(length)
else:
raise IOError('SSH private key must be `rsa` or `dsa`')
private_key_obj.write_private_key(f, password=password)
private_key = f.getvalue()
public_key = ssh_pubkey_gen(private_key_obj, username=username, hostname=hostname)
return private_key, public_key
except IOError:
raise IOError('These is error when generate ssh key.')
def validate_ssh_private_key(text, password=None):
if isinstance(text, bytes):
try:
text = text.decode("utf-8")
except UnicodeDecodeError:
return False
key = ssh_key_string_to_obj(text, password=password)
if key is None:
return False
else:
return True
def validate_ssh_public_key(text):
ssh = sshpubkeys.SSHKey(text)
try:
ssh.parse()
except (sshpubkeys.InvalidKeyException, UnicodeDecodeError):
return False
except NotImplementedError as e:
return False
return True
def setattr_bulk(seq, key, value):
def set_attr(obj):
setattr(obj, key, value)
@ -243,70 +75,6 @@ def set_or_append_attr_bulk(seq, key, value):
setattr(obj, key, value)
def content_md5(data):
"""计算data的MD5值经过Base64编码并返回str类型。
返回值可以直接作为HTTP Content-Type头部的值
"""
if isinstance(data, str):
data = hashlib.md5(data.encode('utf-8'))
value = base64.b64encode(data.hexdigest().encode('utf-8'))
return value.decode('utf-8')
_STRPTIME_LOCK = threading.Lock()
_GMT_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"
_ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S.000Z"
def to_unixtime(time_string, format_string):
time_string = time_string.decode("ascii")
with _STRPTIME_LOCK:
return int(calendar.timegm(time.strptime(time_string, format_string)))
def http_date(timeval=None):
"""返回符合HTTP标准的GMT时间字符串用strftime的格式表示就是"%a, %d %b %Y %H:%M:%S GMT"
但不能使用strftime因为strftime的结果是和locale相关的
"""
return formatdate(timeval, usegmt=True)
def http_to_unixtime(time_string):
"""把HTTP Date格式的字符串转换为UNIX时间自1970年1月1日UTC零点的秒数
HTTP Date形如 `Sat, 05 Dec 2015 11:10:29 GMT`
"""
return to_unixtime(time_string, _GMT_FORMAT)
def iso8601_to_unixtime(time_string):
"""把ISO8601时间字符串形如2012-02-24T06:07:48.000Z转换为UNIX时间精确到秒。"""
return to_unixtime(time_string, _ISO8601_FORMAT)
def make_signature(access_key_secret, date=None):
if isinstance(date, bytes):
date = bytes.decode(date)
if isinstance(date, int):
date_gmt = http_date(date)
elif date is None:
date_gmt = http_date(int(time.time()))
else:
date_gmt = date
data = str(access_key_secret) + "\n" + date_gmt
return content_md5(data)
def encrypt_password(password, salt=None):
from passlib.hash import sha512_crypt
if password:
return sha512_crypt.using(rounds=5000).hash(password, salt=salt)
return None
def capacity_convert(size, expect='auto', rate=1000):
"""
:param size: '100MB', '1G'
@ -374,11 +142,6 @@ def is_uuid(seq):
return True
def get_signer():
signer = Signer(settings.SECRET_KEY)
return signer
def get_request_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')
if x_forwarded_for and x_forwarded_for[0]:
@ -388,22 +151,13 @@ def get_request_ip(request):
return login_ip
def get_command_storage_setting():
default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE
value = settings.TERMINAL_COMMAND_STORAGE
if not value:
return default
value.update(default)
return value
def get_replay_storage_setting():
default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE
value = settings.TERMINAL_REPLAY_STORAGE
if not value:
return default
value.update(default)
return value
def validate_ip(ip):
try:
ipaddress.ip_address(ip)
return True
except ValueError:
pass
return False
def with_cache(func):
@ -537,4 +291,4 @@ class LocalProxy(object):
__rmod__ = lambda x, o: o % x._get_current_object()
__rdivmod__ = lambda x, o: x._get_current_object().__rdivmod__(o)
__copy__ = lambda x: copy.copy(x._get_current_object())
__deepcopy__ = lambda x, memo: copy.deepcopy(x._get_current_object(), memo)
__deepcopy__ = lambda x, memo: copy.deepcopy(x._get_current_object(), memo)

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
#
import re
from django.shortcuts import reverse as dj_reverse
from django.conf import settings
from django.utils import timezone
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
def reverse(view_name, urlconf=None, args=None, kwargs=None,
current_app=None, external=False):
url = dj_reverse(view_name, urlconf=urlconf, args=args,
kwargs=kwargs, current_app=current_app)
if external:
site_url = settings.SITE_URL
url = site_url.strip('/') + url
return url
def get_object_or_none(model, **kwargs):
try:
obj = model.objects.get(**kwargs)
except model.DoesNotExist:
return None
return obj
def date_expired_default():
try:
years = int(settings.DEFAULT_EXPIRED_YEARS)
except TypeError:
years = 70
return timezone.now() + timezone.timedelta(days=365*years)
def get_command_storage_setting():
default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE
value = settings.TERMINAL_COMMAND_STORAGE
if not value:
return default
value.update(default)
return value
def get_replay_storage_setting():
default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE
value = settings.TERMINAL_REPLAY_STORAGE
if not value:
return default
value.update(default)
return value

184
apps/common/utils/encode.py Normal file
View File

@ -0,0 +1,184 @@
# -*- coding: utf-8 -*-
#
import re
from six import string_types
import base64
import os
import time
import hashlib
from io import StringIO
import paramiko
import sshpubkeys
from itsdangerous import (
TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer,
BadSignature, SignatureExpired
)
from django.conf import settings
from .http import http_date
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
class Singleton(type):
def __init__(cls, *args, **kwargs):
cls.__instance = None
super().__init__(*args, **kwargs)
def __call__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super().__call__(*args, **kwargs)
return cls.__instance
else:
return cls.__instance
class Signer(metaclass=Singleton):
"""用来加密,解密,和基于时间戳的方式验证token"""
def __init__(self, secret_key=None):
self.secret_key = secret_key
def sign(self, value):
s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256')
return s.dumps(value).decode()
def unsign(self, value):
if value is None:
return value
s = JSONWebSignatureSerializer(self.secret_key, algorithm_name='HS256')
try:
return s.loads(value)
except BadSignature:
return {}
def sign_t(self, value, expires_in=3600):
s = TimedJSONWebSignatureSerializer(self.secret_key, expires_in=expires_in)
return str(s.dumps(value), encoding="utf8")
def unsign_t(self, value):
s = TimedJSONWebSignatureSerializer(self.secret_key)
try:
return s.loads(value)
except (BadSignature, SignatureExpired):
return {}
def ssh_key_string_to_obj(text, password=None):
key = None
try:
key = paramiko.RSAKey.from_private_key(StringIO(text), password=password)
except paramiko.SSHException:
pass
try:
key = paramiko.DSSKey.from_private_key(StringIO(text), password=password)
except paramiko.SSHException:
pass
return key
def ssh_pubkey_gen(private_key=None, username='jumpserver', hostname='localhost', password=None):
if isinstance(private_key, bytes):
private_key = private_key.decode("utf-8")
if isinstance(private_key, string_types):
private_key = ssh_key_string_to_obj(private_key, password=password)
if not isinstance(private_key, (paramiko.RSAKey, paramiko.DSSKey)):
raise IOError('Invalid private key')
public_key = "%(key_type)s %(key_content)s %(username)s@%(hostname)s" % {
'key_type': private_key.get_name(),
'key_content': private_key.get_base64(),
'username': username,
'hostname': hostname,
}
return public_key
def ssh_key_gen(length=2048, type='rsa', password=None, username='jumpserver', hostname=None):
"""Generate user ssh private and public key
Use paramiko RSAKey generate it.
:return private key str and public key str
"""
if hostname is None:
hostname = os.uname()[1]
f = StringIO()
try:
if type == 'rsa':
private_key_obj = paramiko.RSAKey.generate(length)
elif type == 'dsa':
private_key_obj = paramiko.DSSKey.generate(length)
else:
raise IOError('SSH private key must be `rsa` or `dsa`')
private_key_obj.write_private_key(f, password=password)
private_key = f.getvalue()
public_key = ssh_pubkey_gen(private_key_obj, username=username, hostname=hostname)
return private_key, public_key
except IOError:
raise IOError('These is error when generate ssh key.')
def validate_ssh_private_key(text, password=None):
if isinstance(text, bytes):
try:
text = text.decode("utf-8")
except UnicodeDecodeError:
return False
key = ssh_key_string_to_obj(text, password=password)
if key is None:
return False
else:
return True
def validate_ssh_public_key(text):
ssh = sshpubkeys.SSHKey(text)
try:
ssh.parse()
except (sshpubkeys.InvalidKeyException, UnicodeDecodeError):
return False
except NotImplementedError as e:
return False
return True
def content_md5(data):
"""计算data的MD5值经过Base64编码并返回str类型。
返回值可以直接作为HTTP Content-Type头部的值
"""
if isinstance(data, str):
data = hashlib.md5(data.encode('utf-8'))
value = base64.b64encode(data.hexdigest().encode('utf-8'))
return value.decode('utf-8')
def make_signature(access_key_secret, date=None):
if isinstance(date, bytes):
date = bytes.decode(date)
if isinstance(date, int):
date_gmt = http_date(date)
elif date is None:
date_gmt = http_date(int(time.time()))
else:
date_gmt = date
data = str(access_key_secret) + "\n" + date_gmt
return content_md5(data)
def encrypt_password(password, salt=None):
from passlib.hash import sha512_crypt
if password:
return sha512_crypt.using(rounds=5000).hash(password, salt=salt)
return None
def get_signer():
signer = Signer(settings.SECRET_KEY)
return signer

37
apps/common/utils/http.py Normal file
View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
#
import time
from email.utils import formatdate
import calendar
import threading
_STRPTIME_LOCK = threading.Lock()
_GMT_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"
_ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S.000Z"
def to_unixtime(time_string, format_string):
time_string = time_string.decode("ascii")
with _STRPTIME_LOCK:
return int(calendar.timegm(time.strptime(time_string, format_string)))
def http_date(timeval=None):
"""返回符合HTTP标准的GMT时间字符串用strftime的格式表示就是"%a, %d %b %Y %H:%M:%S GMT"
但不能使用strftime因为strftime的结果是和locale相关的
"""
return formatdate(timeval, usegmt=True)
def http_to_unixtime(time_string):
"""把HTTP Date格式的字符串转换为UNIX时间自1970年1月1日UTC零点的秒数
HTTP Date形如 `Sat, 05 Dec 2015 11:10:29 GMT`
"""
return to_unixtime(time_string, _GMT_FORMAT)
def iso8601_to_unixtime(time_string):
"""把ISO8601时间字符串形如2012-02-24T06:07:48.000Z转换为UNIX时间精确到秒。"""
return to_unixtime(time_string, _ISO8601_FORMAT)

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
#
from .ipdb import *

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
#
import os
import ipdb
ipip_db = None
def get_ip_city(ip):
global ipip_db
if ipip_db is None:
ipip_db_path = os.path.join(os.path.dirname(__file__), 'ipipfree.ipdb')
ipip_db = ipdb.City(ipip_db_path)
info = list(set(ipip_db.find(ip, 'CN')))
if '' in info:
info.remove('')
return ' '.join(info)

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
[{"model": "users.usergroup", "pk": "f3c6b021-59a9-43e7-b022-c2e9bfac84d7", "fields": {"is_discard": false, "discard_time": null, "name": "Default", "comment": "Default user group", "date_created": "2017-12-12T08:13:20.906Z", "created_by": "System"}}, {"model": "users.loginlog", "pk": "328c7d0f-214d-4665-8c7b-f3063718530e", "fields": {"username": "admin", "type": "ST", "ip": "127.0.0.1", "city": "Unknown", "user_agent": "", "datetime": "2017-12-12T08:10:28.973Z"}}, {"model": "users.loginlog", "pk": "a72fa02e-3b2c-40a0-8cb1-0c2f98d8f248", "fields": {"username": "admin", "type": "ST", "ip": "127.0.0.1", "city": "Unknown", "user_agent": "", "datetime": "2017-12-12T08:10:28.980Z"}}, {"model": "assets.cluster", "pk": "a950b8aa-073b-45ab-b72e-5bdfbb614653", "fields": {"name": "Default", "admin_user": null, "bandwidth": "", "contact": "", "phone": "", "address": "", "intranet": "", "extranet": "", "date_created": "2017-12-12T08:13:20.919Z", "operator": "", "created_by": "System", "comment": "Default Cluster"}}, {"model": "assets.assetgroup", "pk": "d742a7be-faf1-4c29-ae0a-e6aa640ab395", "fields": {"name": "Default", "created_by": "", "date_created": "2017-12-12T08:13:20.923Z", "comment": "Default asset group"}}, {"model": "captcha.captchastore", "pk": 1, "fields": {"challenge": "EQEI", "response": "eqei", "hashkey": "c993e8e245252eb8ca40a57c67a63ee9c61dce5c", "expiration": "2017-12-12T08:17:50.235Z"}}, {"model": "users.user", "pk": "61c39c1f-5b57-4268-8180-b6dda235aadd", "fields": {"password": "pbkdf2_sha256$36000$yhYWUEo4DNqj$SpxtdIOm9nwRG+X76jUUlGvdDcLaMBl7Z+rJ8sfSMcU=", "last_login": null, "first_name": "", "last_name": "", "is_active": true, "date_joined": "2017-12-12T08:13:20.827Z", "username": "admin", "name": "Administrator", "email": "admin@jumpserver.org", "role": "Admin", "avatar": "", "wechat": "", "phone": null, "enable_otp": false, "secret_key_otp": "", "_private_key": "", "_public_key": "", "comment": "Administrator is the super user of system", "is_first_login": false, "date_expired": "2087-11-25T08:13:20.827Z", "created_by": "System", "user_permissions": [], "groups": ["f3c6b021-59a9-43e7-b022-c2e9bfac84d7"]}}]

View File

@ -100,7 +100,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'authentication.openid.middleware.OpenIDAuthenticationMiddleware', # openid
'authentication.backends.openid.middleware.OpenIDAuthenticationMiddleware',
'jumpserver.middleware.TimezoneMiddleware',
'jumpserver.middleware.DemoMiddleware',
'jumpserver.middleware.RequestMiddleware',
@ -135,7 +135,7 @@ TEMPLATES = [
# WSGI_APPLICATION = 'jumpserver.wsgi.applications'
LOGIN_REDIRECT_URL = reverse_lazy('index')
LOGIN_URL = reverse_lazy('users:login')
LOGIN_URL = reverse_lazy('authentication:login')
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN
@ -343,10 +343,10 @@ REST_FRAMEWORK = {
),
'DEFAULT_AUTHENTICATION_CLASSES': (
# 'rest_framework.authentication.BasicAuthentication',
'users.authentication.AccessKeyAuthentication',
'users.authentication.AccessTokenAuthentication',
'users.authentication.PrivateTokenAuthentication',
'users.authentication.SessionAuthentication',
'authentication.backends.api.AccessKeyAuthentication',
'authentication.backends.api.AccessTokenAuthentication',
'authentication.backends.api.PrivateTokenAuthentication',
'authentication.backends.api.SessionAuthentication',
),
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
@ -395,7 +395,7 @@ AUTH_LDAP_CONNECTION_OPTIONS = {
}
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 1
AUTH_LDAP_ALWAYS_UPDATE_USER = True
AUTH_LDAP_BACKEND = 'authentication.ldap.backends.LDAPAuthorizationBackend'
AUTH_LDAP_BACKEND = 'authentication.backends.ldap.LDAPAuthorizationBackend'
if AUTH_LDAP:
AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND)
@ -409,18 +409,19 @@ AUTH_OPENID_REALM_NAME = CONFIG.AUTH_OPENID_REALM_NAME
AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID
AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET
AUTH_OPENID_BACKENDS = [
'authentication.openid.backends.OpenIDAuthorizationPasswordBackend',
'authentication.openid.backends.OpenIDAuthorizationCodeBackend',
'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend',
'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend',
]
if AUTH_OPENID:
LOGIN_URL = reverse_lazy("authentication:openid-login")
LOGIN_URL = reverse_lazy("authentication:openid:openid-login")
LOGIN_COMPLETE_URL = reverse_lazy("authentication:openid:openid-login-complete")
AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[0])
AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[1])
# Radius Auth
AUTH_RADIUS = CONFIG.AUTH_RADIUS
AUTH_RADIUS_BACKEND = 'authentication.radius.backends.RadiusBackend'
AUTH_RADIUS_BACKEND = 'authentication.backends.radius.RadiusBackend'
RADIUS_SERVER = CONFIG.RADIUS_SERVER
RADIUS_PORT = CONFIG.RADIUS_PORT
RADIUS_SECRET = CONFIG.RADIUS_SECRET

View File

@ -21,6 +21,7 @@ api_v1_patterns = [
path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')),
path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('settings/v1/', include('settings.urls.api_urls', namespace='api-settings')),
path('authentication/v1/', include('authentication.urls.api_urls', namespace='api-auth')),
]))
]

View File

@ -23,7 +23,7 @@ def on_org_create_or_update(sender, instance=None, created=False, **kwargs):
set_current_org(old_org)
if instance and not created:
instance.expire_cache()
instance.expire_user_cache()
@receiver(m2m_changed, sender=Organization.users.through)

View File

@ -0,0 +1,159 @@
# Generated by Django 2.1.7 on 2019-02-28 10:21
import common.utils.django
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import uuid
# Functions from the following migrations need manual copying.
# Move them and any dependencies into this file, then update the
# RunPython operations to refer to the local versions:
# perms.migrations.0005_migrate_data_20180411_1144
def migrate_node_permissions(apps, schema_editor):
node_perm_model = apps.get_model("perms", "NodePermission")
asset_perm_model = apps.get_model("perms", "AssetPermission")
db_alias = schema_editor.connection.alias
for old in node_perm_model.objects.using(db_alias).all():
perm = asset_perm_model.objects.using(db_alias).create(
name="{}-{}-{}".format(
old.node.value,
old.user_group.name,
old.system_user.name
),
is_active=old.is_active,
date_expired=old.date_expired,
created_by=old.date_expired,
date_created=old.date_created,
comment=old.comment,
)
perm.user_groups.add(old.user_group)
perm.nodes.add(old.node)
perm.system_users.add(old.system_user)
def migrate_system_assets_relation(apps, schema_editor):
system_user_model = apps.get_model("assets", "SystemUser")
db_alias = schema_editor.connection.alias
for s in system_user_model.objects.using(db_alias).all():
nodes = list(s.nodes.all())
s.nodes.set([])
s.nodes.set(nodes)
class Migration(migrations.Migration):
replaces = [('perms', '0002_auto_20171228_0025'), ('perms', '0003_auto_20180225_1815'), ('perms', '0004_auto_20180411_1135'), ('perms', '0005_migrate_data_20180411_1144'), ('perms', '0006_auto_20180606_1505'), ('perms', '0007_auto_20180807_1116'), ('perms', '0008_auto_20180816_1652'), ('perms', '0009_auto_20180903_1132')]
dependencies = [
('users', '0002_auto_20171225_1157'),
('assets', '0007_auto_20180225_1815'),
('assets', '0013_auto_20180411_1135'),
('users', '0004_auto_20180125_1218'),
('perms', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='assetpermission',
name='user_groups',
field=models.ManyToManyField(blank=True, related_name='asset_permissions', to='users.UserGroup', verbose_name='User group'),
),
migrations.AddField(
model_name='assetpermission',
name='users',
field=models.ManyToManyField(blank=True, related_name='asset_permissions', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
migrations.CreateModel(
name='NodePermission',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('date_expired', models.DateTimeField(default=common.utils.django.date_expired_default, verbose_name='Date expired')),
('created_by', models.CharField(blank=True, max_length=128, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('node', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Node', verbose_name='Node')),
('system_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser', verbose_name='System user')),
('user_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.UserGroup', verbose_name='User group')),
],
options={
'verbose_name': 'Asset permission',
},
),
migrations.AlterUniqueTogether(
name='nodepermission',
unique_together={('node', 'user_group', 'system_user')},
),
migrations.RemoveField(
model_name='assetpermission',
name='asset_groups',
),
migrations.AddField(
model_name='assetpermission',
name='date_start',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date start'),
),
migrations.AddField(
model_name='assetpermission',
name='nodes',
field=models.ManyToManyField(blank=True, related_name='granted_by_permissions', to='assets.Node', verbose_name='Nodes'),
),
migrations.RunPython(
code=migrate_node_permissions,
),
migrations.RunPython(
code=migrate_system_assets_relation,
),
migrations.AlterField(
model_name='assetpermission',
name='date_expired',
field=models.DateTimeField(db_index=True, default=common.utils.django.date_expired_default, verbose_name='Date expired'),
),
migrations.AlterField(
model_name='assetpermission',
name='date_start',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Date start'),
),
migrations.AddField(
model_name='assetpermission',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='nodepermission',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AlterField(
model_name='assetpermission',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='assetpermission',
unique_together={('org_id', 'name')},
),
migrations.AlterUniqueTogether(
name='nodepermission',
unique_together=set(),
),
migrations.AlterField(
model_name='assetpermission',
name='org_id',
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='nodepermission',
name='org_id',
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterModelOptions(
name='assetpermission',
options={'verbose_name': 'Asset permission'},
),
]

View File

@ -94,10 +94,10 @@
<li><a id="switch_user"><i class="fa fa-exchange"></i><span> {% trans 'User page' %}</span></a></li>
{% endif %}
{% endif %}
<li><a href="{% url 'users:logout' %}"><i class="fa fa-sign-out"></i> {% trans 'Logout' %}</a></li>
<li><a href="{% url 'authentication:logout' %}"><i class="fa fa-sign-out"></i> {% trans 'Logout' %}</a></li>
</ul>
{% else %}
<a href="{% url 'users:login' %}">
<a href="{% url 'authentication:login' %}">
<i class="fa fa-sign-in"></i>{% trans 'Login' %}
</a>
{% endif %}

View File

@ -0,0 +1,84 @@
# Generated by Django 2.1.7 on 2019-02-28 10:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
replaces = [('terminal', '0002_auto_20171228_0025'), ('terminal', '0003_auto_20171230_0308'), ('terminal', '0004_session_remote_addr'), ('terminal', '0005_auto_20180122_1154'), ('terminal', '0006_auto_20180123_1037'), ('terminal', '0007_session_date_last_active'), ('terminal', '0008_auto_20180307_1603'), ('terminal', '0009_auto_20180326_0957')]
dependencies = [
('terminal', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='session',
name='terminal',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='terminal.Terminal'),
),
migrations.AddField(
model_name='status',
name='terminal',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='terminal.Terminal'),
),
migrations.AddField(
model_name='task',
name='terminal',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='terminal.Terminal'),
),
migrations.AddField(
model_name='terminal',
name='user',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='terminal', to=settings.AUTH_USER_MODEL, verbose_name='Application User'),
),
migrations.AlterField(
model_name='terminal',
name='name',
field=models.CharField(max_length=32, verbose_name='Name'),
),
migrations.AlterField(
model_name='command',
name='asset',
field=models.CharField(db_index=True, max_length=128, verbose_name='Asset'),
),
migrations.AlterField(
model_name='command',
name='system_user',
field=models.CharField(db_index=True, max_length=64, verbose_name='System user'),
),
migrations.AlterField(
model_name='command',
name='user',
field=models.CharField(db_index=True, max_length=64, verbose_name='User'),
),
migrations.AddField(
model_name='session',
name='remote_addr',
field=models.CharField(blank=True, max_length=15, null=True, verbose_name='Remote addr'),
),
migrations.AddField(
model_name='terminal',
name='command_storage',
field=models.CharField(default='default', max_length=128, verbose_name='Command storage'),
),
migrations.AddField(
model_name='terminal',
name='replay_storage',
field=models.CharField(default='default', max_length=128, verbose_name='Replay storage'),
),
migrations.AddField(
model_name='session',
name='date_last_active',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date last active'),
),
migrations.AlterField(
model_name='session',
name='date_start',
field=models.DateTimeField(db_index=True, verbose_name='Date start'),
),
]

View File

@ -2,5 +2,4 @@
#
from .user import *
from .auth import *
from .group import *

View File

@ -1,9 +1,7 @@
# ~*~ coding: utf-8 ~*~
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.utils.translation import gettext_lazy as _
from captcha.fields import CaptchaField
from common.utils import validate_ssh_public_key
from orgs.mixins import OrgModelForm
@ -11,24 +9,6 @@ from orgs.utils import current_org
from .models import User, UserGroup
class UserLoginForm(AuthenticationForm):
username = forms.CharField(label=_('Username'), max_length=100)
password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput,
max_length=128, strip=False
)
def confirm_login_allowed(self, user):
if not user.is_staff:
raise forms.ValidationError(
self.error_messages['inactive'],
code='inactive',)
class UserLoginCaptchaForm(UserLoginForm):
captcha = CaptchaField()
class UserCheckPasswordForm(forms.Form):
username = forms.CharField(label=_('Username'), max_length=100)
password = forms.CharField(

View File

@ -4,10 +4,8 @@ from __future__ import unicode_literals
import common.utils
from django.contrib.auth.hashers import make_password
from django.conf import settings
import django.contrib.auth.models
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import uuid
@ -75,40 +73,6 @@ class Migration(migrations.Migration):
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='AccessKey',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='AccessKeyID')),
('secret', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='AccessKeySecret')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='access_key', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
),
migrations.CreateModel(
name='LoginLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('username', models.CharField(max_length=20, verbose_name='Username')),
('type', models.CharField(choices=[('W', 'Web'), ('T', 'Terminal')], max_length=2, verbose_name='Login type')),
('ip', models.GenericIPAddressField(verbose_name='Login ip')),
('city', models.CharField(blank=True, max_length=254, null=True, verbose_name='Login city')),
('user_agent', models.CharField(blank=True, max_length=254, null=True, verbose_name='User agent')),
('datetime', models.DateTimeField(auto_now_add=True, verbose_name='Date login')),
],
options={
'ordering': ['-datetime', 'username'],
},
),
migrations.CreateModel(
name='PrivateToken',
fields=[
('key', models.CharField(max_length=40, primary_key=True, serialize=False, verbose_name='Key')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='auth_token', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Private Token',
},
),
migrations.CreateModel(
name='UserGroup',
fields=[

View File

@ -0,0 +1,81 @@
# Generated by Django 2.1.7 on 2019-02-28 10:19
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [('users', '0002_auto_20171225_1157'), ('users', '0003_auto_20180101_0046'), ('users', '0004_auto_20180125_1218'), ('users', '0005_auto_20180306_1804'), ('users', '0006_auto_20180411_1135'), ('users', '0007_auto_20180419_1036'), ('users', '0008_auto_20180425_1516'), ('users', '0009_auto_20180517_1537')]
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(max_length=128, unique=True, verbose_name='Email'),
),
migrations.AlterField(
model_name='user',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='user',
name='username',
field=models.CharField(max_length=128, unique=True, verbose_name='Username'),
),
migrations.AlterField(
model_name='user',
name='wechat',
field=models.CharField(blank=True, max_length=128, verbose_name='Wechat'),
),
migrations.AlterField(
model_name='user',
name='is_first_login',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='usergroup',
name='created_by',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AlterModelOptions(
name='user',
options={'ordering': ['username'], 'verbose_name': 'User'},
),
migrations.AlterModelOptions(
name='usergroup',
options={'ordering': ['name'], 'verbose_name': 'User group'},
),
migrations.RenameField(
model_name='user',
old_name='secret_key_otp',
new_name='otp_secret_key',
),
migrations.RemoveField(
model_name='user',
name='enable_otp',
),
migrations.AddField(
model_name='user',
name='otp_level',
field=models.SmallIntegerField(choices=[(0, 'Disable'), (1, 'Enable'), (2, 'Force enable')], default=0, verbose_name='MFA'),
),
migrations.RemoveField(
model_name='user',
name='otp_secret_key',
),
migrations.AddField(
model_name='user',
name='_otp_secret_key',
field=models.CharField(blank=True, max_length=128, null=True),
),
migrations.AlterField(
model_name='usergroup',
name='name',
field=models.CharField(max_length=128, unique=True, verbose_name='Name'),
),
]

View File

@ -0,0 +1,77 @@
# Generated by Django 2.1.7 on 2019-02-28 10:20
import common.utils.django
from django.db import migrations, models
# Functions from the following migrations need manual copying.
# Move them and any dependencies into this file, then update the
# RunPython operations to refer to the local versions:
# users.migrations.0010_auto_20180606_1505
def remove_deleted_group(apps, schema_editor):
db_alias = schema_editor.connection.alias
group_model = apps.get_model("users", "UserGroup")
group_model.objects.using(db_alias).filter(is_discard=True).delete()
class Migration(migrations.Migration):
replaces = [('users', '0010_auto_20180606_1505'), ('users', '0011_user_source'), ('users', '0012_auto_20180710_1641'), ('users', '0013_auto_20180807_1116'), ('users', '0014_auto_20180816_1652'), ('users', '0015_auto_20181105_1112'), ('users', '0016_auto_20181109_1505'), ('users', '0017_auto_20181123_1113'), ('users', '0018_auto_20190107_1912')]
dependencies = [
('users', '0009_auto_20180517_1537'),
]
operations = [
migrations.RunPython(
code=remove_deleted_group,
),
migrations.RemoveField(
model_name='usergroup',
name='discard_time',
),
migrations.RemoveField(
model_name='usergroup',
name='is_discard',
),
migrations.AlterField(
model_name='user',
name='date_expired',
field=models.DateTimeField(blank=True, db_index=True, default=common.utils.django.date_expired_default, null=True, verbose_name='Date expired'),
),
migrations.AddField(
model_name='user',
name='source',
field=models.CharField(choices=[('local', 'Local'), ('ldap', 'LDAP/AD'), ('openid', 'OpenID'), ('radius', 'Radius')], default='local', max_length=30, verbose_name='Source'),
),
migrations.AddField(
model_name='usergroup',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AlterField(
model_name='user',
name='last_name',
field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
),
migrations.AlterField(
model_name='usergroup',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='usergroup',
unique_together={('org_id', 'name')},
),
migrations.AlterField(
model_name='usergroup',
name='org_id',
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AddField(
model_name='user',
name='date_password_last_updated',
field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date password last updated'),
),
]

View File

@ -12,19 +12,4 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AddField(
model_name='loginlog',
name='mfa',
field=models.SmallIntegerField(choices=[(0, 'Disabled'), (1, 'Enabled'), (2, '-')], default=2, verbose_name='MFA'),
),
migrations.AddField(
model_name='loginlog',
name='reason',
field=models.SmallIntegerField(choices=[(0, '-'), (1, 'Username/password check failed'), (2, 'MFA authentication failed')], default=0, verbose_name='Reason'),
),
migrations.AddField(
model_name='loginlog',
name='status',
field=models.BooleanField(choices=[(True, 'Success'), (False, 'Failed')], default=True, max_length=2, verbose_name='Status'),
),
]

View File

@ -10,14 +10,4 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AlterField(
model_name='loginlog',
name='reason',
field=models.SmallIntegerField(choices=[(0, '-'), (1, 'Username/password check failed'), (2, 'MFA authentication failed'), (3, 'Username does not exist')], default=0, verbose_name='Reason'),
),
migrations.AlterField(
model_name='loginlog',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
]

View File

@ -1,8 +1,6 @@
# Generated by Django 2.1.1 on 2018-11-23 03:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
@ -17,14 +15,4 @@ class Migration(migrations.Migration):
name='date_password_last_updated',
field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date password last updated'),
),
migrations.AlterField(
model_name='accesskey',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='access_keys', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
migrations.AlterField(
model_name='loginlog',
name='reason',
field=models.SmallIntegerField(choices=[(0, '-'), (1, 'Username/password check failed'), (2, 'MFA authentication failed'), (3, 'Username does not exist'), (4, 'Password expired')], default=0, verbose_name='Reason'),
),
]

View File

@ -4,5 +4,4 @@
from .user import *
from .group import *
from .authentication import *
from .utils import *

View File

@ -1,88 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token
from .user import User
__all__ = ['AccessKey', 'PrivateToken', 'LoginLog']
class AccessKey(models.Model):
id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True,
default=uuid.uuid4, editable=False)
secret = models.UUIDField(verbose_name='AccessKeySecret',
default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, verbose_name='User',
on_delete=models.CASCADE, related_name='access_keys')
def get_id(self):
return str(self.id)
def get_secret(self):
return str(self.secret)
def get_full_value(self):
return '{}:{}'.format(self.id, self.secret)
def __str__(self):
return str(self.id)
class PrivateToken(Token):
"""Inherit from auth token, otherwise migration is boring"""
class Meta:
verbose_name = _('Private Token')
class LoginLog(models.Model):
LOGIN_TYPE_CHOICE = (
('W', 'Web'),
('T', 'Terminal'),
)
MFA_DISABLED = 0
MFA_ENABLED = 1
MFA_UNKNOWN = 2
MFA_CHOICE = (
(MFA_DISABLED, _('Disabled')),
(MFA_ENABLED, _('Enabled')),
(MFA_UNKNOWN, _('-')),
)
REASON_NOTHING = 0
REASON_PASSWORD = 1
REASON_MFA = 2
REASON_NOT_EXIST = 3
REASON_PASSWORD_EXPIRED = 4
REASON_CHOICE = (
(REASON_NOTHING, _('-')),
(REASON_PASSWORD, _('Username/password check failed')),
(REASON_MFA, _('MFA authentication failed')),
(REASON_NOT_EXIST, _("Username does not exist")),
(REASON_PASSWORD_EXPIRED, _("Password expired")),
)
STATUS_CHOICE = (
(True, _('Success')),
(False, _('Failed'))
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
username = models.CharField(max_length=128, verbose_name=_('Username'))
type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type'))
ip = models.GenericIPAddressField(verbose_name=_('Login ip'))
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city'))
user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent'))
mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA'))
reason = models.SmallIntegerField(default=REASON_NOTHING, choices=REASON_CHOICE, verbose_name=_('Reason'))
status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status'))
datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('Date login'))
class Meta:
ordering = ['-datetime', 'username']

View File

@ -16,7 +16,6 @@ from django.utils import timezone
from django.shortcuts import reverse
from common.utils import get_signer, date_expired_default
from orgs.utils import current_org
__all__ = ['User']
@ -104,6 +103,8 @@ class User(AbstractUser):
verbose_name=_('Date password last updated')
)
user_cache_key_prefix = '_User_{}'
def __str__(self):
return '{0.name}({0.username})'.format(self)
@ -281,6 +282,7 @@ class User(AbstractUser):
self.role = 'Admin'
self.is_active = True
super().save(*args, **kwargs)
self.expire_user_cache()
@property
def private_token(self):
@ -422,8 +424,26 @@ class User(AbstractUser):
def delete(self, using=None, keep_parents=False):
if self.pk == 1 or self.username == 'admin':
return
self.expire_user_cache()
return super(User, self).delete()
def expire_user_cache(self):
key = self.user_cache_key_prefix.format(self.id)
cache.delete(key)
@classmethod
def get_user_or_from_cache(cls, uid):
key = cls.user_cache_key_prefix.format(uid)
user = cache.get(key)
if user:
return user
try:
user = cls.objects.get(id=uid)
cache.set(key, user, 3600)
except cls.DoesNotExist:
user = None
return user
class Meta:
ordering = ['username']
verbose_name = _("User")

View File

@ -2,15 +2,9 @@
#
from django.utils.translation import ugettext as _
from rest_framework import serializers
from ..models import User, AccessKey
from ..models import User
class AccessKeySerializer(serializers.ModelSerializer):
class Meta:
model = AccessKey
fields = ['id', 'secret']
read_only_fields = ['id', 'secret']
from authentication.serializers import AccessKeySerializer
class ServiceAccountSerializer(serializers.ModelSerializer):

View File

@ -2,4 +2,3 @@ from django.dispatch import Signal
post_user_create = Signal(providing_args=('user',))

View File

@ -9,18 +9,13 @@ from celery import shared_task
from ops.celery.utils import create_or_update_celery_periodic_tasks
from ops.celery.decorator import after_app_ready_start, register_as_period_task
from common.utils import get_logger
from .models import User, LoginLog
from .utils import write_login_log, send_password_expiration_reminder_mail
from .models import User
from .utils import send_password_expiration_reminder_mail
logger = get_logger(__file__)
@shared_task
def write_login_log_async(*args, **kwargs):
write_login_log(*args, **kwargs)
@shared_task
def check_password_expired():
users = User.objects.exclude(role=User.ROLE_APP)
@ -48,13 +43,4 @@ def check_password_expired_periodic():
create_or_update_celery_periodic_tasks(tasks)
@register_as_period_task(interval=3600*24)
@shared_task
def clean_login_log_period():
now = timezone.now()
try:
days = int(settings.LOGIN_LOG_KEEP_DAYS)
except ValueError:
days = 90
expired_day = now - datetime.timedelta(days=days)
LoginLog.objects.filter(datetime__lt=expired_day).delete()

View File

@ -88,7 +88,7 @@
<div class="hr-line-dashed"></div>
<p class="text-muted text-center">{% trans "More login options" %}</p>
<div>
<button type="button" class="btn btn-default btn-sm btn-block" onclick="location.href='{% url 'authentication:openid-login' %}'">
<button type="button" class="btn btn-default btn-sm btn-block" onclick="location.href='{% url 'authentication:openid:openid-login' %}'">
<i class="fa fa-openid"></i>
{% trans 'Keycloak' %}
</button>

View File

@ -5,6 +5,8 @@ from __future__ import absolute_import
from django.urls import path
from rest_framework_bulk.routes import BulkRouter
from authentication import api as auth_api
from .. import api
app_name = 'users'
@ -15,11 +17,12 @@ router.register(r'groups', api.UserGroupViewSet, 'user-group')
urlpatterns = [
# path('token/', api.UserToken.as_view(), name='user-token'),
path('connection-token/', api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('connection-token/', auth_api.UserConnectionTokenApi.as_view(),
name='connection-token'),
path('auth/', auth_api.UserAuthApi.as_view(), name='user-auth'),
path('otp/auth/', auth_api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'),
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'),
path('users/<uuid:pk>/otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'),
path('users/<uuid:pk>/password/', api.UserChangePasswordApi.as_view(), name='change-user-password'),

View File

@ -9,8 +9,6 @@ app_name = 'users'
urlpatterns = [
# Login view
path('login/', views.UserLoginView.as_view(), name='login'),
path('logout/', views.UserLogoutView.as_view(), name='logout'),
path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'),
path('password/forgot/', views.UserForgotPasswordView.as_view(), name='forgot-password'),
path('password/forgot/sendmail-success/', views.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'),
path('password/reset/', views.UserResetPasswordView.as_view(), name='reset-password'),
@ -48,7 +46,4 @@ urlpatterns = [
path('user-group/<uuid:pk>/update/', views.UserGroupUpdateView.as_view(), name='user-group-update'),
path('user-group/<uuid:pk>/assets/', views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'),
# Login log
# Abandon
path('login-log/', views.LoginLogListView.as_view(), name='login-log-list'),
]

View File

@ -7,7 +7,6 @@ import pyotp
import base64
import logging
import requests
import ipaddress
from django.http import Http404
from django.conf import settings
@ -18,8 +17,8 @@ from django.core.cache import cache
from datetime import datetime
from common.tasks import send_mail_async
from common.utils import reverse, get_object_or_none
from .models import User, LoginLog
from common.utils import reverse, get_object_or_none, get_ip_city
from .models import User
logger = logging.getLogger('jumpserver')
@ -63,7 +62,7 @@ def send_user_created_mail(user):
'rest_password_token': user.generate_reset_token(),
'forget_password_url': reverse('users:forgot-password', external=True),
'email': user.email,
'login_url': reverse('users:login', external=True),
'login_url': reverse('authentication:login', external=True),
}
if settings.DEBUG:
try:
@ -99,7 +98,7 @@ def send_reset_password_mail(user):
'rest_password_token': user.generate_reset_token(),
'forget_password_url': reverse('users:forgot-password', external=True),
'email': user.email,
'login_url': reverse('users:login', external=True),
'login_url': reverse('authentication:login', external=True),
}
if settings.DEBUG:
logger.debug(message)
@ -137,7 +136,7 @@ def send_password_expiration_reminder_mail(user):
'update_password_url': reverse('users:user-password-update', external=True),
'forget_password_url': reverse('users:forgot-password', external=True),
'email': user.email,
'login_url': reverse('users:login', external=True),
'login_url': reverse('authentication:login', external=True),
}
if settings.DEBUG:
logger.debug(message)
@ -159,7 +158,7 @@ def send_reset_ssh_key_mail(user):
</br>
""") % {
'name': user.name,
'login_url': reverse('users:login', external=True),
'login_url': reverse('authentication:login', external=True),
}
if settings.DEBUG:
logger.debug(message)
@ -199,51 +198,6 @@ def check_user_valid(**kwargs):
return None, _('Password or SSH public key invalid')
def validate_ip(ip):
try:
ipaddress.ip_address(ip)
return True
except ValueError:
pass
return False
def write_login_log(*args, **kwargs):
ip = kwargs.get('ip', '')
if not (ip and validate_ip(ip)):
ip = ip[:15]
city = "Unknown"
else:
city = get_ip_city(ip)
kwargs.update({'ip': ip, 'city': city})
LoginLog.objects.create(**kwargs)
def get_ip_city(ip, timeout=10):
# Taobao ip api: http://ip.taobao.com/service/getIpInfo.php?ip=8.8.8.8
# Sina ip api: http://int.dpool.sina.com.cn/iplookup/iplookup.php?ip=8.8.8.8&format=json
url = 'http://ip.taobao.com/service/getIpInfo.php?ip=%s' % ip
try:
r = requests.get(url, timeout=timeout)
except:
r = None
city = 'Unknown'
if r and r.status_code == 200:
try:
data = r.json()
if not isinstance(data, int) and data['code'] == 0:
country = data['data']['country']
_city = data['data']['city']
if country == 'XX':
city = _city
else:
city = ' '.join([country, _city])
except ValueError:
pass
return city
def get_user_or_tmp_user(request):
user = request.user
tmp_user = get_tmp_user_from_cache(request)

View File

@ -1,244 +1,35 @@
# ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals
import os
from django.core.cache import cache
from django.shortcuts import render
from django.contrib.auth import login as auth_login, logout as auth_logout
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView
from django.views.generic import RedirectView
from django.core.files.storage import default_storage
from django.http import HttpResponseRedirect, HttpResponse
from django.http import HttpResponseRedirect
from django.shortcuts import reverse, redirect
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView
from django.conf import settings
from django.urls import reverse_lazy
from formtools.wizard.views import SessionWizardView
from common.utils import get_object_or_none, get_request_ip
from ..models import User, LoginLog
from ..utils import send_reset_password_mail, check_otp_code, \
redirect_user_first_login_or_index, get_user_or_tmp_user, \
set_tmp_user_to_cache, get_password_check_rules, check_password_rules, \
is_block_login, increase_login_failed_count, clean_failed_count
from ..tasks import write_login_log_async
from common.utils import get_object_or_none
from ..models import User
from ..utils import (
send_reset_password_mail, get_password_check_rules, check_password_rules
)
from .. import forms
__all__ = [
'UserLoginView', 'UserLoginOtpView', 'UserLogoutView',
'UserForgotPasswordView', 'UserForgotPasswordSendmailSuccessView',
'UserResetPasswordView', 'UserResetPasswordSuccessView',
'UserFirstLoginView', 'LoginLogListView'
'UserLoginView', 'UserForgotPasswordSendmailSuccessView',
'UserResetPasswordSuccessView', 'UserResetPasswordSuccessView',
'UserResetPasswordView', 'UserForgotPasswordView', 'UserFirstLoginView',
]
@method_decorator(sensitive_post_parameters(), name='dispatch')
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(never_cache, name='dispatch')
class UserLoginView(FormView):
form_class = forms.UserLoginForm
form_class_captcha = forms.UserLoginCaptchaForm
redirect_field_name = 'next'
key_prefix_captcha = "_LOGIN_INVALID_{}"
def get_template_names(self):
template_name = 'users/login.html'
if not settings.XPACK_ENABLED:
return template_name
from xpack.plugins.license.models import License
if not License.has_valid_license():
return template_name
template_name = 'users/new_login.html'
return template_name
def get(self, request, *args, **kwargs):
if request.user.is_staff:
return redirect(redirect_user_first_login_or_index(
request, self.redirect_field_name)
)
request.session.set_test_cookie()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
# limit login authentication
ip = get_request_ip(request)
username = self.request.POST.get('username')
if is_block_login(username, ip):
return self.render_to_response(self.get_context_data(block_login=True))
return super().post(request, *args, **kwargs)
def form_valid(self, form):
if not self.request.session.test_cookie_worked():
return HttpResponse(_("Please enable cookies and try again."))
user = form.get_user()
# user password expired
if user.password_has_expired:
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_PASSWORD_EXPIRED,
'status': False
}
self.write_login_log(data)
return self.render_to_response(self.get_context_data(password_expired=True))
set_tmp_user_to_cache(self.request, user)
username = form.cleaned_data.get('username')
ip = get_request_ip(self.request)
# 登陆成功,清除缓存计数
clean_failed_count(username, ip)
return redirect(self.get_success_url())
def form_invalid(self, form):
# write login failed log
username = form.cleaned_data.get('username')
exist = User.objects.filter(username=username).first()
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
data = {
'username': username,
'mfa': LoginLog.MFA_UNKNOWN,
'reason': reason,
'status': False
}
self.write_login_log(data)
# limit user login failed count
ip = get_request_ip(self.request)
increase_login_failed_count(username, ip)
# show captcha
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
old_form = form
form = self.form_class_captcha(data=form.data)
form._errors = old_form.errors
return super().form_invalid(form)
def get_form_class(self):
ip = get_request_ip(self.request)
if cache.get(self.key_prefix_captcha.format(ip)):
return self.form_class_captcha
else:
return self.form_class
def get_success_url(self):
user = get_user_or_tmp_user(self.request)
if user.otp_enabled and user.otp_secret_key:
# 1,2,mfa_setting & T
return reverse('users:login-otp')
elif user.otp_enabled and not user.otp_secret_key:
# 1,2,mfa_setting & F
return reverse('users:user-otp-enable-authentication')
elif not user.otp_enabled:
# 0 & T,F
auth_login(self.request, user)
data = {
'username': self.request.user.username,
'mfa': int(self.request.user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(data)
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def get_context_data(self, **kwargs):
context = {
'demo_mode': os.environ.get("DEMO_MODE"),
'AUTH_OPENID': settings.AUTH_OPENID,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def write_login_log(self, data):
login_ip = get_request_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
tmp_data = {
'ip': login_ip,
'type': 'W',
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)
class UserLoginOtpView(FormView):
template_name = 'users/login_otp.html'
form_class = forms.UserCheckOtpCodeForm
redirect_field_name = 'next'
def form_valid(self, form):
user = get_user_or_tmp_user(self.request)
otp_code = form.cleaned_data.get('otp_code')
otp_secret_key = user.otp_secret_key
if check_otp_code(otp_secret_key, otp_code):
auth_login(self.request, user)
data = {
'username': self.request.user.username,
'mfa': int(self.request.user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(data)
return redirect(self.get_success_url())
else:
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_MFA,
'status': False
}
self.write_login_log(data)
form.add_error('otp_code', _('MFA code invalid, or ntp sync server time'))
return super().form_invalid(form)
def get_success_url(self):
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def write_login_log(self, data):
login_ip = get_request_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
tmp_data = {
'ip': login_ip,
'type': 'W',
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)
@method_decorator(never_cache, name='dispatch')
class UserLogoutView(TemplateView):
template_name = 'flash_message_standalone.html'
def get(self, request, *args, **kwargs):
auth_logout(request)
next_uri = request.COOKIES.get("next")
if next_uri:
return redirect(next_uri)
response = super().get(request, *args, **kwargs)
return response
def get_context_data(self, **kwargs):
context = {
'title': _('Logout success'),
'messages': _('Logout success, return login page'),
'interval': 1,
'redirect_url': reverse('users:login'),
'auto_redirect': True,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class UserLoginView(RedirectView):
urls = reverse_lazy('authentication:login')
class UserForgotPasswordView(TemplateView):
@ -267,7 +58,7 @@ class UserForgotPasswordSendmailSuccessView(TemplateView):
'title': _('Send reset password message'),
'messages': _('Send reset password mail success, '
'login your mail box and follow it '),
'redirect_url': reverse('users:login'),
'redirect_url': reverse('authentication:login'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
@ -280,7 +71,7 @@ class UserResetPasswordSuccessView(TemplateView):
context = {
'title': _('Reset password success'),
'messages': _('Reset password success, return to login page'),
'redirect_url': reverse('users:login'),
'redirect_url': reverse('authentication:login'),
'auto_redirect': True,
}
kwargs.update(context)
@ -386,8 +177,3 @@ class UserFirstLoginView(LoginRequiredMixin, SessionWizardView):
form.fields["otp_level"].initial = self.request.user.otp_level
return form
class LoginLogListView(ListView):
def get(self, request, *args, **kwargs):
return redirect(reverse('audits:login-log-list'))

View File

@ -574,7 +574,7 @@ class UserOtpSettingsSuccessView(TemplateView):
'title': title,
'messages': describe,
'interval': 1,
'redirect_url': reverse('users:login'),
'redirect_url': reverse('authentication:login'),
'auto_redirect': True,
}
kwargs.update(context)

View File

@ -17,7 +17,7 @@ decorator==4.1.2
Django==2.1.7
django-auth-ldap==1.7.0
django-bootstrap3==9.1.0
django-celery-beat==1.1.1
django-celery-beat==1.4.0
django-filter==2.0.0
django-formtools==2.1
django-ranged-response==0.2.0
@ -79,3 +79,4 @@ rest_condition==1.0.3
python-ldap==3.1.0
tencentcloud-sdk-python==3.0.40
django-radius==1.3.3
ipip-ipdb==1.2.1