From dc841650cf5e6fb6eba98c690e6eb6dbe4929201 Mon Sep 17 00:00:00 2001
From: wangruidong <940853815@qq.com>
Date: Tue, 31 Oct 2023 14:15:07 +0800
Subject: [PATCH] =?UTF-8?q?perf:=20AKSK=E6=B7=BB=E5=8A=A0=E8=AE=BF?=
 =?UTF-8?q?=E9=97=AEIP=E6=8E=A7=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/authentication/backends/drf.py           | 13 ++++++-
 .../migrations/0024_accesskey_ip_group.py     | 19 ++++++++++
 apps/authentication/models/access_key.py      |  5 +++
 apps/authentication/serializers/token.py      |  8 +++-
 apps/common/auth/signature.py                 | 12 +++++-
 apps/locale/ja/LC_MESSAGES/django.po          | 38 ++++++++++++-------
 apps/locale/zh/LC_MESSAGES/django.po          | 37 +++++++++++-------
 7 files changed, 100 insertions(+), 32 deletions(-)
 create mode 100644 apps/authentication/migrations/0024_accesskey_ip_group.py

diff --git a/apps/authentication/backends/drf.py b/apps/authentication/backends/drf.py
index 4ba879cc2..3822ec4a5 100644
--- a/apps/authentication/backends/drf.py
+++ b/apps/authentication/backends/drf.py
@@ -8,7 +8,7 @@ from django.utils.translation import gettext as _
 from rest_framework import authentication, exceptions
 
 from common.auth import signature
-from common.utils import get_object_or_none
+from common.utils import get_object_or_none, get_request_ip_or_data, contains_ip
 from ..models import AccessKey, PrivateToken
 
 
@@ -122,3 +122,14 @@ class SignatureAuthentication(signature.SignatureAuthentication):
             return user, secret
         except (AccessKey.DoesNotExist, exceptions.ValidationError):
             return None, None
+
+    def is_ip_allow(self, key_id, request):
+        try:
+            ak = AccessKey.objects.get(id=key_id)
+            ip_group = ak.ip_group
+            ip = get_request_ip_or_data(request)
+            if not contains_ip(ip, ip_group):
+                return False
+            return True
+        except (AccessKey.DoesNotExist, exceptions.ValidationError):
+            return False
diff --git a/apps/authentication/migrations/0024_accesskey_ip_group.py b/apps/authentication/migrations/0024_accesskey_ip_group.py
new file mode 100644
index 000000000..ba26ff6af
--- /dev/null
+++ b/apps/authentication/migrations/0024_accesskey_ip_group.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.1.10 on 2023-10-31 05:37
+
+import authentication.models.access_key
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('authentication', '0023_auto_20231010_1101'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='accesskey',
+            name='ip_group',
+            field=models.JSONField(default=authentication.models.access_key.defatult_ip_group, verbose_name='IP group'),
+        ),
+    ]
diff --git a/apps/authentication/models/access_key.py b/apps/authentication/models/access_key.py
index 5d9571569..51e5b8849 100644
--- a/apps/authentication/models/access_key.py
+++ b/apps/authentication/models/access_key.py
@@ -12,9 +12,14 @@ def default_secret():
     return random_string(36)
 
 
+def defatult_ip_group():
+    return ["*"]
+
+
 class AccessKey(models.Model):
     id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, default=uuid.uuid4, editable=False)
     secret = models.CharField(verbose_name='AccessKeySecret', default=default_secret, max_length=36)
+    ip_group = models.JSONField(default=defatult_ip_group, verbose_name=_('IP group'))
     user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User',
                              on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='access_keys')
     is_active = models.BooleanField(default=True, verbose_name=_('Active'))
diff --git a/apps/authentication/serializers/token.py b/apps/authentication/serializers/token.py
index e4dab13a5..6cd3af912 100644
--- a/apps/authentication/serializers/token.py
+++ b/apps/authentication/serializers/token.py
@@ -4,6 +4,7 @@ from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
 from rest_framework import serializers
 
+from acls.serializers.rules import ip_group_child_validator, ip_group_help_text
 from common.utils import get_object_or_none, random_string
 from users.models import User
 from users.serializers import UserProfileSerializer
@@ -17,9 +18,14 @@ __all__ = [
 
 
 class AccessKeySerializer(serializers.ModelSerializer):
+    ip_group = serializers.ListField(
+        default=['*'], label=_('AccessIP'), help_text=ip_group_help_text,
+        child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator])
+    )
+
     class Meta:
         model = AccessKey
-        fields = ['id', 'is_active', 'date_created', 'date_last_used']
+        fields = ['id', 'is_active', 'date_created', 'date_last_used'] + ['ip_group']
         read_only_fields = ['id', 'date_created', 'date_last_used']
 
 
diff --git a/apps/common/auth/signature.py b/apps/common/auth/signature.py
index 9661b9620..c1e392f30 100644
--- a/apps/common/auth/signature.py
+++ b/apps/common/auth/signature.py
@@ -17,6 +17,7 @@ Reusing failure exceptions serves several purposes:
 
 """
 FAILED = exceptions.AuthenticationFailed('Invalid signature.')
+IP_NOT_ALLOW = exceptions.AuthenticationFailed('Ip is not in access ip list.')
 
 
 class SignatureAuthentication(authentication.BaseAuthentication):
@@ -43,6 +44,9 @@ class SignatureAuthentication(authentication.BaseAuthentication):
         """Returns a tuple (User, secret) or (None, None)."""
         raise NotImplementedError()
 
+    def is_ip_allow(self, key_id, request):
+        raise NotImplementedError()
+
     def authenticate_header(self, request):
         """
         DRF sends this for unauthenticated responses if we're the primary
@@ -50,7 +54,7 @@ class SignatureAuthentication(authentication.BaseAuthentication):
         """
         h = " ".join(self.required_headers)
         return 'Signature realm="%s",headers="%s"' % (
-        self.www_authenticate_realm, h)
+            self.www_authenticate_realm, h)
 
     def authenticate(self, request):
         """
@@ -78,15 +82,19 @@ class SignatureAuthentication(authentication.BaseAuthentication):
         if len({"keyid", "algorithm", "signature"} - set(fields.keys())) > 0:
             raise FAILED
 
+        key_id = fields["keyid"]
         # Fetch the secret associated with the keyid
         user, secret = self.fetch_user_data(
-            fields["keyid"],
+            key_id,
             algorithm=fields["algorithm"]
         )
 
         if not (user and secret):
             raise FAILED
 
+        if not self.is_ip_allow(key_id, request):
+            raise IP_NOT_ALLOW
+
         # Gather all request headers and translate them as stated in the Django docs:
         # https://docs.djangoproject.com/en/1.6/ref/request-response/#django.http.HttpRequest.META
         headers = {}
diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po
index e32694415..0f9d4b4a6 100644
--- a/apps/locale/ja/LC_MESSAGES/django.po
+++ b/apps/locale/ja/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-10-30 11:28+0800\n"
+"POT-Creation-Date: 2023-10-31 14:04+0800\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -41,7 +41,7 @@ msgstr "パスワード"
 msgid "SSH key"
 msgstr "SSH キー"
 
-#: accounts/const/account.py:8 authentication/models/access_key.py:37
+#: accounts/const/account.py:8 authentication/models/access_key.py:42
 msgid "Access key"
 msgstr "アクセスキー"
 
@@ -843,7 +843,7 @@ msgstr "关联平台,可以配置推送参数,如果不关联,则使用默
 #: assets/models/group.py:20 common/db/models.py:36 ops/models/adhoc.py:26
 #: ops/models/job.py:145 ops/models/playbook.py:31 rbac/models/role.py:37
 #: settings/models.py:37 terminal/models/applet/applet.py:45
-#: terminal/models/applet/applet.py:321 terminal/models/applet/host.py:143
+#: terminal/models/applet/applet.py:317 terminal/models/applet/host.py:143
 #: terminal/models/component/endpoint.py:24
 #: terminal/models/component/endpoint.py:104
 #: terminal/models/session/session.py:46 tickets/models/comment.py:32
@@ -1014,7 +1014,7 @@ msgstr "1-100、低い値は最初に一致します"
 msgid "Reviewers"
 msgstr "レビュー担当者"
 
-#: acls/models/base.py:43 authentication/models/access_key.py:20
+#: acls/models/base.py:43 authentication/models/access_key.py:25
 #: authentication/models/connection_token.py:53
 #: authentication/templates/authentication/_access_key_modal.html:32
 #: perms/models/asset_permission.py:81 terminal/models/session/sharing.py:29
@@ -1712,7 +1712,7 @@ msgstr "アセットの自動化タスク"
 
 #: assets/models/automations/base.py:113 audits/models.py:207
 #: audits/serializers.py:51 ops/models/base.py:49 ops/models/job.py:220
-#: terminal/models/applet/applet.py:320 terminal/models/applet/host.py:140
+#: terminal/models/applet/applet.py:316 terminal/models/applet/host.py:140
 #: terminal/models/component/status.py:30 terminal/serializers/applet.py:18
 #: terminal/serializers/applet_host.py:124 tickets/models/ticket/general.py:283
 #: tickets/serializers/super_ticket.py:13
@@ -2653,7 +2653,7 @@ msgid "Added on"
 msgstr "に追加"
 
 #: authentication/backends/passkey/models.py:14
-#: authentication/models/access_key.py:21
+#: authentication/models/access_key.py:26
 #: authentication/models/private_token.py:8
 msgid "Date last used"
 msgstr "最後に使用した日付"
@@ -2956,6 +2956,11 @@ msgstr "MFAタイプ ({}) が有効になっていない"
 msgid "Please change your password"
 msgstr "パスワードを変更してください"
 
+#: authentication/models/access_key.py:22
+#: terminal/models/component/endpoint.py:95
+msgid "IP group"
+msgstr "IP グループ"
+
 #: authentication/models/connection_token.py:38
 #: terminal/serializers/storage.py:113
 msgid "Account name"
@@ -3108,7 +3113,13 @@ msgstr "メール"
 msgid "The {} cannot be empty"
 msgstr "{} 空にしてはならない"
 
-#: authentication/serializers/token.py:86 perms/serializers/permission.py:37
+#: authentication/serializers/token.py:23
+#, fuzzy
+#| msgid "Access key"
+msgid "AccessIP"
+msgstr "アクセスキー"
+
+#: authentication/serializers/token.py:93 perms/serializers/permission.py:37
 #: perms/serializers/permission.py:59 users/serializers/user.py:98
 #: users/serializers/user.py:168
 msgid "Is valid"
@@ -4615,7 +4626,7 @@ msgid "My assets"
 msgstr "私の資産"
 
 #: rbac/tree.py:56 terminal/models/applet/applet.py:52
-#: terminal/models/applet/applet.py:317 terminal/models/applet/host.py:30
+#: terminal/models/applet/applet.py:313 terminal/models/applet/host.py:30
 #: terminal/serializers/applet.py:15
 msgid "Applet"
 msgstr "リモートアプリケーション"
@@ -6346,7 +6357,7 @@ msgstr "カスタムプラットフォームのみをサポート"
 msgid "Missing type in platform.yml"
 msgstr "platform.ymlにタイプがありません"
 
-#: terminal/models/applet/applet.py:319 terminal/models/applet/host.py:36
+#: terminal/models/applet/applet.py:315 terminal/models/applet/host.py:36
 #: terminal/models/applet/host.py:138
 msgid "Hosting"
 msgstr "ホスト マシン"
@@ -6425,10 +6436,6 @@ msgstr "Redis ポート"
 msgid "Endpoint"
 msgstr "エンドポイント"
 
-#: terminal/models/component/endpoint.py:95
-msgid "IP group"
-msgstr "IP グループ"
-
 #: terminal/models/component/endpoint.py:108
 msgid "Endpoint rule"
 msgstr "エンドポイントルール"
@@ -6732,7 +6739,10 @@ msgid ""
 "Connect to the host using the same account first. For security reasons, "
 "please set the configuration item CACHE_LOGIN_PASSWORD_ENABLED=true and "
 "restart the service to enable it."
-msgstr "同じアカウントを使用してホストに接続します。セキュリティ上の理由から、構成項目 CACHE_LOGIN_PASSWORD_ENABLED=true を設定してサービスを再起動して有効にしてください。"
+msgstr ""
+"同じアカウントを使用してホストに接続します。セキュリティ上の理由から、構成項"
+"目 CACHE_LOGIN_PASSWORD_ENABLED=true を設定してサービスを再起動して有効にして"
+"ください。"
 
 #: terminal/serializers/command.py:19
 msgid "Session ID"
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 5a066450d..07c704639 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: JumpServer 0.3.3\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-10-30 11:28+0800\n"
+"POT-Creation-Date: 2023-10-31 14:04+0800\n"
 "PO-Revision-Date: 2021-05-20 10:54+0800\n"
 "Last-Translator: ibuler <ibuler@qq.com>\n"
 "Language-Team: JumpServer team<ibuler@qq.com>\n"
@@ -40,7 +40,7 @@ msgstr "密码"
 msgid "SSH key"
 msgstr "SSH 密钥"
 
-#: accounts/const/account.py:8 authentication/models/access_key.py:37
+#: accounts/const/account.py:8 authentication/models/access_key.py:42
 msgid "Access key"
 msgstr "Access key"
 
@@ -841,7 +841,7 @@ msgstr "关联平台,可配置推送参数,如果不关联,将使用默认
 #: assets/models/group.py:20 common/db/models.py:36 ops/models/adhoc.py:26
 #: ops/models/job.py:145 ops/models/playbook.py:31 rbac/models/role.py:37
 #: settings/models.py:37 terminal/models/applet/applet.py:45
-#: terminal/models/applet/applet.py:321 terminal/models/applet/host.py:143
+#: terminal/models/applet/applet.py:317 terminal/models/applet/host.py:143
 #: terminal/models/component/endpoint.py:24
 #: terminal/models/component/endpoint.py:104
 #: terminal/models/session/session.py:46 tickets/models/comment.py:32
@@ -1011,7 +1011,7 @@ msgstr "优先级可选范围为 1-100 (数值越小越优先)"
 msgid "Reviewers"
 msgstr "审批人"
 
-#: acls/models/base.py:43 authentication/models/access_key.py:20
+#: acls/models/base.py:43 authentication/models/access_key.py:25
 #: authentication/models/connection_token.py:53
 #: authentication/templates/authentication/_access_key_modal.html:32
 #: perms/models/asset_permission.py:81 terminal/models/session/sharing.py:29
@@ -1705,7 +1705,7 @@ msgstr "资产自动化任务"
 
 #: assets/models/automations/base.py:113 audits/models.py:207
 #: audits/serializers.py:51 ops/models/base.py:49 ops/models/job.py:220
-#: terminal/models/applet/applet.py:320 terminal/models/applet/host.py:140
+#: terminal/models/applet/applet.py:316 terminal/models/applet/host.py:140
 #: terminal/models/component/status.py:30 terminal/serializers/applet.py:18
 #: terminal/serializers/applet_host.py:124 tickets/models/ticket/general.py:283
 #: tickets/serializers/super_ticket.py:13
@@ -2632,7 +2632,7 @@ msgid "Added on"
 msgstr "附加"
 
 #: authentication/backends/passkey/models.py:14
-#: authentication/models/access_key.py:21
+#: authentication/models/access_key.py:26
 #: authentication/models/private_token.py:8
 msgid "Date last used"
 msgstr "最后使用日期"
@@ -2925,6 +2925,11 @@ msgstr "该 MFA ({}) 方式没有启用"
 msgid "Please change your password"
 msgstr "请修改密码"
 
+#: authentication/models/access_key.py:22
+#: terminal/models/component/endpoint.py:95
+msgid "IP group"
+msgstr "IPグループ"
+
 #: authentication/models/connection_token.py:38
 #: terminal/serializers/storage.py:113
 msgid "Account name"
@@ -3077,7 +3082,13 @@ msgstr "邮箱"
 msgid "The {} cannot be empty"
 msgstr "{} 不能为空"
 
-#: authentication/serializers/token.py:86 perms/serializers/permission.py:37
+#: authentication/serializers/token.py:23
+#, fuzzy
+#| msgid "Access key"
+msgid "AccessIP"
+msgstr "アクセスIP"
+
+#: authentication/serializers/token.py:93 perms/serializers/permission.py:37
 #: perms/serializers/permission.py:59 users/serializers/user.py:98
 #: users/serializers/user.py:168
 msgid "Is valid"
@@ -4563,7 +4574,7 @@ msgid "My assets"
 msgstr "我的资产"
 
 #: rbac/tree.py:56 terminal/models/applet/applet.py:52
-#: terminal/models/applet/applet.py:317 terminal/models/applet/host.py:30
+#: terminal/models/applet/applet.py:313 terminal/models/applet/host.py:30
 #: terminal/serializers/applet.py:15
 msgid "Applet"
 msgstr "远程应用"
@@ -6256,7 +6267,7 @@ msgstr "只支持自定义平台"
 msgid "Missing type in platform.yml"
 msgstr "在 platform.yml 中缺少类型"
 
-#: terminal/models/applet/applet.py:319 terminal/models/applet/host.py:36
+#: terminal/models/applet/applet.py:315 terminal/models/applet/host.py:36
 #: terminal/models/applet/host.py:138
 msgid "Hosting"
 msgstr "宿主机"
@@ -6333,10 +6344,6 @@ msgstr "Redis 端口"
 msgid "Endpoint"
 msgstr "端点"
 
-#: terminal/models/component/endpoint.py:95
-msgid "IP group"
-msgstr "IP 组"
-
 #: terminal/models/component/endpoint.py:108
 msgid "Endpoint rule"
 msgstr "端点规则"
@@ -6637,7 +6644,9 @@ msgid ""
 "Connect to the host using the same account first. For security reasons, "
 "please set the configuration item CACHE_LOGIN_PASSWORD_ENABLED=true and "
 "restart the service to enable it."
-msgstr "优先使用同名账号连接发布机。为了安全,需配置文件中开启配置 CACHE_LOGIN_PASSWORD_ENABLED=true, 修改后重启服务"
+msgstr ""
+"优先使用同名账号连接发布机。为了安全,需配置文件中开启配置 "
+"CACHE_LOGIN_PASSWORD_ENABLED=true, 修改后重启服务"
 
 #: terminal/serializers/command.py:19
 msgid "Session ID"