diff --git a/apps/accounts/migrations/0002_auto_20220616_0021.py b/apps/accounts/migrations/0002_auto_20220616_0021.py index 5693689cb..cef9c230f 100644 --- a/apps/accounts/migrations/0002_auto_20220616_0021.py +++ b/apps/accounts/migrations/0002_auto_20220616_0021.py @@ -28,7 +28,7 @@ class Migration(migrations.Migration): ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('name', models.CharField(max_length=128, verbose_name='Name')), - ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic run')), ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Interval')), ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Crontab')), ('types', models.JSONField(default=list)), diff --git a/apps/assets/migrations/0084_auto_20220112_1959.py b/apps/assets/migrations/0084_auto_20220112_1959.py index 0660edb75..71951bad3 100644 --- a/apps/assets/migrations/0084_auto_20220112_1959.py +++ b/apps/assets/migrations/0084_auto_20220112_1959.py @@ -20,7 +20,7 @@ class Migration(migrations.Migration): fields=[ ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('name', models.CharField(max_length=128, verbose_name='Name')), - ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic run')), ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Interval')), ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Crontab')), ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), diff --git a/apps/assets/migrations/0107_automation.py b/apps/assets/migrations/0107_automation.py index 1a44f6dd1..e5909b104 100644 --- a/apps/assets/migrations/0107_automation.py +++ b/apps/assets/migrations/0107_automation.py @@ -24,7 +24,7 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('name', models.CharField(max_length=128, verbose_name='Name')), - ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic run')), ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Interval')), ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Crontab')), ('accounts', models.JSONField(default=list, verbose_name='Accounts')), diff --git a/apps/i18n/core/en/LC_MESSAGES/django.mo b/apps/i18n/core/en/LC_MESSAGES/django.mo index 9bf456726..ed2c9e9e5 100644 --- a/apps/i18n/core/en/LC_MESSAGES/django.mo +++ b/apps/i18n/core/en/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16cedef767e949250b792d7f5921071b001545fc17cff4be093cd5faf5a20176 -size 2453 +oid sha256:d25379bd2019d09bc9608414a5516b3d19fac2f5a829f3dd845ffeb194599669 +size 2535 diff --git a/apps/i18n/core/en/LC_MESSAGES/django.po b/apps/i18n/core/en/LC_MESSAGES/django.po index d8f474ede..6224dfb70 100644 --- a/apps/i18n/core/en/LC_MESSAGES/django.po +++ b/apps/i18n/core/en/LC_MESSAGES/django.po @@ -2021,11 +2021,11 @@ msgstr "" #: assets/models/platform.py:107 assets/serializers/platform.py:166 msgid "Su enabled" -msgstr "" +msgstr "Switch enabled" #: assets/models/platform.py:108 assets/serializers/platform.py:144 msgid "Su method" -msgstr "" +msgstr "Switch method" #: assets/models/platform.py:109 assets/serializers/platform.py:147 msgid "Custom fields" diff --git a/apps/i18n/core/ja/LC_MESSAGES/django.mo b/apps/i18n/core/ja/LC_MESSAGES/django.mo new file mode 100644 index 000000000..fffc5e73a --- /dev/null +++ b/apps/i18n/core/ja/LC_MESSAGES/django.mo @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa826a4ec69f3ea3fb9250affe699f452857201b8e48065038479b9b88d9bfc1 +size 167853 diff --git a/apps/i18n/core/ja/LC_MESSAGES/django.po b/apps/i18n/core/ja/LC_MESSAGES/django.po index 8d92e6fca..eaaeaebfc 100644 --- a/apps/i18n/core/ja/LC_MESSAGES/django.po +++ b/apps/i18n/core/ja/LC_MESSAGES/django.po @@ -4247,7 +4247,7 @@ msgstr "利用可能なプログラムポータルがありません" #: ops/mixin.py:23 ops/mixin.py:104 settings/serializers/auth/ldap.py:66 #, fuzzy -#| msgid "Periodic perform" +#| msgid "Periodic run" msgid "Periodic run" msgstr "定期的なパフォーマンス" diff --git a/apps/i18n/core/zh/LC_MESSAGES/django.mo b/apps/i18n/core/zh/LC_MESSAGES/django.mo new file mode 100644 index 000000000..fff0bea68 --- /dev/null +++ b/apps/i18n/core/zh/LC_MESSAGES/django.mo @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da4f0f84c01c061cbab1d2fff69f4cd84edb4bbb5088c83a83789a3149575b8f +size 138961 diff --git a/apps/i18n/lina/en.json b/apps/i18n/lina/en.json index 36f7ed0d4..cc196c255 100644 --- a/apps/i18n/lina/en.json +++ b/apps/i18n/lina/en.json @@ -440,8 +440,8 @@ "Expired": "Expiration Date", "Export": "Export", "ExportAll": "Export All", - "ExportOnlyFiltered": "Export Search Results Only", - "ExportOnlySelectedItems": "Export selected Items Only", + "ExportOnlyFiltered": "Export filtered items", + "ExportOnlySelectedItems": "Export selected items", "ExportRange": "Export Range", "FC": "Fusion Compute", "Failed": "Failed", @@ -826,7 +826,7 @@ "Receivers": "Receiver", "RecentLogin": "Recent Login", "RecentSession": "Recent sessions", - "RecentlyUsed": "Recently Used", + "RecentlyUsed": "Recently", "RecipientHelpText": "If both recipient A and B are set, the account's key will be split into two parts", "RecipientServer": "Receiving Server", "Reconnect": "Reconnect", diff --git a/apps/ops/migrations/0001_initial.py b/apps/ops/migrations/0001_initial.py index 94643ffa0..56a6030ba 100644 --- a/apps/ops/migrations/0001_initial.py +++ b/apps/ops/migrations/0001_initial.py @@ -59,7 +59,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), ('interval', models.IntegerField(blank=True, help_text='Units: seconds', null=True, verbose_name='Interval')), ('crontab', models.CharField(blank=True, help_text='5 * * * *', max_length=128, null=True, verbose_name='Crontab')), - ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic run')), ('callback', models.CharField(blank=True, max_length=128, null=True, verbose_name='Callback')), ('is_deleted', models.BooleanField(default=False)), ('comment', models.TextField(blank=True, verbose_name='Comment')), diff --git a/apps/ops/migrations/0023_auto_20220912_0021.py b/apps/ops/migrations/0023_auto_20220912_0021.py index 4079d88ab..15af282fe 100644 --- a/apps/ops/migrations/0023_auto_20220912_0021.py +++ b/apps/ops/migrations/0023_auto_20220912_0021.py @@ -60,7 +60,7 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic run')), ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Interval')), ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Crontab')), ('name', models.CharField(max_length=128, null=True, verbose_name='Name')), @@ -164,7 +164,7 @@ class Migration(migrations.Migration): ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic run')), ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Interval')), ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Crontab')), ('name', models.CharField(max_length=128, null=True, verbose_name='Name')), diff --git a/apps/ops/mixin.py b/apps/ops/mixin.py index d25409ada..9e884ee4b 100644 --- a/apps/ops/mixin.py +++ b/apps/ops/mixin.py @@ -22,12 +22,10 @@ class PeriodTaskModelMixin(models.Model): ) is_periodic = models.BooleanField(default=False, verbose_name=_("Periodic run")) interval = models.IntegerField( - default=24, null=True, blank=True, - verbose_name=_("Interval"), + default=24, null=True, blank=True, verbose_name=_("Interval"), ) crontab = models.CharField( - blank=True, max_length=128, - verbose_name=_("Crontab"), + blank=True, max_length=128, null=True, verbose_name=_("Crontab"), ) @abc.abstractmethod diff --git a/apps/settings/models.py b/apps/settings/models.py index d6cda11df..b39408098 100644 --- a/apps/settings/models.py +++ b/apps/settings/models.py @@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _ from common.db.models import JMSBaseModel from common.utils import signer, get_logger +from .signals import setting_changed logger = get_logger(__name__) @@ -84,6 +85,7 @@ class Setting(models.Model): if not item: return item.refresh_setting() + setting_changed.send(sender=cls, name=name, item=item) def refresh_setting(self): setattr(settings, self.name, self.cleaned_value) diff --git a/apps/settings/signals.py b/apps/settings/signals.py index ccc9cd2d5..b15fa74e9 100644 --- a/apps/settings/signals.py +++ b/apps/settings/signals.py @@ -1,3 +1,4 @@ from django.dispatch import Signal category_setting_updated = Signal() +setting_changed = Signal() diff --git a/apps/users/models/user.py b/apps/users/models/user.py index da433899c..dd95099c9 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -31,7 +31,7 @@ from ..signals import ( post_user_change_password, post_user_leave_org, pre_user_leave_org ) -__all__ = ['User', 'UserPasswordHistory'] +__all__ = ['User', 'UserPasswordHistory', ] logger = get_logger(__file__) @@ -737,20 +737,25 @@ class JSONFilterMixin: return models.Q(id__in=user_id) -class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterMixin, AbstractUser): - class Source(models.TextChoices): - local = 'local', _('Local') - ldap = 'ldap', 'LDAP/AD' - openid = 'openid', 'OpenID' - radius = 'radius', 'Radius' - cas = 'cas', 'CAS' - saml2 = 'saml2', 'SAML2' - oauth2 = 'oauth2', 'OAuth2' - wecom = 'wecom', _('WeCom') - dingtalk = 'dingtalk', _('DingTalk') - feishu = 'feishu', _('FeiShu') - slack = 'slack', _('Slack') - custom = 'custom', 'Custom' +class Source(models.TextChoices): + local = 'local', _('Local') + ldap = 'ldap', 'LDAP/AD' + openid = 'openid', 'OpenID' + radius = 'radius', 'Radius' + cas = 'cas', 'CAS' + saml2 = 'saml2', 'SAML2' + oauth2 = 'oauth2', 'OAuth2' + wecom = 'wecom', _('WeCom') + dingtalk = 'dingtalk', _('DingTalk') + feishu = 'feishu', _('FeiShu') + slack = 'slack', _('Slack') + custom = 'custom', 'Custom' + + +class SourceMixin: + source: str + _source_choices = [] + Source = Source SOURCE_BACKEND_MAPPING = { Source.local: [ @@ -793,6 +798,61 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM ] } + @classmethod + def get_sources_enabled(cls): + mapper = { + cls.Source.local: True, + cls.Source.ldap: settings.AUTH_LDAP, + cls.Source.openid: settings.AUTH_OPENID, + cls.Source.radius: settings.AUTH_RADIUS, + cls.Source.cas: settings.AUTH_CAS, + cls.Source.saml2: settings.AUTH_SAML2, + cls.Source.oauth2: settings.AUTH_OAUTH2, + cls.Source.wecom: settings.AUTH_WECOM, + cls.Source.feishu: settings.AUTH_FEISHU, + cls.Source.slack: settings.AUTH_SLACK, + cls.Source.dingtalk: settings.AUTH_DINGTALK, + cls.Source.custom: settings.AUTH_CUSTOM + } + return [str(k) for k, v in mapper.items() if v] + + @property + def source_display(self): + return self.get_source_display() + + @property + def is_local(self): + return self.source == self.Source.local.value + + @classmethod + def get_source_choices(cls): + if cls._source_choices: + return cls._source_choices + used = cls.objects.values_list('source', flat=True).order_by('source').distinct() + enabled_sources = cls.get_sources_enabled() + _choices = [] + for k, v in cls.Source.choices: + if k in enabled_sources or k in used: + _choices.append((k, v)) + cls._source_choices = _choices + return cls._source_choices + + @classmethod + def get_user_allowed_auth_backend_paths(cls, username): + if not settings.ONLY_ALLOW_AUTH_FROM_SOURCE or not username: + return None + user = cls.objects.filter(username=username).first() + if not user: + return None + return user.get_allowed_auth_backend_paths() + + def get_allowed_auth_backend_paths(self): + if not settings.ONLY_ALLOW_AUTH_FROM_SOURCE: + return None + return self.SOURCE_BACKEND_MAPPING.get(self.source, []) + + +class User(AuthMixin, SourceMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterMixin, AbstractUser): id = models.UUIDField(default=uuid.uuid4, primary_key=True) username = models.CharField( max_length=128, unique=True, verbose_name=_('Username') @@ -842,7 +902,6 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM ) created_by = models.CharField(max_length=30, default='', blank=True, verbose_name=_('Created by')) updated_by = models.CharField(max_length=30, default='', blank=True, verbose_name=_('Updated by')) - source = models.CharField(max_length=30, default=Source.local, choices=Source.choices, verbose_name=_('Source')) date_password_last_updated = models.DateTimeField( auto_now_add=True, blank=True, null=True, verbose_name=_('Date password last updated') @@ -850,13 +909,16 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM need_update_password = models.BooleanField( default=False, verbose_name=_('Need update password') ) - date_api_key_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date api key used')) - date_updated = models.DateTimeField(auto_now=True, verbose_name=_('Date updated')) + source = models.CharField( + max_length=30, default=Source.local, + choices=Source.choices, verbose_name=_('Source') + ) wecom_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('WeCom')) dingtalk_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('DingTalk')) feishu_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('FeiShu')) slack_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('Slack')) - + date_api_key_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date api key used')) + date_updated = models.DateTimeField(auto_now=True, verbose_name=_('Date updated')) DATE_EXPIRED_WARNING_DAYS = 5 def __str__(self): @@ -888,10 +950,6 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM def get_absolute_url(self): return reverse('users:user-detail', args=(self.id,)) - @property - def source_display(self): - return self.get_source_display() - @property def is_expired(self): if self.date_expired and self.date_expired < timezone.now(): @@ -899,6 +957,12 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM else: return False + def is_password_authenticate(self): + cas = self.Source.cas + saml2 = self.Source.saml2 + oauth2 = self.Source.oauth2 + return self.source not in [cas, saml2, oauth2] + @property def expired_remain_days(self): date_remain = self.date_expired - timezone.now() @@ -917,16 +981,6 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM return True return False - @property - def is_local(self): - return self.source == self.Source.local.value - - def is_password_authenticate(self): - cas = self.Source.cas - saml2 = self.Source.saml2 - oauth2 = self.Source.oauth2 - return self.source not in [cas, saml2, oauth2] - def set_required_attr_if_need(self): if not self.name: self.name = self.username @@ -985,20 +1039,6 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterM raise PermissionDenied(_('Can not delete admin user')) return super(User, self).delete(using=using, keep_parents=keep_parents) - @classmethod - def get_user_allowed_auth_backend_paths(cls, username): - if not settings.ONLY_ALLOW_AUTH_FROM_SOURCE or not username: - return None - user = cls.objects.filter(username=username).first() - if not user: - return None - return user.get_allowed_auth_backend_paths() - - def get_allowed_auth_backend_paths(self): - if not settings.ONLY_ALLOW_AUTH_FROM_SOURCE: - return None - return self.SOURCE_BACKEND_MAPPING.get(self.source, []) - class Meta: ordering = ['username'] verbose_name = _("User") diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 78b64bc8e..1467b4c2b 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -87,7 +87,7 @@ class UserSerializer(RolesSerializerMixin, ResourceLabelsMixin, CommonBulkModelS default=PasswordStrategy.email, allow_null=True, required=False, - label=_("Password strategy"), + label=_("Password option"), ) mfa_enabled = serializers.BooleanField(read_only=True, label=_("MFA enabled")) mfa_force_enabled = serializers.BooleanField( @@ -182,6 +182,16 @@ class UserSerializer(RolesSerializerMixin, ResourceLabelsMixin, CommonBulkModelS 'mfa_level': {'label': _("MFA level")}, } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_source_options() + + def set_source_options(self): + field = self.fields.get("source") + if not field: + return + field.choices = User.get_source_choices() + def validate_password(self, password): password_strategy = self.initial_data.get("password_strategy") if self.instance is None and password_strategy != PasswordStrategy.custom: diff --git a/apps/users/signal_handlers.py b/apps/users/signal_handlers.py index 8e1f92254..6e1f00304 100644 --- a/apps/users/signal_handlers.py +++ b/apps/users/signal_handlers.py @@ -15,9 +15,11 @@ from authentication.backends.oidc.signals import openid_create_or_update_user from authentication.backends.saml2.signals import saml2_create_or_update_user from common.const.crontab import CRONTAB_AT_PM_TWO from common.decorators import on_transaction_commit +from common.signals import django_ready from common.utils import get_logger from jumpserver.utils import get_current_request from ops.celery.decorator import register_as_period_task +from settings.signals import setting_changed from .models import User, UserPasswordHistory from .signals import post_user_create @@ -173,3 +175,16 @@ def clean_expired_user_session_period(): def user_logged_out_callback(sender, request, user, **kwargs): session_key = request.session.session_key UserSession.objects.filter(key=session_key).delete() + + +@receiver(setting_changed) +@on_transaction_commit +def on_auth_setting_changed_clear_source_choice(sender, name='', **kwargs): + print("Receive setting changed signal: {}".format(name)) + if name.startswith('AUTH_'): + User._source_choices = [] + + +@receiver(django_ready) +def on_django_ready_refresh_source(sender, **kwargs): + User._source_choices = []