mirror of https://github.com/jumpserver/jumpserver
commit
3c54c82ce9
|
@ -6,7 +6,6 @@ labels: 类型:需求
|
||||||
assignees:
|
assignees:
|
||||||
- ibuler
|
- ibuler
|
||||||
- baijiangjie
|
- baijiangjie
|
||||||
- wojiushixiaobai
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**请描述您的需求或者改进建议.**
|
**请描述您的需求或者改进建议.**
|
||||||
|
|
|
@ -2,11 +2,9 @@
|
||||||
name: Bug 提交
|
name: Bug 提交
|
||||||
about: 提交产品缺陷帮助我们更好的改进
|
about: 提交产品缺陷帮助我们更好的改进
|
||||||
title: "[Bug] "
|
title: "[Bug] "
|
||||||
labels: 类型:bug
|
labels: 类型:Bug
|
||||||
assignees:
|
assignees:
|
||||||
- wojiushixiaobai
|
|
||||||
- baijiangjie
|
- baijiangjie
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**JumpServer 版本( v2.28 之前的版本不再支持 )**
|
**JumpServer 版本( v2.28 之前的版本不再支持 )**
|
||||||
|
|
|
@ -4,9 +4,7 @@ about: 提出针对本项目安装部署、使用及其他方面的相关问题
|
||||||
title: "[Question] "
|
title: "[Question] "
|
||||||
labels: 类型:提问
|
labels: 类型:提问
|
||||||
assignees:
|
assignees:
|
||||||
- wojiushixiaobai
|
|
||||||
- baijiangjie
|
- baijiangjie
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**请描述您的问题.**
|
**请描述您的问题.**
|
||||||
|
|
|
@ -7,10 +7,10 @@ from rest_framework.status import HTTP_200_OK
|
||||||
from accounts import serializers
|
from accounts import serializers
|
||||||
from accounts.filters import AccountFilterSet
|
from accounts.filters import AccountFilterSet
|
||||||
from accounts.models import Account
|
from accounts.models import Account
|
||||||
|
from accounts.mixins import AccountRecordViewLogMixin
|
||||||
from assets.models import Asset, Node
|
from assets.models import Asset, Node
|
||||||
from common.api import ExtraFilterFieldsMixin
|
from common.api.mixin import ExtraFilterFieldsMixin
|
||||||
from common.permissions import UserConfirmation, ConfirmType, IsValidUser
|
from common.permissions import UserConfirmation, ConfirmType, IsValidUser
|
||||||
from common.views.mixins import RecordViewLogMixin
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||||
return Response(status=HTTP_200_OK)
|
return Response(status=HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
|
||||||
"""
|
"""
|
||||||
因为可能要导出所有账号,所以单独建立了一个 viewset
|
因为可能要导出所有账号,所以单独建立了一个 viewset
|
||||||
"""
|
"""
|
||||||
|
@ -115,7 +115,7 @@ class AssetAccountBulkCreateApi(CreateAPIView):
|
||||||
return Response(data=serializer.data, status=HTTP_200_OK)
|
return Response(data=serializer.data, status=HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, RecordViewLogMixin, ListAPIView):
|
class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixin, ListAPIView):
|
||||||
model = Account.history.model
|
model = Account.history.model
|
||||||
serializer_class = serializers.AccountHistorySerializer
|
serializer_class = serializers.AccountHistorySerializer
|
||||||
http_method_names = ['get', 'options']
|
http_method_names = ['get', 'options']
|
||||||
|
@ -143,4 +143,3 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, RecordViewLogMixin, List
|
||||||
return histories
|
return histories
|
||||||
histories = histories.exclude(history_id=latest_history.history_id)
|
histories = histories.exclude(history_id=latest_history.history_id)
|
||||||
return histories
|
return histories
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,9 @@ class AccountsTaskCreateAPI(CreateAPIView):
|
||||||
code = 'accounts.push_account'
|
code = 'accounts.push_account'
|
||||||
else:
|
else:
|
||||||
code = 'accounts.verify_account'
|
code = 'accounts.verify_account'
|
||||||
return request.user.has_perm(code)
|
has = request.user.has_perm(code)
|
||||||
|
if not has:
|
||||||
|
self.permission_denied(request)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
data = serializer.validated_data
|
data = serializer.validated_data
|
||||||
|
@ -44,6 +46,6 @@ class AccountsTaskCreateAPI(CreateAPIView):
|
||||||
|
|
||||||
def get_exception_handler(self):
|
def get_exception_handler(self):
|
||||||
def handler(e, context):
|
def handler(e, context):
|
||||||
return Response({"error": str(e)}, status=400)
|
return Response({"error": str(e)}, status=401)
|
||||||
|
|
||||||
return handler
|
return handler
|
||||||
|
|
|
@ -4,10 +4,10 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
from accounts import serializers
|
from accounts import serializers
|
||||||
from accounts.models import AccountTemplate
|
from accounts.models import AccountTemplate
|
||||||
|
from accounts.mixins import AccountRecordViewLogMixin
|
||||||
from assets.const import Protocol
|
from assets.const import Protocol
|
||||||
from common.drf.filters import BaseFilterSet
|
from common.drf.filters import BaseFilterSet
|
||||||
from common.permissions import UserConfirmation, ConfirmType
|
from common.permissions import UserConfirmation, ConfirmType
|
||||||
from common.views.mixins import RecordViewLogMixin
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ class AccountTemplateViewSet(OrgBulkModelViewSet):
|
||||||
return Response(data=serializer.data)
|
return Response(data=serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class AccountTemplateSecretsViewSet(RecordViewLogMixin, AccountTemplateViewSet):
|
class AccountTemplateSecretsViewSet(AccountRecordViewLogMixin, AccountTemplateViewSet):
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': serializers.AccountTemplateSecretSerializer,
|
'default': serializers.AccountTemplateSecretSerializer,
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,9 @@
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
ssl: "{{ jms_asset.spec_info.use_ssl | default('') }}"
|
||||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||||
connection_options:
|
connection_options:
|
||||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||||
register: db_info
|
register: db_info
|
||||||
|
@ -31,8 +31,8 @@
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||||
connection_options:
|
connection_options:
|
||||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||||
db: "{{ jms_asset.spec_info.db_name }}"
|
db: "{{ jms_asset.spec_info.db_name }}"
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||||
connection_options:
|
connection_options:
|
||||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||||
|
|
|
@ -11,6 +11,10 @@
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||||
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||||
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||||
filter: version
|
filter: version
|
||||||
register: db_info
|
register: db_info
|
||||||
|
|
||||||
|
@ -24,6 +28,10 @@
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||||
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||||
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret }}"
|
password: "{{ account.secret }}"
|
||||||
host: "%"
|
host: "%"
|
||||||
|
@ -37,4 +45,8 @@
|
||||||
login_password: "{{ account.secret }}"
|
login_password: "{{ account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||||
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||||
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||||
filter: version
|
filter: version
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
|
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: user_exist.query_results[0] | length != 0
|
when: user_exist.query_results[0] | length != 0
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
|
script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; CREATE USER {{ account.username }} FOR LOGIN {{ account.username }}; select @@version"
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: user_exist.query_results[0] | length == 0
|
when: user_exist.query_results[0] | length == 0
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||||
connection_options:
|
connection_options:
|
||||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||||
filter: users
|
filter: users
|
||||||
|
|
|
@ -10,6 +10,10 @@
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||||
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||||
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||||
filter: users
|
filter: users
|
||||||
register: db_info
|
register: db_info
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||||
connection_options:
|
connection_options:
|
||||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||||
register: db_info
|
register: db_info
|
||||||
|
@ -31,8 +31,8 @@
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||||
connection_options:
|
connection_options:
|
||||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||||
db: "{{ jms_asset.spec_info.db_name }}"
|
db: "{{ jms_asset.spec_info.db_name }}"
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||||
connection_options:
|
connection_options:
|
||||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||||
|
|
|
@ -11,6 +11,10 @@
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||||
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||||
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||||
filter: version
|
filter: version
|
||||||
register: db_info
|
register: db_info
|
||||||
|
|
||||||
|
@ -24,6 +28,10 @@
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||||
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||||
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret }}"
|
password: "{{ account.secret }}"
|
||||||
host: "%"
|
host: "%"
|
||||||
|
@ -37,4 +45,8 @@
|
||||||
login_password: "{{ account.secret }}"
|
login_password: "{{ account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||||
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||||
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||||
filter: version
|
filter: version
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
role_attr_flags: LOGIN
|
role_attr_flags: LOGIN
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: result is succeeded
|
when: result is succeeded
|
||||||
|
register: change_info
|
||||||
|
|
||||||
- name: Verify password
|
- name: Verify password
|
||||||
community.postgresql.postgresql_ping:
|
community.postgresql.postgresql_ping:
|
||||||
|
@ -42,3 +43,5 @@
|
||||||
when:
|
when:
|
||||||
- result is succeeded
|
- result is succeeded
|
||||||
- change_info is succeeded
|
- change_info is succeeded
|
||||||
|
register: result
|
||||||
|
failed_when: not result.is_available
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
|
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: user_exist.query_results[0] | length != 0
|
when: user_exist.query_results[0] | length != 0
|
||||||
register: change_info
|
register: change_info
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
name: '{{ jms_asset.spec_info.db_name }}'
|
name: '{{ jms_asset.spec_info.db_name }}'
|
||||||
script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
|
script: "CREATE LOGIN [{{ account.username }}] WITH PASSWORD = '{{ account.secret }}'; CREATE USER [{{ account.username }}] FOR LOGIN [{{ account.username }}]; select @@version"
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: user_exist.query_results[0] | length == 0
|
when: user_exist.query_results[0] | length == 0
|
||||||
register: change_info
|
register: change_info
|
||||||
|
|
|
@ -80,7 +80,7 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_runner_failed(self, runner, e):
|
def on_runner_failed(self, runner, e):
|
||||||
logger.error("Pust account error: ", e)
|
logger.error("Pust account error: {}".format(e))
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
if self.secret_type and not self.check_secret():
|
if self.secret_type and not self.check_secret():
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||||
connection_options:
|
connection_options:
|
||||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
|
|
@ -10,4 +10,8 @@
|
||||||
login_password: "{{ account.secret }}"
|
login_password: "{{ account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||||
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||||
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||||
filter: version
|
filter: version
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /usr/local/bin/python
|
||||||
|
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account
|
- name: Verify account
|
||||||
community.postgresql.postgresql_ping:
|
community.postgresql.postgresql_ping:
|
||||||
|
|
|
@ -4,11 +4,13 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from assets.const import Connectivity
|
from assets.const import Connectivity
|
||||||
from common.db.fields import TreeChoices
|
from common.db.fields import TreeChoices
|
||||||
|
|
||||||
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
|
|
||||||
DEFAULT_PASSWORD_LENGTH = 30
|
DEFAULT_PASSWORD_LENGTH = 30
|
||||||
DEFAULT_PASSWORD_RULES = {
|
DEFAULT_PASSWORD_RULES = {
|
||||||
'length': DEFAULT_PASSWORD_LENGTH,
|
'length': DEFAULT_PASSWORD_LENGTH,
|
||||||
'symbol_set': string_punctuation
|
'uppercase': True,
|
||||||
|
'lowercase': True,
|
||||||
|
'digit': True,
|
||||||
|
'symbol': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -41,8 +43,8 @@ class AutomationTypes(models.TextChoices):
|
||||||
|
|
||||||
|
|
||||||
class SecretStrategy(models.TextChoices):
|
class SecretStrategy(models.TextChoices):
|
||||||
custom = 'specific', _('Specific password')
|
custom = 'specific', _('Specific secret')
|
||||||
random = 'random', _('Random')
|
random = 'random', _('Random generate')
|
||||||
|
|
||||||
|
|
||||||
class SSHKeyStrategy(models.TextChoices):
|
class SSHKeyStrategy(models.TextChoices):
|
||||||
|
|
|
@ -113,7 +113,7 @@ class Migration(migrations.Migration):
|
||||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
('old_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Old secret')),
|
('old_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Old secret')),
|
||||||
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='New secret')),
|
||||||
('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')),
|
('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')),
|
||||||
('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')),
|
('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')),
|
||||||
('status', models.CharField(default='pending', max_length=16)),
|
('status', models.CharField(default='pending', max_length=16)),
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 4.1.10 on 2023-08-25 03:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0122_auto_20230803_1553'),
|
||||||
|
('accounts', '0014_virtualaccount'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accounttemplate',
|
||||||
|
name='auto_push',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Auto push'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accounttemplate',
|
||||||
|
name='platforms',
|
||||||
|
field=models.ManyToManyField(related_name='account_templates', to='assets.platform', verbose_name='Platforms', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accounttemplate',
|
||||||
|
name='push_params',
|
||||||
|
field=models.JSONField(default=dict, verbose_name='Push params'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accounttemplate',
|
||||||
|
name='secret_strategy',
|
||||||
|
field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.1.10 on 2023-09-18 08:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0015_auto_20230825_1120'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accounttemplate',
|
||||||
|
name='password_rules',
|
||||||
|
field=models.JSONField(default=dict, verbose_name='Password rules'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,75 @@
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from django.utils import translation
|
||||||
|
from django.utils.translation import gettext_noop
|
||||||
|
|
||||||
|
from audits.const import ActionChoices
|
||||||
|
from common.views.mixins import RecordViewLogMixin
|
||||||
|
from common.utils import i18n_fmt
|
||||||
|
|
||||||
|
|
||||||
|
class AccountRecordViewLogMixin(RecordViewLogMixin):
|
||||||
|
get_object: callable
|
||||||
|
get_queryset: callable
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _filter_params(params):
|
||||||
|
new_params = {}
|
||||||
|
need_pop_params = ('format', 'order')
|
||||||
|
for key, value in params.items():
|
||||||
|
if key in need_pop_params:
|
||||||
|
continue
|
||||||
|
if isinstance(value, list):
|
||||||
|
value = list(filter(None, value))
|
||||||
|
if value:
|
||||||
|
new_params[key] = value
|
||||||
|
return new_params
|
||||||
|
|
||||||
|
def get_resource_display(self, request):
|
||||||
|
query_params = dict(request.query_params)
|
||||||
|
params = self._filter_params(query_params)
|
||||||
|
|
||||||
|
spm_filter = params.pop("spm", None)
|
||||||
|
|
||||||
|
if not params and not spm_filter:
|
||||||
|
display_message = gettext_noop("Export all")
|
||||||
|
elif spm_filter:
|
||||||
|
display_message = gettext_noop("Export only selected items")
|
||||||
|
else:
|
||||||
|
query = ",".join(
|
||||||
|
["%s=%s" % (key, value) for key, value in params.items()]
|
||||||
|
)
|
||||||
|
display_message = i18n_fmt(gettext_noop("Export filtered: %s"), query)
|
||||||
|
return display_message
|
||||||
|
|
||||||
|
@property
|
||||||
|
def detail_msg(self):
|
||||||
|
return i18n_fmt(
|
||||||
|
gettext_noop('User %s view/export secret'), self.request.user
|
||||||
|
)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
list_func = getattr(super(), 'list')
|
||||||
|
if not callable(list_func):
|
||||||
|
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
|
response = list_func(request, *args, **kwargs)
|
||||||
|
with translation.override('en'):
|
||||||
|
resource_display = self.get_resource_display(request)
|
||||||
|
ids = [q.id for q in self.get_queryset()]
|
||||||
|
self.record_logs(
|
||||||
|
ids, ActionChoices.view, self.detail_msg, resource_display=resource_display
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
retrieve_func = getattr(super(), 'retrieve')
|
||||||
|
if not callable(retrieve_func):
|
||||||
|
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
|
response = retrieve_func(request, *args, **kwargs)
|
||||||
|
with translation.override('en'):
|
||||||
|
resource = self.get_object()
|
||||||
|
self.record_logs(
|
||||||
|
[resource.id], ActionChoices.view, self.detail_msg, resource=resource
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from .account import *
|
from .account import * # noqa
|
||||||
from .automations import *
|
from .base import * # noqa
|
||||||
from .base import *
|
from .automations import * # noqa
|
||||||
from .template import *
|
from .template import * # noqa
|
||||||
from .virtual import *
|
from .virtual import * # noqa
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from accounts.const import SSHKeyStrategy
|
||||||
|
from accounts.models import Account, SecretWithRandomMixin
|
||||||
from accounts.tasks import execute_account_automation_task
|
from accounts.tasks import execute_account_automation_task
|
||||||
from assets.models.automations import (
|
from assets.models.automations import (
|
||||||
BaseAutomation as AssetBaseAutomation,
|
BaseAutomation as AssetBaseAutomation,
|
||||||
AutomationExecution as AssetAutomationExecution
|
AutomationExecution as AssetAutomationExecution
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = ['AccountBaseAutomation', 'AutomationExecution']
|
__all__ = ['AccountBaseAutomation', 'AutomationExecution', 'ChangeSecretMixin']
|
||||||
|
|
||||||
|
|
||||||
class AccountBaseAutomation(AssetBaseAutomation):
|
class AccountBaseAutomation(AssetBaseAutomation):
|
||||||
|
@ -43,3 +46,40 @@ class AutomationExecution(AssetAutomationExecution):
|
||||||
from accounts.automations.endpoint import ExecutionManager
|
from accounts.automations.endpoint import ExecutionManager
|
||||||
manager = ExecutionManager(execution=self)
|
manager = ExecutionManager(execution=self)
|
||||||
return manager.run()
|
return manager.run()
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeSecretMixin(SecretWithRandomMixin):
|
||||||
|
ssh_key_change_strategy = models.CharField(
|
||||||
|
choices=SSHKeyStrategy.choices, max_length=16,
|
||||||
|
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
|
||||||
|
)
|
||||||
|
get_all_assets: callable # get all assets
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def create_nonlocal_accounts(self, usernames, asset):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_account_ids(self):
|
||||||
|
usernames = self.accounts
|
||||||
|
accounts = Account.objects.none()
|
||||||
|
for asset in self.get_all_assets():
|
||||||
|
self.create_nonlocal_accounts(usernames, asset)
|
||||||
|
accounts = accounts | asset.accounts.all()
|
||||||
|
account_ids = accounts.filter(
|
||||||
|
username__in=usernames, secret_type=self.secret_type
|
||||||
|
).values_list('id', flat=True)
|
||||||
|
return [str(_id) for _id in account_ids]
|
||||||
|
|
||||||
|
def to_attr_json(self):
|
||||||
|
attr_json = super().to_attr_json()
|
||||||
|
attr_json.update({
|
||||||
|
'secret': self.secret,
|
||||||
|
'secret_type': self.secret_type,
|
||||||
|
'accounts': self.get_account_ids(),
|
||||||
|
'password_rules': self.password_rules,
|
||||||
|
'secret_strategy': self.secret_strategy,
|
||||||
|
'ssh_key_change_strategy': self.ssh_key_change_strategy,
|
||||||
|
})
|
||||||
|
return attr_json
|
||||||
|
|
|
@ -2,62 +2,13 @@ from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import (
|
from accounts.const import (
|
||||||
AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
|
AutomationTypes
|
||||||
)
|
)
|
||||||
from accounts.models import Account
|
|
||||||
from common.db import fields
|
from common.db import fields
|
||||||
from common.db.models import JMSBaseModel
|
from common.db.models import JMSBaseModel
|
||||||
from .base import AccountBaseAutomation
|
from .base import AccountBaseAutomation, ChangeSecretMixin
|
||||||
|
|
||||||
__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', 'ChangeSecretMixin']
|
__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', ]
|
||||||
|
|
||||||
|
|
||||||
class ChangeSecretMixin(models.Model):
|
|
||||||
secret_type = models.CharField(
|
|
||||||
choices=SecretType.choices, max_length=16,
|
|
||||||
default=SecretType.PASSWORD, verbose_name=_('Secret type')
|
|
||||||
)
|
|
||||||
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
|
||||||
secret_strategy = models.CharField(
|
|
||||||
choices=SecretStrategy.choices, max_length=16,
|
|
||||||
default=SecretStrategy.custom, verbose_name=_('Secret strategy')
|
|
||||||
)
|
|
||||||
password_rules = models.JSONField(default=dict, verbose_name=_('Password rules'))
|
|
||||||
ssh_key_change_strategy = models.CharField(
|
|
||||||
choices=SSHKeyStrategy.choices, max_length=16,
|
|
||||||
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
|
|
||||||
)
|
|
||||||
|
|
||||||
get_all_assets: callable # get all assets
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
def create_nonlocal_accounts(self, usernames, asset):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_account_ids(self):
|
|
||||||
usernames = self.accounts
|
|
||||||
accounts = Account.objects.none()
|
|
||||||
for asset in self.get_all_assets():
|
|
||||||
self.create_nonlocal_accounts(usernames, asset)
|
|
||||||
accounts = accounts | asset.accounts.all()
|
|
||||||
account_ids = accounts.filter(
|
|
||||||
username__in=usernames, secret_type=self.secret_type
|
|
||||||
).values_list('id', flat=True)
|
|
||||||
return [str(_id) for _id in account_ids]
|
|
||||||
|
|
||||||
def to_attr_json(self):
|
|
||||||
attr_json = super().to_attr_json()
|
|
||||||
attr_json.update({
|
|
||||||
'secret': self.secret,
|
|
||||||
'secret_type': self.secret_type,
|
|
||||||
'accounts': self.get_account_ids(),
|
|
||||||
'password_rules': self.password_rules,
|
|
||||||
'secret_strategy': self.secret_strategy,
|
|
||||||
'ssh_key_change_strategy': self.ssh_key_change_strategy,
|
|
||||||
})
|
|
||||||
return attr_json
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||||
|
|
|
@ -17,9 +17,9 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||||
|
|
||||||
def create_nonlocal_accounts(self, usernames, asset):
|
def create_nonlocal_accounts(self, usernames, asset):
|
||||||
secret_type = self.secret_type
|
secret_type = self.secret_type
|
||||||
account_usernames = asset.accounts.filter(secret_type=self.secret_type).values_list(
|
account_usernames = asset.accounts \
|
||||||
'username', flat=True
|
.filter(secret_type=self.secret_type) \
|
||||||
)
|
.values_list('username', flat=True)
|
||||||
create_usernames = set(usernames) - set(account_usernames)
|
create_usernames = set(usernames) - set(account_usernames)
|
||||||
create_account_objs = [
|
create_account_objs = [
|
||||||
Account(
|
Account(
|
||||||
|
@ -30,9 +30,6 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||||
]
|
]
|
||||||
Account.objects.bulk_create(create_account_objs)
|
Account.objects.bulk_create(create_account_objs)
|
||||||
|
|
||||||
def set_period_schedule(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dynamic_username(self):
|
def dynamic_username(self):
|
||||||
return self.username == '@USER'
|
return self.username == '@USER'
|
||||||
|
|
|
@ -8,12 +8,14 @@ from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import SecretType
|
from accounts.const import SecretType, SecretStrategy
|
||||||
|
from accounts.models.mixins import VaultModelMixin, VaultManagerMixin, VaultQuerySetMixin
|
||||||
|
from accounts.utils import SecretGenerator
|
||||||
|
from common.db import fields
|
||||||
from common.utils import (
|
from common.utils import (
|
||||||
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
||||||
random_string, lazyproperty, parse_ssh_public_key_str, is_openssh_format_key
|
random_string, lazyproperty, parse_ssh_public_key_str, is_openssh_format_key
|
||||||
)
|
)
|
||||||
from accounts.models.mixins import VaultModelMixin, VaultManagerMixin, VaultQuerySetMixin
|
|
||||||
from orgs.mixins.models import JMSOrgBaseModel, OrgManager
|
from orgs.mixins.models import JMSOrgBaseModel, OrgManager
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
@ -29,6 +31,35 @@ class BaseAccountManager(VaultManagerMixin, OrgManager):
|
||||||
return self.get_queryset().active()
|
return self.get_queryset().active()
|
||||||
|
|
||||||
|
|
||||||
|
class SecretWithRandomMixin(models.Model):
|
||||||
|
secret_type = models.CharField(
|
||||||
|
choices=SecretType.choices, max_length=16,
|
||||||
|
default=SecretType.PASSWORD, verbose_name=_('Secret type')
|
||||||
|
)
|
||||||
|
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
||||||
|
secret_strategy = models.CharField(
|
||||||
|
choices=SecretStrategy.choices, max_length=16,
|
||||||
|
default=SecretStrategy.custom, verbose_name=_('Secret strategy')
|
||||||
|
)
|
||||||
|
password_rules = models.JSONField(default=dict, verbose_name=_('Password rules'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def secret_generator(self):
|
||||||
|
return SecretGenerator(
|
||||||
|
self.secret_strategy, self.secret_type,
|
||||||
|
self.password_rules,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_secret(self):
|
||||||
|
if self.secret_strategy == 'random':
|
||||||
|
return self.secret_generator.get_secret()
|
||||||
|
else:
|
||||||
|
return self.secret
|
||||||
|
|
||||||
|
|
||||||
class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
||||||
|
|
|
@ -4,16 +4,22 @@ from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .account import Account
|
from .account import Account
|
||||||
from .base import BaseAccount
|
from .base import BaseAccount, SecretWithRandomMixin
|
||||||
|
|
||||||
__all__ = ['AccountTemplate', ]
|
__all__ = ['AccountTemplate', ]
|
||||||
|
|
||||||
|
|
||||||
class AccountTemplate(BaseAccount):
|
class AccountTemplate(BaseAccount, SecretWithRandomMixin):
|
||||||
su_from = models.ForeignKey(
|
su_from = models.ForeignKey(
|
||||||
'self', related_name='su_to', null=True,
|
'self', related_name='su_to', null=True,
|
||||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||||
)
|
)
|
||||||
|
auto_push = models.BooleanField(default=False, verbose_name=_('Auto push'))
|
||||||
|
platforms = models.ManyToManyField(
|
||||||
|
'assets.Platform', related_name='account_templates',
|
||||||
|
verbose_name=_('Platforms'), blank=True,
|
||||||
|
)
|
||||||
|
push_params = models.JSONField(default=dict, verbose_name=_('Push params'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('Account template')
|
verbose_name = _('Account template')
|
||||||
|
@ -25,15 +31,15 @@ class AccountTemplate(BaseAccount):
|
||||||
('change_accounttemplatesecret', _('Can change asset account template secret')),
|
('change_accounttemplatesecret', _('Can change asset account template secret')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.name}({self.username})'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_su_from_account_templates(cls, pk=None):
|
def get_su_from_account_templates(cls, pk=None):
|
||||||
if pk is None:
|
if pk is None:
|
||||||
return cls.objects.all()
|
return cls.objects.all()
|
||||||
return cls.objects.exclude(Q(id=pk) | Q(su_from_id=pk))
|
return cls.objects.exclude(Q(id=pk) | Q(su_from_id=pk))
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f'{self.name}({self.username})'
|
|
||||||
|
|
||||||
def get_su_from_account(self, asset):
|
def get_su_from_account(self, asset):
|
||||||
su_from = self.su_from
|
su_from = self.su_from
|
||||||
if su_from and asset.platform.su_enabled:
|
if su_from and asset.platform.su_enabled:
|
||||||
|
|
|
@ -2,6 +2,7 @@ import uuid
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
@ -73,6 +74,22 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||||
name = name + '_' + uuid.uuid4().hex[:4]
|
name = name + '_' + uuid.uuid4().hex[:4]
|
||||||
initial_data['name'] = name
|
initial_data['name'] = name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_template_attr_for_account(template):
|
||||||
|
# Set initial data from template
|
||||||
|
field_names = [
|
||||||
|
'username', 'secret', 'secret_type', 'privileged', 'is_active'
|
||||||
|
]
|
||||||
|
|
||||||
|
attrs = {}
|
||||||
|
for name in field_names:
|
||||||
|
value = getattr(template, name, None)
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
attrs[name] = value
|
||||||
|
attrs['secret'] = template.get_secret()
|
||||||
|
return attrs
|
||||||
|
|
||||||
def from_template_if_need(self, initial_data):
|
def from_template_if_need(self, initial_data):
|
||||||
if isinstance(initial_data, str):
|
if isinstance(initial_data, str):
|
||||||
return
|
return
|
||||||
|
@ -89,20 +106,7 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||||
raise serializers.ValidationError({'template': 'Template not found'})
|
raise serializers.ValidationError({'template': 'Template not found'})
|
||||||
|
|
||||||
self._template = template
|
self._template = template
|
||||||
# Set initial data from template
|
attrs = self.get_template_attr_for_account(template)
|
||||||
ignore_fields = ['id', 'date_created', 'date_updated', 'su_from', 'org_id']
|
|
||||||
field_names = [
|
|
||||||
field.name for field in template._meta.fields
|
|
||||||
if field.name not in ignore_fields
|
|
||||||
]
|
|
||||||
field_names = [name if name != '_secret' else 'secret' for name in field_names]
|
|
||||||
|
|
||||||
attrs = {}
|
|
||||||
for name in field_names:
|
|
||||||
value = getattr(template, name, None)
|
|
||||||
if value is None:
|
|
||||||
continue
|
|
||||||
attrs[name] = value
|
|
||||||
initial_data.update(attrs)
|
initial_data.update(attrs)
|
||||||
initial_data.update({
|
initial_data.update({
|
||||||
'source': Source.TEMPLATE,
|
'source': Source.TEMPLATE,
|
||||||
|
@ -114,10 +118,13 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||||
asset = get_object_or_404(Asset, pk=asset_id)
|
asset = get_object_or_404(Asset, pk=asset_id)
|
||||||
initial_data['su_from'] = template.get_su_from_account(asset)
|
initial_data['su_from'] = template.get_su_from_account(asset)
|
||||||
|
|
||||||
@staticmethod
|
def push_account_if_need(self, instance, push_now, params, stat):
|
||||||
def push_account_if_need(instance, push_now, params, stat):
|
|
||||||
if not push_now or stat not in ['created', 'updated']:
|
if not push_now or stat not in ['created', 'updated']:
|
||||||
return
|
return
|
||||||
|
transaction.on_commit(lambda: self.start_push(instance, params))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def start_push(instance, params):
|
||||||
push_accounts_to_assets_task.delay([str(instance.id)], params)
|
push_accounts_to_assets_task.delay([str(instance.id)], params)
|
||||||
|
|
||||||
def get_validators(self):
|
def get_validators(self):
|
||||||
|
|
|
@ -7,10 +7,19 @@ from common.serializers.fields import ObjectRelatedField
|
||||||
from .base import BaseAccountSerializer
|
from .base import BaseAccountSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordRulesSerializer(serializers.Serializer):
|
||||||
|
length = serializers.IntegerField(min_value=8, max_value=30, default=16, label=_('Password length'))
|
||||||
|
lowercase = serializers.BooleanField(default=True, label=_('Lowercase'))
|
||||||
|
uppercase = serializers.BooleanField(default=True, label=_('Uppercase'))
|
||||||
|
digit = serializers.BooleanField(default=True, label=_('Digit'))
|
||||||
|
symbol = serializers.BooleanField(default=True, label=_('Special symbol'))
|
||||||
|
|
||||||
|
|
||||||
class AccountTemplateSerializer(BaseAccountSerializer):
|
class AccountTemplateSerializer(BaseAccountSerializer):
|
||||||
is_sync_account = serializers.BooleanField(default=False, write_only=True)
|
is_sync_account = serializers.BooleanField(default=False, write_only=True)
|
||||||
_is_sync_account = False
|
_is_sync_account = False
|
||||||
|
|
||||||
|
password_rules = PasswordRulesSerializer(required=False, label=_('Password rules'))
|
||||||
su_from = ObjectRelatedField(
|
su_from = ObjectRelatedField(
|
||||||
required=False, queryset=AccountTemplate.objects, allow_null=True,
|
required=False, queryset=AccountTemplate.objects, allow_null=True,
|
||||||
allow_empty=True, label=_('Su from'), attrs=('id', 'name', 'username')
|
allow_empty=True, label=_('Su from'), attrs=('id', 'name', 'username')
|
||||||
|
@ -18,7 +27,22 @@ class AccountTemplateSerializer(BaseAccountSerializer):
|
||||||
|
|
||||||
class Meta(BaseAccountSerializer.Meta):
|
class Meta(BaseAccountSerializer.Meta):
|
||||||
model = AccountTemplate
|
model = AccountTemplate
|
||||||
fields = BaseAccountSerializer.Meta.fields + ['is_sync_account', 'su_from']
|
fields = BaseAccountSerializer.Meta.fields + [
|
||||||
|
'secret_strategy', 'password_rules',
|
||||||
|
'auto_push', 'push_params', 'platforms',
|
||||||
|
'is_sync_account', 'su_from'
|
||||||
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
'secret_strategy': {'help_text': _('Secret generation strategy for account creation')},
|
||||||
|
'auto_push': {'help_text': _('Whether to automatically push the account to the asset')},
|
||||||
|
'platforms': {
|
||||||
|
'help_text': _(
|
||||||
|
'Associated platform, you can configure push parameters. '
|
||||||
|
'If not associated, default parameters will be used'
|
||||||
|
),
|
||||||
|
'required': False
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
def sync_accounts_secret(self, instance, diff):
|
def sync_accounts_secret(self, instance, diff):
|
||||||
if not self._is_sync_account or 'secret' not in diff:
|
if not self._is_sync_account or 'secret' not in diff:
|
||||||
|
|
|
@ -19,8 +19,12 @@ class VirtualAccountSerializer(serializers.ModelSerializer):
|
||||||
'comment': {'label': _('Comment')},
|
'comment': {'label': _('Comment')},
|
||||||
'name': {'label': _('Name')},
|
'name': {'label': _('Name')},
|
||||||
'username': {'label': _('Username')},
|
'username': {'label': _('Username')},
|
||||||
'secret_from_login': {'help_text': _('Current only support login from AD/LDAP. Secret priority: '
|
'secret_from_login': {
|
||||||
'Same account in asset secret > Login secret > Manual input')
|
'help_text': _(
|
||||||
},
|
'Current only support login from AD/LDAP. Secret priority: '
|
||||||
|
'Same account in asset secret > Login secret > Manual input. <br/ >'
|
||||||
|
'For security, please set config CACHE_LOGIN_PASSWORD_ENABLED to true'
|
||||||
|
)
|
||||||
|
},
|
||||||
'alias': {'required': False},
|
'alias': {'required': False},
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,13 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.const import (
|
from accounts.const import (
|
||||||
AutomationTypes, DEFAULT_PASSWORD_RULES,
|
AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
|
||||||
SecretType, SecretStrategy, SSHKeyStrategy
|
|
||||||
)
|
)
|
||||||
from accounts.models import (
|
from accounts.models import (
|
||||||
Account, ChangeSecretAutomation,
|
Account, ChangeSecretAutomation,
|
||||||
ChangeSecretRecord, AutomationExecution
|
ChangeSecretRecord, AutomationExecution
|
||||||
)
|
)
|
||||||
from accounts.serializers import AuthValidateMixin
|
from accounts.serializers import AuthValidateMixin, PasswordRulesSerializer
|
||||||
from assets.models import Asset
|
from assets.models import Asset
|
||||||
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
|
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
@ -42,7 +41,7 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
||||||
ssh_key_change_strategy = LabeledChoiceField(
|
ssh_key_change_strategy = LabeledChoiceField(
|
||||||
choices=SSHKeyStrategy.choices, required=False, label=_('SSH Key strategy')
|
choices=SSHKeyStrategy.choices, required=False, label=_('SSH Key strategy')
|
||||||
)
|
)
|
||||||
password_rules = serializers.DictField(default=DEFAULT_PASSWORD_RULES)
|
password_rules = PasswordRulesSerializer(required=False, label=_('Password rules'))
|
||||||
secret_type = LabeledChoiceField(choices=get_secret_types(), required=True, label=_('Secret type'))
|
secret_type = LabeledChoiceField(choices=get_secret_types(), required=True, label=_('Secret type'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
from django.db.models.signals import pre_save, post_save, post_delete
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from django.db.models.signals import post_delete
|
||||||
|
from django.db.models.signals import pre_save, post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from django.utils.translation import gettext_noop
|
||||||
|
|
||||||
from accounts.backends import vault_client
|
from accounts.backends import vault_client
|
||||||
from common.utils import get_logger
|
from audits.const import ActivityChoices
|
||||||
|
from audits.signal_handlers import create_activities
|
||||||
|
from common.decorators import merge_delay_run
|
||||||
|
from common.utils import get_logger, i18n_fmt
|
||||||
from .models import Account, AccountTemplate
|
from .models import Account, AccountTemplate
|
||||||
|
from .tasks.push_account import push_accounts_to_assets_task
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -16,6 +24,53 @@ def on_account_pre_save(sender, instance, **kwargs):
|
||||||
instance.version = instance.history.count()
|
instance.version = instance.history.count()
|
||||||
|
|
||||||
|
|
||||||
|
@merge_delay_run(ttl=5)
|
||||||
|
def push_accounts_if_need(accounts=()):
|
||||||
|
from .models import AccountTemplate
|
||||||
|
|
||||||
|
template_accounts = defaultdict(list)
|
||||||
|
for ac in accounts:
|
||||||
|
# 再强调一次吧
|
||||||
|
if ac.source != 'template':
|
||||||
|
continue
|
||||||
|
template_accounts[ac.source_id].append(ac)
|
||||||
|
|
||||||
|
for source_id, accounts in template_accounts.items():
|
||||||
|
template = AccountTemplate.objects.filter(id=source_id).first()
|
||||||
|
if not template or not template.auto_push:
|
||||||
|
continue
|
||||||
|
logger.debug("Push accounts to source: %s", source_id)
|
||||||
|
account_ids = [str(ac.id) for ac in accounts]
|
||||||
|
task = push_accounts_to_assets_task.delay(account_ids, params=template.push_params)
|
||||||
|
detail = i18n_fmt(
|
||||||
|
gettext_noop('Push related accounts to assets: %s, by system'),
|
||||||
|
len(account_ids)
|
||||||
|
)
|
||||||
|
create_activities([str(template.id)], detail, task.id, ActivityChoices.task, template.org_id)
|
||||||
|
logger.debug("Push accounts to source: %s, task: %s", source_id, task)
|
||||||
|
|
||||||
|
|
||||||
|
def create_accounts_activities(account, action='create'):
|
||||||
|
if action == 'create':
|
||||||
|
detail = i18n_fmt(gettext_noop('Add account: %s'), str(account))
|
||||||
|
else:
|
||||||
|
detail = i18n_fmt(gettext_noop('Delete account: %s'), str(account))
|
||||||
|
create_activities([account.asset_id], detail, None, ActivityChoices.operate_log, account.org_id)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Account)
|
||||||
|
def on_account_create_by_template(sender, instance, created=False, **kwargs):
|
||||||
|
if not created or instance.source != 'template':
|
||||||
|
return
|
||||||
|
push_accounts_if_need(accounts=(instance,))
|
||||||
|
create_accounts_activities(instance, action='create')
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_delete, sender=Account)
|
||||||
|
def on_account_delete(sender, instance, **kwargs):
|
||||||
|
create_accounts_activities(instance, action='delete')
|
||||||
|
|
||||||
|
|
||||||
class VaultSignalHandler(object):
|
class VaultSignalHandler(object):
|
||||||
""" 处理 Vault 相关的信号 """
|
""" 处理 Vault 相关的信号 """
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import copy
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
@ -18,9 +20,19 @@ class SecretGenerator:
|
||||||
return private_key
|
return private_key
|
||||||
|
|
||||||
def generate_password(self):
|
def generate_password(self):
|
||||||
length = int(self.password_rules.get('length', 0))
|
password_rules = self.password_rules
|
||||||
length = length if length else DEFAULT_PASSWORD_RULES['length']
|
if not password_rules or not isinstance(password_rules, dict):
|
||||||
return random_string(length, special_char=True)
|
password_rules = {}
|
||||||
|
rules = copy.deepcopy(DEFAULT_PASSWORD_RULES)
|
||||||
|
rules.update(password_rules)
|
||||||
|
rules = {
|
||||||
|
'length': rules['length'],
|
||||||
|
'lower': rules['lowercase'],
|
||||||
|
'upper': rules['uppercase'],
|
||||||
|
'digit': rules['digit'],
|
||||||
|
'special_char': rules['symbol']
|
||||||
|
}
|
||||||
|
return random_string(**rules)
|
||||||
|
|
||||||
def get_secret(self):
|
def get_secret(self):
|
||||||
if self.secret_type == SecretType.SSH_KEY:
|
if self.secret_type == SecretType.SSH_KEY:
|
||||||
|
@ -39,6 +51,8 @@ def validate_password_for_ansible(password):
|
||||||
# Ansible 推送的时候不支持
|
# Ansible 推送的时候不支持
|
||||||
if '{{' in password:
|
if '{{' in password:
|
||||||
raise serializers.ValidationError(_('Password can not contains `{{` '))
|
raise serializers.ValidationError(_('Password can not contains `{{` '))
|
||||||
|
if '{%' in password:
|
||||||
|
raise serializers.ValidationError(_('Password can not contains `{%` '))
|
||||||
# Ansible Windows 推送的时候不支持
|
# Ansible Windows 推送的时候不支持
|
||||||
if "'" in password:
|
if "'" in password:
|
||||||
raise serializers.ValidationError(_("Password can not contains `'` "))
|
raise serializers.ValidationError(_("Password can not contains `'` "))
|
||||||
|
|
|
@ -103,25 +103,27 @@ class UserAssetAccountBaseACL(OrgModelMixin, UserBaseACL):
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter_queryset(cls, user=None, asset=None, account=None, account_username=None, **kwargs):
|
def _get_filter_queryset(cls, user=None, asset=None, account=None, account_username=None, **kwargs):
|
||||||
queryset = cls.objects.all()
|
queryset = cls.objects.all()
|
||||||
|
q = models.Q()
|
||||||
if user:
|
|
||||||
q = cls.users.get_filter_q(user)
|
|
||||||
queryset = queryset.filter(q)
|
|
||||||
|
|
||||||
if asset:
|
if asset:
|
||||||
org_id = asset.org_id
|
q &= cls.assets.get_filter_q(asset)
|
||||||
with tmp_to_org(org_id):
|
if user:
|
||||||
q = cls.assets.get_filter_q(asset)
|
q &= cls.users.get_filter_q(user)
|
||||||
queryset = queryset.filter(q)
|
|
||||||
if account and not account_username:
|
if account and not account_username:
|
||||||
account_username = account.username
|
account_username = account.username
|
||||||
if account_username:
|
if account_username:
|
||||||
q = models.Q(accounts__contains=account_username) | \
|
q &= models.Q(accounts__contains=account_username) | \
|
||||||
models.Q(accounts__contains='*') | \
|
models.Q(accounts__contains='*') | \
|
||||||
models.Q(accounts__contains='@ALL')
|
models.Q(accounts__contains='@ALL')
|
||||||
queryset = queryset.filter(q)
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
queryset = queryset.filter(**kwargs)
|
q &= models.Q(**kwargs)
|
||||||
|
queryset = queryset.filter(q)
|
||||||
return queryset.valid().distinct()
|
return queryset.valid().distinct()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def filter_queryset(cls, asset=None, **kwargs):
|
||||||
|
org_id = asset.org_id if asset else ''
|
||||||
|
with tmp_to_org(org_id):
|
||||||
|
return cls._get_filter_queryset(asset=asset, **kwargs)
|
||||||
|
|
|
@ -42,7 +42,7 @@ class SerializeToTreeNodeMixin:
|
||||||
'name': _name(node),
|
'name': _name(node),
|
||||||
'title': _name(node),
|
'title': _name(node),
|
||||||
'pId': node.parent_key,
|
'pId': node.parent_key,
|
||||||
'isParent': node.assets_amount > 0,
|
'isParent': True,
|
||||||
'open': _open(node),
|
'open': _open(node),
|
||||||
'meta': {
|
'meta': {
|
||||||
'data': {
|
'data': {
|
||||||
|
|
|
@ -49,13 +49,19 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
||||||
@action(methods=['post'], detail=False, url_path='filter-nodes-assets')
|
@action(methods=['post'], detail=False, url_path='filter-nodes-assets')
|
||||||
def filter_nodes_assets(self, request, *args, **kwargs):
|
def filter_nodes_assets(self, request, *args, **kwargs):
|
||||||
node_ids = request.data.get('node_ids', [])
|
node_ids = request.data.get('node_ids', [])
|
||||||
asset_ids = request.data.get('asset_ids', [])
|
asset_ids = set(request.data.get('asset_ids', []))
|
||||||
nodes = Node.objects.filter(id__in=node_ids)
|
platform_ids = set(request.data.get('platform_ids', []))
|
||||||
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
|
||||||
direct_asset_ids = Asset.objects.filter(id__in=asset_ids).values_list('id', flat=True)
|
if node_ids:
|
||||||
platform_ids = Asset.objects.filter(
|
nodes = Node.objects.filter(id__in=node_ids)
|
||||||
id__in=set(list(direct_asset_ids) + list(node_asset_ids))
|
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||||
).values_list('platform_id', flat=True)
|
asset_ids |= set(node_asset_ids)
|
||||||
|
|
||||||
|
if asset_ids:
|
||||||
|
_platform_ids = Asset.objects \
|
||||||
|
.filter(id__in=set(asset_ids)) \
|
||||||
|
.values_list('platform_id', flat=True)
|
||||||
|
platform_ids |= set(_platform_ids)
|
||||||
platforms = Platform.objects.filter(id__in=platform_ids)
|
platforms = Platform.objects.filter(id__in=platform_ids)
|
||||||
serializer = self.get_serializer(platforms, many=True)
|
serializer = self.get_serializer(platforms, many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
# ~*~ coding: utf-8 ~*~
|
# ~*~ coding: utf-8 ~*~
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.generics import get_object_or_404
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from assets.locks import NodeAddChildrenLock
|
from assets.locks import NodeAddChildrenLock
|
||||||
|
from common.exceptions import JMSException
|
||||||
from common.tree import TreeNodeSerializer
|
from common.tree import TreeNodeSerializer
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from common.exceptions import JMSException
|
|
||||||
from orgs.mixins import generics
|
from orgs.mixins import generics
|
||||||
from orgs.utils import current_org
|
from orgs.utils import current_org
|
||||||
from .mixin import SerializeToTreeNodeMixin
|
from .mixin import SerializeToTreeNodeMixin
|
||||||
|
@ -35,8 +35,8 @@ class NodeChildrenApi(generics.ListCreateAPIView):
|
||||||
is_initial = False
|
is_initial = False
|
||||||
|
|
||||||
def initial(self, request, *args, **kwargs):
|
def initial(self, request, *args, **kwargs):
|
||||||
|
super().initial(request, *args, **kwargs)
|
||||||
self.instance = self.get_object()
|
self.instance = self.get_object()
|
||||||
return super().initial(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
with NodeAddChildrenLock(self.instance):
|
with NodeAddChildrenLock(self.instance):
|
||||||
|
|
|
@ -175,7 +175,7 @@ class BasePlaybookManager:
|
||||||
method = self.method_id_meta_mapper.get(method_id)
|
method = self.method_id_meta_mapper.get(method_id)
|
||||||
if not method:
|
if not method:
|
||||||
logger.error("Method not found: {}".format(method_id))
|
logger.error("Method not found: {}".format(method_id))
|
||||||
return method
|
return
|
||||||
method_playbook_dir_path = method['dir']
|
method_playbook_dir_path = method['dir']
|
||||||
sub_playbook_path = os.path.join(sub_playbook_dir, 'project', 'main.yml')
|
sub_playbook_path = os.path.join(sub_playbook_dir, 'project', 'main.yml')
|
||||||
shutil.copytree(method_playbook_dir_path, os.path.dirname(sub_playbook_path))
|
shutil.copytree(method_playbook_dir_path, os.path.dirname(sub_playbook_path))
|
||||||
|
@ -196,6 +196,11 @@ class BasePlaybookManager:
|
||||||
print(msg)
|
print(msg)
|
||||||
runners = []
|
runners = []
|
||||||
for platform, assets in assets_group_by_platform.items():
|
for platform, assets in assets_group_by_platform.items():
|
||||||
|
if not assets:
|
||||||
|
continue
|
||||||
|
if not platform.automation or not platform.automation.ansible_enabled:
|
||||||
|
print(_(" - Platform {} ansible disabled").format(platform.name))
|
||||||
|
continue
|
||||||
assets_bulked = [assets[i:i + self.bulk_size] for i in range(0, len(assets), self.bulk_size)]
|
assets_bulked = [assets[i:i + self.bulk_size] for i in range(0, len(assets), self.bulk_size)]
|
||||||
|
|
||||||
for i, _assets in enumerate(assets_bulked, start=1):
|
for i, _assets in enumerate(assets_bulked, start=1):
|
||||||
|
@ -204,6 +209,8 @@ class BasePlaybookManager:
|
||||||
inventory_path = os.path.join(self.runtime_dir, sub_dir, 'hosts.json')
|
inventory_path = os.path.join(self.runtime_dir, sub_dir, 'hosts.json')
|
||||||
self.generate_inventory(_assets, inventory_path)
|
self.generate_inventory(_assets, inventory_path)
|
||||||
playbook_path = self.generate_playbook(_assets, platform, playbook_dir)
|
playbook_path = self.generate_playbook(_assets, platform, playbook_dir)
|
||||||
|
if not playbook_path:
|
||||||
|
continue
|
||||||
|
|
||||||
runer = PlaybookRunner(
|
runer = PlaybookRunner(
|
||||||
inventory_path,
|
inventory_path,
|
||||||
|
@ -309,6 +316,7 @@ class BasePlaybookManager:
|
||||||
shutil.rmtree(self.runtime_dir)
|
shutil.rmtree(self.runtime_dir)
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
|
print(">>> 任务准备阶段\n")
|
||||||
runners = self.get_runners()
|
runners = self.get_runners()
|
||||||
if len(runners) > 1:
|
if len(runners) > 1:
|
||||||
print("### 分次执行任务, 总共 {}\n".format(len(runners)))
|
print("### 分次执行任务, 总共 {}\n".format(len(runners)))
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||||
connection_options:
|
connection_options:
|
||||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||||
register: db_info
|
register: db_info
|
||||||
|
|
|
@ -10,6 +10,10 @@
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||||
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||||
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||||
filter: version
|
filter: version
|
||||||
register: db_info
|
register: db_info
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||||
connection_options:
|
connection_options:
|
||||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||||
|
|
|
@ -10,4 +10,8 @@
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
|
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||||
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||||
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||||
filter: version
|
filter: version
|
||||||
|
|
|
@ -24,7 +24,7 @@ class DeviceTypes(BaseType):
|
||||||
def _get_protocol_constrains(cls) -> dict:
|
def _get_protocol_constrains(cls) -> dict:
|
||||||
return {
|
return {
|
||||||
'*': {
|
'*': {
|
||||||
'choices': ['ssh', 'telnet']
|
'choices': ['ssh', 'telnet', 'sftp']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,13 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||||
'sftp_home': {
|
'sftp_home': {
|
||||||
'type': 'str',
|
'type': 'str',
|
||||||
'default': '/tmp',
|
'default': '/tmp',
|
||||||
'label': _('SFTP home')
|
'label': _('SFTP root'),
|
||||||
|
'help_text': _(
|
||||||
|
'SFTP root directory, Support variable: <br>'
|
||||||
|
'- ${ACCOUNT} The connected account username <br>'
|
||||||
|
'- ${HOME} The home directory of the connected account <br>'
|
||||||
|
'- ${USER} The username of the user'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -154,6 +160,15 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
||||||
'required': True,
|
'required': True,
|
||||||
'secret_types': ['password'],
|
'secret_types': ['password'],
|
||||||
'xpack': True,
|
'xpack': True,
|
||||||
|
'setting': {
|
||||||
|
'version': {
|
||||||
|
'type': 'choice',
|
||||||
|
'choices': [('>=2014', '>= 2014'), ('<2014', '< 2014')],
|
||||||
|
'default': '>=2014',
|
||||||
|
'label': _('Version'),
|
||||||
|
'help_text': _('SQL Server version, Different versions have different connection drivers')
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
cls.clickhouse: {
|
cls.clickhouse: {
|
||||||
'port': 9000,
|
'port': 9000,
|
||||||
|
|
|
@ -49,11 +49,11 @@ def migrate_assets_sftp_protocol(apps, schema_editor):
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
print("\nAsset add sftp protocol: ")
|
print("\nAsset add sftp protocol: ")
|
||||||
asset_ids = asset_cls.objects\
|
asset_ids = list(asset_cls.objects\
|
||||||
.filter(platform__in=sftp_platforms)\
|
.filter(platform__in=sftp_platforms)\
|
||||||
.exclude(protocols__name='sftp')\
|
.exclude(protocols__name='sftp')\
|
||||||
.distinct()\
|
.distinct()\
|
||||||
.values_list('id', flat=True)
|
.values_list('id', flat=True))
|
||||||
while True:
|
while True:
|
||||||
_asset_ids = asset_ids[count:count + 1000]
|
_asset_ids = asset_ids[count:count + 1000]
|
||||||
if not _asset_ids:
|
if not _asset_ids:
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 4.1.10 on 2023-09-13 10:59
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_device_automation_ansible_enabled(apps, *args):
|
||||||
|
platform_model = apps.get_model('assets', 'Platform')
|
||||||
|
automation_model = apps.get_model('assets', 'PlatformAutomation')
|
||||||
|
ids = platform_model.objects.filter(category='device').values_list('id', flat=True)
|
||||||
|
automation_model.objects.filter(platform_id__in=ids).update(ansible_enabled=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0122_auto_20230803_1553'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migrate_device_automation_ansible_enabled)
|
||||||
|
]
|
|
@ -1,6 +1,7 @@
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.validators import UniqueValidator
|
||||||
|
|
||||||
from common.serializers import (
|
from common.serializers import (
|
||||||
WritableNestedModelSerializer, type_field_map, MethodSerializer,
|
WritableNestedModelSerializer, type_field_map, MethodSerializer,
|
||||||
|
@ -123,6 +124,10 @@ class PlatformSerializer(WritableNestedModelSerializer):
|
||||||
("super", "super 15"),
|
("super", "super 15"),
|
||||||
("super_level", "super level 15")
|
("super_level", "super level 15")
|
||||||
]
|
]
|
||||||
|
id = serializers.IntegerField(
|
||||||
|
label='ID', required=False,
|
||||||
|
validators=[UniqueValidator(queryset=Platform.objects.all())]
|
||||||
|
)
|
||||||
charset = LabeledChoiceField(choices=Platform.CharsetChoices.choices, label=_("Charset"), default='utf-8')
|
charset = LabeledChoiceField(choices=Platform.CharsetChoices.choices, label=_("Charset"), default='utf-8')
|
||||||
type = LabeledChoiceField(choices=AllTypes.choices(), label=_("Type"))
|
type = LabeledChoiceField(choices=AllTypes.choices(), label=_("Type"))
|
||||||
category = LabeledChoiceField(choices=Category.choices, label=_("Category"))
|
category = LabeledChoiceField(choices=Category.choices, label=_("Category"))
|
||||||
|
@ -213,7 +218,7 @@ class PlatformSerializer(WritableNestedModelSerializer):
|
||||||
def validate_automation(self, automation):
|
def validate_automation(self, automation):
|
||||||
automation = automation or {}
|
automation = automation or {}
|
||||||
ansible_enabled = automation.get('ansible_enabled', False) \
|
ansible_enabled = automation.get('ansible_enabled', False) \
|
||||||
and self.constraints['automation'].get('ansible_enabled', False)
|
and self.constraints['automation'].get('ansible_enabled', False)
|
||||||
automation['ansible_enable'] = ansible_enabled
|
automation['ansible_enable'] = ansible_enabled
|
||||||
return automation
|
return automation
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ class KubernetesClient:
|
||||||
|
|
||||||
remote_bind_address = (
|
remote_bind_address = (
|
||||||
urlparse(asset.address).hostname,
|
urlparse(asset.address).hostname,
|
||||||
urlparse(asset.address).port
|
urlparse(asset.address).port or 443
|
||||||
)
|
)
|
||||||
server = SSHTunnelForwarder(
|
server = SSHTunnelForwarder(
|
||||||
(gateway.address, gateway.port),
|
(gateway.address, gateway.port),
|
||||||
|
|
|
@ -1,50 +1,52 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import os
|
|
||||||
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.db.models import F, Value, CharField, Q
|
from django.db.models import F, Value, CharField, Q
|
||||||
from django.http import HttpResponse, FileResponse
|
from django.http import HttpResponse, FileResponse
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.encoding import escape_uri_path
|
from django.utils.encoding import escape_uri_path
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.decorators import action
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
|
|
||||||
from common.api import AsyncApiMixin
|
from common.api import CommonApiMixin
|
||||||
from common.drf.filters import DatetimeRangeFilter
|
from common.const.http import GET, POST
|
||||||
|
from common.drf.filters import DatetimeRangeFilterBackend
|
||||||
from common.permissions import IsServiceAccount
|
from common.permissions import IsServiceAccount
|
||||||
from common.plugins.es import QuerySet as ESQuerySet
|
from common.plugins.es import QuerySet as ESQuerySet
|
||||||
from common.utils import is_uuid, get_logger, lazyproperty
|
|
||||||
from common.const.http import GET, POST
|
|
||||||
from common.storage.ftp_file import FTPFileStorageHandler
|
from common.storage.ftp_file import FTPFileStorageHandler
|
||||||
|
from common.utils import is_uuid, get_logger, lazyproperty
|
||||||
from orgs.mixins.api import OrgReadonlyModelViewSet, OrgModelViewSet
|
from orgs.mixins.api import OrgReadonlyModelViewSet, OrgModelViewSet
|
||||||
from orgs.utils import current_org, tmp_to_root_org
|
|
||||||
from orgs.models import Organization
|
from orgs.models import Organization
|
||||||
|
from orgs.utils import current_org, tmp_to_root_org
|
||||||
from rbac.permissions import RBACPermission
|
from rbac.permissions import RBACPermission
|
||||||
from terminal.models import default_storage
|
from terminal.models import default_storage
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from .backends import TYPE_ENGINE_MAPPING
|
from .backends import TYPE_ENGINE_MAPPING
|
||||||
from .const import ActivityChoices
|
from .const import ActivityChoices
|
||||||
from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog, ActivityLog, JobLog
|
from .models import (
|
||||||
|
FTPLog, UserLoginLog, OperateLog, PasswordChangeLog,
|
||||||
|
ActivityLog, JobLog, UserSession
|
||||||
|
)
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
FTPLogSerializer, UserLoginLogSerializer, JobLogSerializer,
|
FTPLogSerializer, UserLoginLogSerializer, JobLogSerializer,
|
||||||
OperateLogSerializer, OperateLogActionDetailSerializer,
|
OperateLogSerializer, OperateLogActionDetailSerializer,
|
||||||
PasswordChangeLogSerializer, ActivityUnionLogSerializer,
|
PasswordChangeLogSerializer, ActivityUnionLogSerializer,
|
||||||
FileSerializer
|
FileSerializer, UserSessionSerializer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class JobAuditViewSet(OrgReadonlyModelViewSet):
|
class JobAuditViewSet(OrgReadonlyModelViewSet):
|
||||||
model = JobLog
|
model = JobLog
|
||||||
extra_filter_backends = [DatetimeRangeFilter]
|
extra_filter_backends = [DatetimeRangeFilterBackend]
|
||||||
date_range_filter_fields = [
|
date_range_filter_fields = [
|
||||||
('date_start', ('date_from', 'date_to'))
|
('date_start', ('date_from', 'date_to'))
|
||||||
]
|
]
|
||||||
|
@ -57,7 +59,7 @@ class JobAuditViewSet(OrgReadonlyModelViewSet):
|
||||||
class FTPLogViewSet(OrgModelViewSet):
|
class FTPLogViewSet(OrgModelViewSet):
|
||||||
model = FTPLog
|
model = FTPLog
|
||||||
serializer_class = FTPLogSerializer
|
serializer_class = FTPLogSerializer
|
||||||
extra_filter_backends = [DatetimeRangeFilter]
|
extra_filter_backends = [DatetimeRangeFilterBackend]
|
||||||
date_range_filter_fields = [
|
date_range_filter_fields = [
|
||||||
('date_start', ('date_from', 'date_to'))
|
('date_start', ('date_from', 'date_to'))
|
||||||
]
|
]
|
||||||
|
@ -113,7 +115,7 @@ class FTPLogViewSet(OrgModelViewSet):
|
||||||
class UserLoginCommonMixin:
|
class UserLoginCommonMixin:
|
||||||
model = UserLoginLog
|
model = UserLoginLog
|
||||||
serializer_class = UserLoginLogSerializer
|
serializer_class = UserLoginLogSerializer
|
||||||
extra_filter_backends = [DatetimeRangeFilter]
|
extra_filter_backends = [DatetimeRangeFilterBackend]
|
||||||
date_range_filter_fields = [
|
date_range_filter_fields = [
|
||||||
('datetime', ('date_from', 'date_to'))
|
('datetime', ('date_from', 'date_to'))
|
||||||
]
|
]
|
||||||
|
@ -193,7 +195,7 @@ class ResourceActivityAPIView(generics.ListAPIView):
|
||||||
class OperateLogViewSet(OrgReadonlyModelViewSet):
|
class OperateLogViewSet(OrgReadonlyModelViewSet):
|
||||||
model = OperateLog
|
model = OperateLog
|
||||||
serializer_class = OperateLogSerializer
|
serializer_class = OperateLogSerializer
|
||||||
extra_filter_backends = [DatetimeRangeFilter]
|
extra_filter_backends = [DatetimeRangeFilterBackend]
|
||||||
date_range_filter_fields = [
|
date_range_filter_fields = [
|
||||||
('datetime', ('date_from', 'date_to'))
|
('datetime', ('date_from', 'date_to'))
|
||||||
]
|
]
|
||||||
|
@ -232,7 +234,7 @@ class OperateLogViewSet(OrgReadonlyModelViewSet):
|
||||||
class PasswordChangeLogViewSet(OrgReadonlyModelViewSet):
|
class PasswordChangeLogViewSet(OrgReadonlyModelViewSet):
|
||||||
model = PasswordChangeLog
|
model = PasswordChangeLog
|
||||||
serializer_class = PasswordChangeLogSerializer
|
serializer_class = PasswordChangeLogSerializer
|
||||||
extra_filter_backends = [DatetimeRangeFilter]
|
extra_filter_backends = [DatetimeRangeFilterBackend]
|
||||||
date_range_filter_fields = [
|
date_range_filter_fields = [
|
||||||
('datetime', ('date_from', 'date_to'))
|
('datetime', ('date_from', 'date_to'))
|
||||||
]
|
]
|
||||||
|
@ -248,3 +250,44 @@ class PasswordChangeLogViewSet(OrgReadonlyModelViewSet):
|
||||||
user__in=[str(user) for user in users]
|
user__in=[str(user) for user in users]
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
||||||
|
http_method_names = ('get', 'post', 'head', 'options', 'trace')
|
||||||
|
serializer_class = UserSessionSerializer
|
||||||
|
filterset_fields = ['id', 'ip', 'city', 'type']
|
||||||
|
search_fields = ['id', 'ip', 'city']
|
||||||
|
|
||||||
|
rbac_perms = {
|
||||||
|
'offline': ['users.offline_usersession']
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def org_user_ids(self):
|
||||||
|
user_ids = current_org.get_members().values_list('id', flat=True)
|
||||||
|
return user_ids
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
keys = UserSession.get_keys()
|
||||||
|
queryset = UserSession.objects.filter(
|
||||||
|
date_expired__gt=timezone.now(), key__in=keys
|
||||||
|
)
|
||||||
|
if current_org.is_root():
|
||||||
|
return queryset
|
||||||
|
user_ids = self.org_user_ids
|
||||||
|
queryset = queryset.filter(user_id__in=user_ids)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
@action(['POST'], detail=False, url_path='offline')
|
||||||
|
def offline(self, request, *args, **kwargs):
|
||||||
|
ids = request.data.get('ids', [])
|
||||||
|
queryset = self.get_queryset().exclude(key=request.session.session_key).filter(id__in=ids)
|
||||||
|
if not queryset.exists():
|
||||||
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
keys = queryset.values_list('key', flat=True)
|
||||||
|
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
for key in keys:
|
||||||
|
session_store_cls(key).delete()
|
||||||
|
queryset.delete()
|
||||||
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
|
@ -25,6 +25,7 @@ class ActionChoices(TextChoices):
|
||||||
delete = "delete", _("Delete")
|
delete = "delete", _("Delete")
|
||||||
create = "create", _("Create")
|
create = "create", _("Create")
|
||||||
# Activities action
|
# Activities action
|
||||||
|
download = "download", _("Download")
|
||||||
connect = "connect", _("Connect")
|
connect = "connect", _("Connect")
|
||||||
login = "login", _("Login")
|
login = "login", _("Login")
|
||||||
change_auth = "change_password", _("Change password")
|
change_auth = "change_password", _("Change password")
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 4.1.10 on 2023-09-06 05:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('audits', '0022_auto_20230605_1555'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ftplog',
|
||||||
|
name='date_start',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Date start'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='operatelog',
|
||||||
|
name='action',
|
||||||
|
field=models.CharField(choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create'), ('download', 'Download'), ('connect', 'Connect'), ('login', 'Login'), ('change_password', 'Change password')], max_length=16, verbose_name='Action'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userloginlog',
|
||||||
|
name='datetime',
|
||||||
|
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Date login'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Generated by Django 4.1.10 on 2023-09-15 08:58
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('audits', '0023_auto_20230906_1322'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserSession',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
|
('ip', models.GenericIPAddressField(verbose_name='Login IP')),
|
||||||
|
('key', models.CharField(max_length=128, verbose_name='Session key')),
|
||||||
|
('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')),
|
||||||
|
('type', models.CharField(choices=[('W', 'Web'), ('T', 'Terminal'), ('U', 'Unknown')], max_length=2, verbose_name='Login type')),
|
||||||
|
('backend', models.CharField(default='', max_length=32, verbose_name='Authentication backend')),
|
||||||
|
('date_created', models.DateTimeField(blank=True, null=True, verbose_name='Date created')),
|
||||||
|
('date_expired', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Date expired')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sessions', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'User session',
|
||||||
|
'ordering': ['-date_created'],
|
||||||
|
'permissions': [('offline_usersession', 'Offline ussr session')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,7 +1,9 @@
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.cache import caches
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -28,7 +30,8 @@ __all__ = [
|
||||||
"ActivityLog",
|
"ActivityLog",
|
||||||
"PasswordChangeLog",
|
"PasswordChangeLog",
|
||||||
"UserLoginLog",
|
"UserLoginLog",
|
||||||
"JobLog"
|
"JobLog",
|
||||||
|
"UserSession"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,7 +60,7 @@ class FTPLog(OrgModelMixin):
|
||||||
)
|
)
|
||||||
filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
|
filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
|
||||||
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
|
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
|
||||||
date_start = models.DateTimeField(auto_now_add=True, verbose_name=_("Date start"))
|
date_start = models.DateTimeField(auto_now_add=True, verbose_name=_("Date start"), db_index=True)
|
||||||
has_file = models.BooleanField(default=False, verbose_name=_("File"))
|
has_file = models.BooleanField(default=False, verbose_name=_("File"))
|
||||||
session = models.CharField(max_length=36, verbose_name=_("Session"), default=uuid.uuid4)
|
session = models.CharField(max_length=36, verbose_name=_("Session"), default=uuid.uuid4)
|
||||||
|
|
||||||
|
@ -198,7 +201,7 @@ class UserLoginLog(models.Model):
|
||||||
choices=LoginStatusChoices.choices,
|
choices=LoginStatusChoices.choices,
|
||||||
verbose_name=_("Status"),
|
verbose_name=_("Status"),
|
||||||
)
|
)
|
||||||
datetime = models.DateTimeField(default=timezone.now, verbose_name=_("Date login"))
|
datetime = models.DateTimeField(default=timezone.now, verbose_name=_("Date login"), db_index=True)
|
||||||
backend = models.CharField(
|
backend = models.CharField(
|
||||||
max_length=32, default="", verbose_name=_("Authentication backend")
|
max_length=32, default="", verbose_name=_("Authentication backend")
|
||||||
)
|
)
|
||||||
|
@ -245,3 +248,44 @@ class UserLoginLog(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-datetime", "username"]
|
ordering = ["-datetime", "username"]
|
||||||
verbose_name = _("User login log")
|
verbose_name = _("User login log")
|
||||||
|
|
||||||
|
|
||||||
|
class UserSession(models.Model):
|
||||||
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
|
ip = models.GenericIPAddressField(verbose_name=_("Login IP"))
|
||||||
|
key = models.CharField(max_length=128, verbose_name=_("Session key"))
|
||||||
|
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"))
|
||||||
|
type = models.CharField(choices=LoginTypeChoices.choices, max_length=2, verbose_name=_("Login type"))
|
||||||
|
backend = models.CharField(max_length=32, default="", verbose_name=_("Authentication backend"))
|
||||||
|
date_created = models.DateTimeField(null=True, blank=True, verbose_name=_('Date created'))
|
||||||
|
date_expired = models.DateTimeField(null=True, blank=True, verbose_name=_("Date expired"), db_index=True)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
'users.User', verbose_name=_('User'), related_name='sessions', on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '%s(%s)' % (self.user, self.ip)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backend_display(self):
|
||||||
|
return gettext(self.backend)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_keys():
|
||||||
|
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
cache_key_prefix = session_store_cls.cache_key_prefix
|
||||||
|
keys = caches[settings.SESSION_CACHE_ALIAS].keys('*')
|
||||||
|
return [k.replace(cache_key_prefix, '') for k in keys]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def clear_expired_sessions(cls):
|
||||||
|
cls.objects.filter(date_expired__lt=timezone.now()).delete()
|
||||||
|
cls.objects.exclude(key__in=cls.get_keys()).delete()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-date_created']
|
||||||
|
verbose_name = _('User session')
|
||||||
|
permissions = [
|
||||||
|
('offline_usersession', _('Offline ussr session')),
|
||||||
|
]
|
||||||
|
|
|
@ -4,12 +4,13 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from audits.backends.db import OperateLogStore
|
from audits.backends.db import OperateLogStore
|
||||||
from common.serializers.fields import LabeledChoiceField
|
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
|
||||||
from common.utils import reverse, i18n_trans
|
from common.utils import reverse, i18n_trans
|
||||||
from common.utils.timezone import as_current_tz
|
from common.utils.timezone import as_current_tz
|
||||||
from ops.serializers.job import JobExecutionSerializer
|
from ops.serializers.job import JobExecutionSerializer
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
from terminal.models import Session
|
from terminal.models import Session
|
||||||
|
from users.models import User
|
||||||
from . import models
|
from . import models
|
||||||
from .const import (
|
from .const import (
|
||||||
ActionChoices, OperateChoices,
|
ActionChoices, OperateChoices,
|
||||||
|
@ -163,3 +164,27 @@ class ActivityUnionLogSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class FileSerializer(serializers.Serializer):
|
class FileSerializer(serializers.Serializer):
|
||||||
file = serializers.FileField(allow_empty_file=True)
|
file = serializers.FileField(allow_empty_file=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UserSessionSerializer(serializers.ModelSerializer):
|
||||||
|
type = LabeledChoiceField(choices=LoginTypeChoices.choices, label=_("Type"))
|
||||||
|
user = ObjectRelatedField(required=False, queryset=User.objects, label=_('User'))
|
||||||
|
is_current_user_session = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.UserSession
|
||||||
|
fields_mini = ['id']
|
||||||
|
fields_small = fields_mini + [
|
||||||
|
'type', 'ip', 'city', 'user_agent', 'user', 'is_current_user_session',
|
||||||
|
'backend', 'backend_display', 'date_created', 'date_expired'
|
||||||
|
]
|
||||||
|
fields = fields_small
|
||||||
|
extra_kwargs = {
|
||||||
|
"backend_display": {"label": _("Authentication backend")},
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_is_current_user_session(self, obj):
|
||||||
|
request = self.context.get('request')
|
||||||
|
if not request:
|
||||||
|
return False
|
||||||
|
return request.session.session_key == obj.key
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
from datetime import timedelta
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
@ -8,10 +11,13 @@ from django.utils.functional import LazyObject
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
||||||
|
from audits.models import UserLoginLog
|
||||||
from authentication.signals import post_auth_failed, post_auth_success
|
from authentication.signals import post_auth_failed, post_auth_success
|
||||||
from authentication.utils import check_different_city_login_if_need
|
from authentication.utils import check_different_city_login_if_need
|
||||||
from common.utils import get_request_ip, get_logger
|
from common.utils import get_request_ip, get_logger
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
from ..const import LoginTypeChoices
|
||||||
|
from ..models import UserSession
|
||||||
from ..utils import write_login_log
|
from ..utils import write_login_log
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
@ -32,6 +38,7 @@ class AuthBackendLabelMapping(LazyObject):
|
||||||
backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _("FeiShu")
|
backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _("FeiShu")
|
||||||
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk")
|
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk")
|
||||||
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token")
|
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token")
|
||||||
|
backend_label_mapping[settings.AUTH_BACKEND_PASSKEY] = _("Passkey")
|
||||||
return backend_label_mapping
|
return backend_label_mapping
|
||||||
|
|
||||||
def _setup(self):
|
def _setup(self):
|
||||||
|
@ -74,6 +81,27 @@ def generate_data(username, request, login_type=None):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def create_user_session(request, user_id, instance: UserLoginLog):
|
||||||
|
session_key = request.session.session_key or '-'
|
||||||
|
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
session_store = session_store_cls(session_key=session_key)
|
||||||
|
ttl = session_store.get_expiry_age()
|
||||||
|
|
||||||
|
online_session_data = {
|
||||||
|
'user_id': user_id,
|
||||||
|
'ip': instance.ip,
|
||||||
|
'key': session_key,
|
||||||
|
'city': instance.city,
|
||||||
|
'type': instance.type,
|
||||||
|
'backend': instance.backend,
|
||||||
|
'user_agent': instance.user_agent,
|
||||||
|
'date_created': instance.datetime,
|
||||||
|
'date_expired': instance.datetime + timedelta(seconds=ttl),
|
||||||
|
}
|
||||||
|
user_session = UserSession.objects.create(**online_session_data)
|
||||||
|
request.session['user_session_id'] = user_session.id
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_auth_success)
|
@receiver(post_auth_success)
|
||||||
def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
|
def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
|
||||||
logger.debug('User login success: {}'.format(user.username))
|
logger.debug('User login success: {}'.format(user.username))
|
||||||
|
@ -83,7 +111,11 @@ def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
|
||||||
)
|
)
|
||||||
request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S")
|
request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S")
|
||||||
data.update({'mfa': int(user.mfa_enabled), 'status': True})
|
data.update({'mfa': int(user.mfa_enabled), 'status': True})
|
||||||
write_login_log(**data)
|
instance = write_login_log(**data)
|
||||||
|
# TODO 目前只记录 web 登录的 session
|
||||||
|
if instance.type != LoginTypeChoices.web:
|
||||||
|
return
|
||||||
|
create_user_session(request, user.id, instance)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_auth_failed)
|
@receiver(post_auth_failed)
|
||||||
|
|
|
@ -15,6 +15,7 @@ router.register(r'operate-logs', api.OperateLogViewSet, 'operate-log')
|
||||||
router.register(r'password-change-logs', api.PasswordChangeLogViewSet, 'password-change-log')
|
router.register(r'password-change-logs', api.PasswordChangeLogViewSet, 'password-change-log')
|
||||||
router.register(r'job-logs', api.JobAuditViewSet, 'job-log')
|
router.register(r'job-logs', api.JobAuditViewSet, 'job-log')
|
||||||
router.register(r'my-login-logs', api.MyLoginLogViewSet, 'my-login-log')
|
router.register(r'my-login-logs', api.MyLoginLogViewSet, 'my-login-log')
|
||||||
|
router.register(r'user-sessions', api.UserSessionViewSet, 'user-session')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('activities/', api.ResourceActivityAPIView.as_view(), name='resource-activities'),
|
path('activities/', api.ResourceActivityAPIView.as_view(), name='resource-activities'),
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import copy
|
import copy
|
||||||
from itertools import chain
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from common.utils.timezone import as_current_tz
|
|
||||||
from common.utils import validate_ip, get_ip_city, get_logger
|
|
||||||
from common.db.fields import RelatedManager
|
from common.db.fields import RelatedManager
|
||||||
|
from common.utils import validate_ip, get_ip_city, get_logger
|
||||||
|
from common.utils.timezone import as_current_tz
|
||||||
from .const import DEFAULT_CITY
|
from .const import DEFAULT_CITY
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
@ -22,7 +22,7 @@ def write_login_log(*args, **kwargs):
|
||||||
else:
|
else:
|
||||||
city = get_ip_city(ip) or DEFAULT_CITY
|
city = get_ip_city(ip) or DEFAULT_CITY
|
||||||
kwargs.update({'ip': ip, 'city': city})
|
kwargs.update({'ip': ip, 'city': city})
|
||||||
UserLoginLog.objects.create(**kwargs)
|
return UserLoginLog.objects.create(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def _get_instance_field_value(
|
def _get_instance_field_value(
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
|
||||||
from .connection_token import *
|
|
||||||
from .token import *
|
|
||||||
from .mfa import *
|
|
||||||
from .access_key import *
|
from .access_key import *
|
||||||
from .confirm import *
|
from .confirm import *
|
||||||
from .login_confirm import *
|
from .connection_token import *
|
||||||
from .sso import *
|
|
||||||
from .wecom import *
|
|
||||||
from .dingtalk import *
|
from .dingtalk import *
|
||||||
from .feishu import *
|
from .feishu import *
|
||||||
|
from .login_confirm import *
|
||||||
|
from .mfa import *
|
||||||
from .password import *
|
from .password import *
|
||||||
|
from .sso import *
|
||||||
from .temp_token import *
|
from .temp_token import *
|
||||||
|
from .token import *
|
||||||
|
from .wecom import *
|
||||||
|
|
|
@ -13,7 +13,7 @@ from ..serializers import ConfirmSerializer
|
||||||
|
|
||||||
|
|
||||||
class ConfirmBindORUNBindOAuth(RetrieveAPIView):
|
class ConfirmBindORUNBindOAuth(RetrieveAPIView):
|
||||||
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
|
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),)
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
return Response('ok')
|
return Response('ok')
|
||||||
|
|
|
@ -24,6 +24,8 @@ from orgs.mixins.api import RootOrgViewMixin
|
||||||
from perms.models import ActionChoices
|
from perms.models import ActionChoices
|
||||||
from terminal.connect_methods import NativeClient, ConnectMethodUtil
|
from terminal.connect_methods import NativeClient, ConnectMethodUtil
|
||||||
from terminal.models import EndpointRule, Endpoint
|
from terminal.models import EndpointRule, Endpoint
|
||||||
|
from users.const import FileNameConflictResolution
|
||||||
|
from users.models import Preference
|
||||||
from ..models import ConnectionToken, date_expired_default
|
from ..models import ConnectionToken, date_expired_default
|
||||||
from ..serializers import (
|
from ..serializers import (
|
||||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||||
|
@ -310,9 +312,20 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
||||||
self.validate_serializer(serializer)
|
self.validate_serializer(serializer)
|
||||||
return super().perform_create(serializer)
|
return super().perform_create(serializer)
|
||||||
|
|
||||||
|
def _insert_connect_options(self, data, user):
|
||||||
|
name = 'file_name_conflict_resolution'
|
||||||
|
connect_options = data.pop('connect_options', {})
|
||||||
|
preference = Preference.objects.filter(
|
||||||
|
name=name, user=user, category='koko'
|
||||||
|
).first()
|
||||||
|
value = preference.value if preference else FileNameConflictResolution.REPLACE
|
||||||
|
connect_options[name] = value
|
||||||
|
data['connect_options'] = connect_options
|
||||||
|
|
||||||
def validate_serializer(self, serializer):
|
def validate_serializer(self, serializer):
|
||||||
data = serializer.validated_data
|
data = serializer.validated_data
|
||||||
user = self.get_user(serializer)
|
user = self.get_user(serializer)
|
||||||
|
self._insert_connect_options(data, user)
|
||||||
asset = data.get('asset')
|
asset = data.get('asset')
|
||||||
account_name = data.get('account')
|
account_name = data.get('account')
|
||||||
_data = self._validate(user, asset, account_name)
|
_data = self._validate(user, asset, account_name)
|
||||||
|
@ -363,7 +376,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
||||||
|
|
||||||
def _validate_acl(self, user, asset, account):
|
def _validate_acl(self, user, asset, account):
|
||||||
from acls.models import LoginAssetACL
|
from acls.models import LoginAssetACL
|
||||||
acls = LoginAssetACL.filter_queryset(user, asset, account)
|
acls = LoginAssetACL.filter_queryset(user=user, asset=asset, account=account)
|
||||||
ip = get_request_ip(self.request)
|
ip = get_request_ip(self.request)
|
||||||
acl = LoginAssetACL.get_match_rule_acls(user, ip, acls)
|
acl = LoginAssetACL.get_match_rule_acls(user, ip, acls)
|
||||||
if not acl:
|
if not acl:
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from users.models import User
|
|
||||||
from common.utils import get_logger
|
|
||||||
from common.permissions import UserConfirmation
|
|
||||||
from common.api import RoleUserMixin, RoleAdminMixin
|
|
||||||
from authentication.const import ConfirmType
|
|
||||||
from authentication import errors
|
from authentication import errors
|
||||||
|
from authentication.const import ConfirmType
|
||||||
|
from common.api import RoleUserMixin, RoleAdminMixin
|
||||||
|
from common.permissions import UserConfirmation, IsValidUser
|
||||||
|
from common.utils import get_logger
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class DingTalkQRUnBindBase(APIView):
|
||||||
|
|
||||||
|
|
||||||
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
|
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
|
||||||
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
|
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),)
|
||||||
|
|
||||||
|
|
||||||
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
|
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from users.models import User
|
|
||||||
from common.utils import get_logger
|
|
||||||
from common.permissions import UserConfirmation
|
|
||||||
from common.api import RoleUserMixin, RoleAdminMixin
|
|
||||||
from authentication.const import ConfirmType
|
|
||||||
from authentication import errors
|
from authentication import errors
|
||||||
|
from authentication.const import ConfirmType
|
||||||
|
from common.api import RoleUserMixin, RoleAdminMixin
|
||||||
|
from common.permissions import UserConfirmation, IsValidUser
|
||||||
|
from common.utils import get_logger
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class FeiShuQRUnBindBase(APIView):
|
||||||
|
|
||||||
|
|
||||||
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
|
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
|
||||||
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
|
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),)
|
||||||
|
|
||||||
|
|
||||||
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
|
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
|
||||||
|
@ -38,7 +38,7 @@ class FeiShuEventSubscriptionCallback(APIView):
|
||||||
"""
|
"""
|
||||||
# https://open.feishu.cn/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM
|
# https://open.feishu.cn/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM
|
||||||
"""
|
"""
|
||||||
permission_classes = ()
|
permission_classes = (IsValidUser,)
|
||||||
|
|
||||||
def post(self, request: Request, *args, **kwargs):
|
def post(self, request: Request, *args, **kwargs):
|
||||||
return Response(data=request.data)
|
return Response(data=request.data)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from rest_framework import exceptions
|
||||||
from rest_framework.generics import CreateAPIView
|
from rest_framework.generics import CreateAPIView
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
@ -13,6 +14,7 @@ from common.utils import get_logger
|
||||||
from users.models.user import User
|
from users.models.user import User
|
||||||
from .. import errors
|
from .. import errors
|
||||||
from .. import serializers
|
from .. import serializers
|
||||||
|
from ..errors import SessionEmptyError
|
||||||
from ..mixins import AuthMixin
|
from ..mixins import AuthMixin
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
@ -56,6 +58,7 @@ class MFASendCodeApi(AuthMixin, CreateAPIView):
|
||||||
if not mfa_backend or not mfa_backend.challenge_required:
|
if not mfa_backend or not mfa_backend.challenge_required:
|
||||||
error = _('Current user not support mfa type: {}').format(mfa_type)
|
error = _('Current user not support mfa type: {}').format(mfa_type)
|
||||||
raise ValidationError({'error': error})
|
raise ValidationError({'error': error})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mfa_backend.send_challenge()
|
mfa_backend.send_challenge()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -66,6 +69,15 @@ class MFAChallengeVerifyApi(AuthMixin, CreateAPIView):
|
||||||
permission_classes = (AllowAny,)
|
permission_classes = (AllowAny,)
|
||||||
serializer_class = serializers.MFAChallengeSerializer
|
serializer_class = serializers.MFAChallengeSerializer
|
||||||
|
|
||||||
|
def initial(self, request, *args, **kwargs):
|
||||||
|
super().initial(request, *args, **kwargs)
|
||||||
|
try:
|
||||||
|
user = self.get_user_from_session()
|
||||||
|
except SessionEmptyError:
|
||||||
|
user = None
|
||||||
|
if not user:
|
||||||
|
raise exceptions.NotAuthenticated()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
user = self.get_user_from_session()
|
user = self.get_user_from_session()
|
||||||
code = serializer.validated_data.get('code')
|
code = serializer.validated_data.get('code')
|
||||||
|
|
|
@ -1,26 +1,27 @@
|
||||||
from uuid import UUID
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from django.contrib.auth import login
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import login
|
||||||
from django.http.response import HttpResponseRedirect
|
from django.http.response import HttpResponseRedirect
|
||||||
|
from rest_framework import serializers
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.request import Request
|
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from common.utils.timezone import utc_now
|
|
||||||
from common.const.http import POST, GET
|
|
||||||
from common.api import JMSGenericViewSet
|
from common.api import JMSGenericViewSet
|
||||||
from common.serializers import EmptySerializer
|
from common.const.http import POST, GET
|
||||||
from common.permissions import OnlySuperUser
|
from common.permissions import OnlySuperUser
|
||||||
|
from common.serializers import EmptySerializer
|
||||||
from common.utils import reverse
|
from common.utils import reverse
|
||||||
|
from common.utils.timezone import utc_now
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from ..serializers import SSOTokenSerializer
|
from ..errors import SSOAuthClosed
|
||||||
from ..models import SSOToken
|
|
||||||
from ..filters import AuthKeyQueryDeclaration
|
from ..filters import AuthKeyQueryDeclaration
|
||||||
from ..mixins import AuthMixin
|
from ..mixins import AuthMixin
|
||||||
from ..errors import SSOAuthClosed
|
from ..models import SSOToken
|
||||||
|
from ..serializers import SSOTokenSerializer
|
||||||
|
|
||||||
NEXT_URL = 'next'
|
NEXT_URL = 'next'
|
||||||
AUTH_KEY = 'authkey'
|
AUTH_KEY = 'authkey'
|
||||||
|
@ -67,6 +68,9 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet):
|
||||||
if not next_url or not next_url.startswith('/'):
|
if not next_url or not next_url.startswith('/'):
|
||||||
next_url = reverse('index')
|
next_url = reverse('index')
|
||||||
|
|
||||||
|
if not authkey:
|
||||||
|
raise serializers.ValidationError("authkey is required")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
authkey = UUID(authkey)
|
authkey = UUID(authkey)
|
||||||
token = SSOToken.objects.get(authkey=authkey, expired=False)
|
token = SSOToken.objects.get(authkey=authkey, expired=False)
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
from rest_framework.views import APIView
|
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from users.models import User
|
|
||||||
from common.utils import get_logger
|
|
||||||
from common.permissions import UserConfirmation
|
|
||||||
from common.api import RoleUserMixin, RoleAdminMixin
|
|
||||||
from authentication.const import ConfirmType
|
|
||||||
from authentication import errors
|
from authentication import errors
|
||||||
|
from authentication.const import ConfirmType
|
||||||
|
from common.api import RoleUserMixin, RoleAdminMixin
|
||||||
|
from common.permissions import UserConfirmation, IsValidUser
|
||||||
|
from common.utils import get_logger
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class WeComQRUnBindBase(APIView):
|
||||||
|
|
||||||
|
|
||||||
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
|
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
|
||||||
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
|
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.ReLogin),)
|
||||||
|
|
||||||
|
|
||||||
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
|
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from .backends import *
|
|
@ -0,0 +1,59 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentication.mixins import AuthMixin
|
||||||
|
from .fido import register_begin, register_complete, auth_begin, auth_complete
|
||||||
|
from .models import Passkey
|
||||||
|
from .serializer import PasskeySerializer
|
||||||
|
from ...views import FlashMessageMixin
|
||||||
|
|
||||||
|
|
||||||
|
class PasskeyViewSet(AuthMixin, FlashMessageMixin, ModelViewSet):
|
||||||
|
serializer_class = PasskeySerializer
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Passkey.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
|
@action(methods=['get', 'post'], detail=False, url_path='register')
|
||||||
|
def register(self, request):
|
||||||
|
if request.method == 'GET':
|
||||||
|
register_data, state = register_begin(request)
|
||||||
|
return JsonResponse(dict(register_data))
|
||||||
|
else:
|
||||||
|
passkey = register_complete(request)
|
||||||
|
return JsonResponse({'id': passkey.id.__str__(), 'name': passkey.name})
|
||||||
|
|
||||||
|
@action(methods=['get'], detail=False, url_path='login', permission_classes=[AllowAny])
|
||||||
|
def login(self, request):
|
||||||
|
return render(request, 'authentication/passkey.html', {})
|
||||||
|
|
||||||
|
def redirect_to_error(self, error):
|
||||||
|
self.send_auth_signal(success=False, username='unknown', reason='passkey')
|
||||||
|
return render(self.request, 'authentication/passkey.html', {'error': error})
|
||||||
|
|
||||||
|
@action(methods=['get', 'post'], detail=False, url_path='auth', permission_classes=[AllowAny])
|
||||||
|
def auth(self, request):
|
||||||
|
if request.method == 'GET':
|
||||||
|
auth_data = auth_begin(request)
|
||||||
|
return JsonResponse(dict(auth_data))
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = auth_complete(request)
|
||||||
|
except ValueError as e:
|
||||||
|
return self.redirect_to_error(str(e))
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return self.redirect_to_error(_('Auth failed'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.check_oauth2_auth(user, settings.AUTH_BACKEND_PASSKEY)
|
||||||
|
return self.redirect_to_guard_view()
|
||||||
|
except Exception as e:
|
||||||
|
msg = getattr(e, 'msg', '') or str(e)
|
||||||
|
return self.redirect_to_error(msg)
|
|
@ -0,0 +1,9 @@
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from ..base import JMSModelBackend
|
||||||
|
|
||||||
|
|
||||||
|
class PasskeyAuthBackend(JMSModelBackend):
|
||||||
|
@staticmethod
|
||||||
|
def is_enabled():
|
||||||
|
return settings.AUTH_PASSKEY
|
|
@ -0,0 +1,157 @@
|
||||||
|
import json
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import fido2.features
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from fido2.server import Fido2Server
|
||||||
|
from fido2.utils import websafe_decode, websafe_encode
|
||||||
|
from fido2.webauthn import PublicKeyCredentialRpEntity, AttestedCredentialData, PublicKeyCredentialUserEntity
|
||||||
|
from rest_framework.serializers import ValidationError
|
||||||
|
from user_agents.parsers import parse as ua_parse
|
||||||
|
|
||||||
|
from common.utils import get_logger
|
||||||
|
from .models import Passkey
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
fido2.features.webauthn_json_mapping.enabled = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_platform(request):
|
||||||
|
ua = ua_parse(request.META["HTTP_USER_AGENT"])
|
||||||
|
if 'Safari' in ua.browser.family:
|
||||||
|
return "Apple"
|
||||||
|
elif 'Chrome' in ua.browser.family and ua.os.family == "Mac OS X":
|
||||||
|
return "Chrome on Apple"
|
||||||
|
elif 'Android' in ua.os.family:
|
||||||
|
return "Google"
|
||||||
|
elif "Windows" in ua.os.family:
|
||||||
|
return "Microsoft"
|
||||||
|
else:
|
||||||
|
return "Key"
|
||||||
|
|
||||||
|
|
||||||
|
def get_server_id_from_request(request, allowed=()):
|
||||||
|
origin = request.META.get('HTTP_REFERER')
|
||||||
|
if not origin:
|
||||||
|
origin = request.get_host()
|
||||||
|
p = urlparse(origin)
|
||||||
|
if p.netloc in allowed or p.hostname in allowed:
|
||||||
|
return p.hostname
|
||||||
|
else:
|
||||||
|
return 'localhost'
|
||||||
|
|
||||||
|
|
||||||
|
def default_server_id(request):
|
||||||
|
domains = list(settings.ALLOWED_DOMAINS)
|
||||||
|
if settings.SITE_URL:
|
||||||
|
domains.append(urlparse(settings.SITE_URL).hostname)
|
||||||
|
return get_server_id_from_request(request, allowed=domains)
|
||||||
|
|
||||||
|
|
||||||
|
def get_server(request=None):
|
||||||
|
"""Get Server Info from settings and returns a Fido2Server"""
|
||||||
|
|
||||||
|
server_id = settings.FIDO_SERVER_ID or default_server_id(request)
|
||||||
|
if callable(server_id):
|
||||||
|
fido_server_id = settings.FIDO_SERVER_ID(request)
|
||||||
|
elif ',' in server_id:
|
||||||
|
fido_server_id = get_server_id_from_request(request, allowed=server_id.split(','))
|
||||||
|
else:
|
||||||
|
fido_server_id = server_id
|
||||||
|
|
||||||
|
logger.debug('Fido server id: {}'.format(fido_server_id))
|
||||||
|
if callable(settings.FIDO_SERVER_NAME):
|
||||||
|
fido_server_name = settings.FIDO_SERVER_NAME(request)
|
||||||
|
else:
|
||||||
|
fido_server_name = settings.FIDO_SERVER_NAME
|
||||||
|
|
||||||
|
rp = PublicKeyCredentialRpEntity(id=fido_server_id, name=fido_server_name)
|
||||||
|
return Fido2Server(rp)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_credentials(username):
|
||||||
|
user_passkeys = Passkey.objects.filter(user__username=username)
|
||||||
|
return [AttestedCredentialData(websafe_decode(uk.token)) for uk in user_passkeys]
|
||||||
|
|
||||||
|
|
||||||
|
def register_begin(request):
|
||||||
|
server = get_server(request)
|
||||||
|
user = request.user
|
||||||
|
user_credentials = get_user_credentials(user.username)
|
||||||
|
|
||||||
|
prefix = request.query_params.get('name', '')
|
||||||
|
prefix = '(' + prefix + ')'
|
||||||
|
user_entity = PublicKeyCredentialUserEntity(
|
||||||
|
id=str(user.id).encode('utf8'),
|
||||||
|
name=user.username + prefix,
|
||||||
|
display_name=user.name,
|
||||||
|
)
|
||||||
|
auth_attachment = getattr(settings, 'KEY_ATTACHMENT', None)
|
||||||
|
data, state = server.register_begin(
|
||||||
|
user_entity, user_credentials,
|
||||||
|
authenticator_attachment=auth_attachment,
|
||||||
|
resident_key_requirement=fido2.webauthn.ResidentKeyRequirement.PREFERRED
|
||||||
|
)
|
||||||
|
request.session['fido2_state'] = state
|
||||||
|
data = dict(data)
|
||||||
|
return data, state
|
||||||
|
|
||||||
|
|
||||||
|
def register_complete(request):
|
||||||
|
if not request.session.get("fido2_state"):
|
||||||
|
raise ValidationError("No state found")
|
||||||
|
data = request.data
|
||||||
|
server = get_server(request)
|
||||||
|
state = request.session.pop("fido2_state")
|
||||||
|
auth_data = server.register_complete(state, response=data)
|
||||||
|
encoded = websafe_encode(auth_data.credential_data)
|
||||||
|
platform = get_current_platform(request)
|
||||||
|
name = data.pop("key_name", '') or platform
|
||||||
|
passkey = Passkey.objects.create(
|
||||||
|
user=request.user,
|
||||||
|
token=encoded,
|
||||||
|
name=name,
|
||||||
|
platform=platform,
|
||||||
|
credential_id=data.get('id')
|
||||||
|
)
|
||||||
|
return passkey
|
||||||
|
|
||||||
|
|
||||||
|
def auth_begin(request):
|
||||||
|
server = get_server(request)
|
||||||
|
credentials = []
|
||||||
|
|
||||||
|
username = None
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
username = request.user.username
|
||||||
|
if username:
|
||||||
|
credentials = get_user_credentials(username)
|
||||||
|
auth_data, state = server.authenticate_begin(credentials)
|
||||||
|
request.session['fido2_state'] = state
|
||||||
|
return auth_data
|
||||||
|
|
||||||
|
|
||||||
|
def auth_complete(request):
|
||||||
|
server = get_server(request)
|
||||||
|
data = request.data.get("passkeys")
|
||||||
|
data = json.loads(data)
|
||||||
|
cid = data['id']
|
||||||
|
|
||||||
|
key = Passkey.objects.filter(credential_id=cid, is_active=True).first()
|
||||||
|
if not key:
|
||||||
|
raise ValueError(_("This key is not registered"))
|
||||||
|
|
||||||
|
credentials = [AttestedCredentialData(websafe_decode(key.token))]
|
||||||
|
state = request.session.get('fido2_state')
|
||||||
|
server.authenticate_complete(state, credentials=credentials, response=data)
|
||||||
|
|
||||||
|
request.session["passkey"] = '{}_{}'.format(key.id, key.name)
|
||||||
|
key.date_last_used = timezone.now()
|
||||||
|
key.save(update_fields=['date_last_used'])
|
||||||
|
return key.user
|
|
@ -0,0 +1,19 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from common.db.models import JMSBaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Passkey(JMSBaseModel):
|
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
|
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
||||||
|
is_active = models.BooleanField(default=True, verbose_name=_("Enabled"))
|
||||||
|
platform = models.CharField(max_length=255, default='', verbose_name=_("Platform"))
|
||||||
|
added_on = models.DateTimeField(auto_now_add=True, verbose_name=_("Added on"))
|
||||||
|
date_last_used = models.DateTimeField(null=True, default=None, verbose_name=_("Date last used"))
|
||||||
|
credential_id = models.CharField(max_length=255, unique=True, null=False, verbose_name=_("Credential ID"))
|
||||||
|
token = models.CharField(max_length=255, null=False, verbose_name=_("Token"))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
|
@ -0,0 +1,13 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from .models import Passkey
|
||||||
|
|
||||||
|
|
||||||
|
class PasskeySerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Passkey
|
||||||
|
fields = [
|
||||||
|
'id', 'name', 'is_active', 'platform', 'created_by',
|
||||||
|
'date_last_used', 'date_created',
|
||||||
|
]
|
||||||
|
read_only_fields = list(set(fields) - {'is_active'})
|
|
@ -0,0 +1,9 @@
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register('passkeys', api.PasskeyViewSet, 'passkey')
|
||||||
|
|
||||||
|
urlpatterns = []
|
||||||
|
urlpatterns += router.urls
|
|
@ -146,7 +146,9 @@ class PrepareRequestMixin:
|
||||||
},
|
},
|
||||||
'singleLogoutService': {
|
'singleLogoutService': {
|
||||||
'url': f"{sp_host}{reverse('authentication:saml2:saml2-logout')}"
|
'url': f"{sp_host}{reverse('authentication:saml2:saml2-logout')}"
|
||||||
}
|
},
|
||||||
|
'privateKey': getattr(settings, 'SAML2_SP_KEY_CONTENT', ''),
|
||||||
|
'x509cert': getattr(settings, 'SAML2_SP_CERT_CONTENT', ''),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sp_settings['sp'].update(attrs)
|
sp_settings['sp'].update(attrs)
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Generated by Django 4.1.10 on 2023-09-08 08:10
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('authentication', '0021_auto_20230713_1459'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Passkey',
|
||||||
|
fields=[
|
||||||
|
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
|
||||||
|
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
|
||||||
|
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||||
|
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||||
|
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Enabled')),
|
||||||
|
('platform', models.CharField(default='', max_length=255, verbose_name='Platform')),
|
||||||
|
('added_on', models.DateTimeField(auto_now_add=True, verbose_name='Added on')),
|
||||||
|
('date_last_used', models.DateTimeField(default=None, null=True, verbose_name='Date last used')),
|
||||||
|
('credential_id', models.CharField(max_length=255, unique=True, verbose_name='Credential ID')),
|
||||||
|
('token', models.CharField(max_length=255, verbose_name='Token')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -132,11 +132,11 @@ class CommonMixin:
|
||||||
return user
|
return user
|
||||||
|
|
||||||
user_id = self.request.session.get('user_id')
|
user_id = self.request.session.get('user_id')
|
||||||
auth_password = self.request.session.get('auth_password')
|
auth_ok = self.request.session.get('auth_password')
|
||||||
auth_expired_at = self.request.session.get('auth_password_expired_at')
|
auth_expired_at = self.request.session.get('auth_password_expired_at')
|
||||||
auth_expired = auth_expired_at < time.time() if auth_expired_at else False
|
auth_expired = auth_expired_at < time.time() if auth_expired_at else False
|
||||||
|
|
||||||
if not user_id or not auth_password or auth_expired:
|
if not user_id or not auth_ok or auth_expired:
|
||||||
raise errors.SessionEmptyError()
|
raise errors.SessionEmptyError()
|
||||||
|
|
||||||
user = get_object_or_404(User, pk=user_id)
|
user = get_object_or_404(User, pk=user_id)
|
||||||
|
@ -479,6 +479,7 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
|
||||||
request.session['auto_login'] = auto_login
|
request.session['auto_login'] = auto_login
|
||||||
if not auth_backend:
|
if not auth_backend:
|
||||||
auth_backend = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
|
auth_backend = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
|
||||||
|
|
||||||
request.session['auth_backend'] = auth_backend
|
request.session['auth_backend'] = auth_backend
|
||||||
|
|
||||||
def check_oauth2_auth(self, user: User, auth_backend):
|
def check_oauth2_auth(self, user: User, auth_backend):
|
||||||
|
@ -511,7 +512,8 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
|
||||||
|
|
||||||
def clear_auth_mark(self):
|
def clear_auth_mark(self):
|
||||||
keys = [
|
keys = [
|
||||||
'auth_password', 'user_id', 'auth_confirm_required', 'auth_ticket_id', 'auth_acl_id'
|
'auth_password', 'user_id', 'auth_confirm_required',
|
||||||
|
'auth_ticket_id', 'auth_acl_id'
|
||||||
]
|
]
|
||||||
for k in keys:
|
for k in keys:
|
||||||
self.request.session.pop(k, '')
|
self.request.session.pop(k, '')
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.dispatch import receiver
|
||||||
from django_cas_ng.signals import cas_user_authenticated
|
from django_cas_ng.signals import cas_user_authenticated
|
||||||
|
|
||||||
from apps.jumpserver.settings.auth import AUTHENTICATION_BACKENDS_THIRD_PARTY
|
from apps.jumpserver.settings.auth import AUTHENTICATION_BACKENDS_THIRD_PARTY
|
||||||
|
from audits.models import UserSession
|
||||||
from .signals import post_auth_success, post_auth_failed, user_auth_failed, user_auth_success
|
from .signals import post_auth_success, post_auth_failed, user_auth_failed, user_auth_success
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,6 +24,9 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
|
||||||
if not request.session.get("auth_third_party_done") and \
|
if not request.session.get("auth_third_party_done") and \
|
||||||
request.session.get('auth_backend') in AUTHENTICATION_BACKENDS_THIRD_PARTY:
|
request.session.get('auth_backend') in AUTHENTICATION_BACKENDS_THIRD_PARTY:
|
||||||
request.session['auth_third_party_required'] = 1
|
request.session['auth_third_party_required'] = 1
|
||||||
|
|
||||||
|
user_session_id = request.session.get('user_session_id')
|
||||||
|
UserSession.objects.filter(id=user_session_id).update(key=request.session.session_key)
|
||||||
# 单点登录,超过了自动退出
|
# 单点登录,超过了自动退出
|
||||||
if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED:
|
if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED:
|
||||||
lock_key = 'single_machine_login_' + str(user.id)
|
lock_key = 'single_machine_login_' + str(user.id)
|
||||||
|
@ -30,6 +34,7 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
|
||||||
if session_key and session_key != request.session.session_key:
|
if session_key and session_key != request.session.session_key:
|
||||||
session = import_module(settings.SESSION_ENGINE).SessionStore(session_key)
|
session = import_module(settings.SESSION_ENGINE).SessionStore(session_key)
|
||||||
session.delete()
|
session.delete()
|
||||||
|
UserSession.objects.filter(key=session_key).delete()
|
||||||
cache.set(lock_key, request.session.session_key, None)
|
cache.set(lock_key, request.session.session_key, None)
|
||||||
|
|
||||||
# 标记登录,设置 cookie,前端可以控制刷新, Middleware 会拦截这个生成 cookie
|
# 标记登录,设置 cookie,前端可以控制刷新, Middleware 会拦截这个生成 cookie
|
||||||
|
|
|
@ -223,10 +223,55 @@
|
||||||
height: 13px;
|
height: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-info {
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-logo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-form-box {
|
||||||
|
width: 100%;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-image-box {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-top-links {
|
||||||
|
display: inline;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-logo {
|
||||||
|
display: block;
|
||||||
|
padding: 20px 30px 0 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
{% if error_origin %}
|
||||||
|
<div class='alert alert-danger error-info'>
|
||||||
|
{% trans 'Configuration file has problems and cannot be logged in. Please contact the administrator or view latest docs' %}<br/>
|
||||||
|
{% trans 'If you are administrator, you can update the config resolve it, set' %} <br/>
|
||||||
|
DOMAINS={{ error_origin }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="login-content extra-fields-{{ extra_fields_count }}">
|
<div class="login-content extra-fields-{{ extra_fields_count }}">
|
||||||
<div class="right-image-box">
|
<div class="right-image-box">
|
||||||
<a href="{% if not XPACK_ENABLED %}https://github.com/jumpserver/jumpserver.git{% endif %}">
|
<a href="{% if not XPACK_ENABLED %}https://github.com/jumpserver/jumpserver.git{% endif %}">
|
||||||
|
@ -234,6 +279,11 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="left-form-box {% if not form.challenge and not form.captcha %} no-captcha-challenge {% endif %}">
|
<div class="left-form-box {% if not form.challenge and not form.captcha %} no-captcha-challenge {% endif %}">
|
||||||
|
<div class="mobile-logo">
|
||||||
|
<a href="{% if not XPACK_ENABLED %}https://github.com/jumpserver/jumpserver.git{% endif %}">
|
||||||
|
<img src="{% static 'img/logo_text_green.png' %}" class="right-image" alt="screen-image"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div style="position: relative;top: 50%;transform: translateY(-50%);">
|
<div style="position: relative;top: 50%;transform: translateY(-50%);">
|
||||||
<div style='padding: 15px 60px; text-align: left'>
|
<div style='padding: 15px 60px; text-align: left'>
|
||||||
<h2 style='font-weight: 400;display: inline'>
|
<h2 style='font-weight: 400;display: inline'>
|
||||||
|
@ -294,13 +344,13 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="form-group auto-login" style="margin-bottom: 10px">
|
<div class="form-group auto-login" style="margin-bottom: 10px">
|
||||||
<div class="row" style="overflow: hidden;">
|
<div class="row" style="overflow: hidden;">
|
||||||
<div class="col-md-6" style="text-align: left">
|
<div class="col-md-6 col-xs-6" style="text-align: left">
|
||||||
{% if form.auto_login %}
|
{% if form.auto_login %}
|
||||||
{% bootstrap_field form.auto_login form_group_class='auto_login_box' %}
|
{% bootstrap_field form.auto_login form_group_class='auto_login_box' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6" style="line-height: 25px">
|
<div class="col-md-6 col-xs-6" style="line-height: 25px">
|
||||||
<a id="forgot_password" href="{{ forgot_password_url }}" style="float: right">
|
<a id="forgot_password" href="{{ forgot_password_url }}" style="float: right">
|
||||||
<small>{% trans 'Forgot password' %}?</small>
|
<small>{% trans 'Forgot password' %}?</small>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Login passkey</title>
|
||||||
|
<script src="{% static "js/jquery-3.6.1.min.js" %}?_=9"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form action='{% url 'api-auth:passkey-auth' %}' method="post" id="loginForm">
|
||||||
|
<input type="hidden" name="passkeys" id="passkeys"/>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
const loginUrl = "/core/auth/login/";
|
||||||
|
window.conditionalUI = false;
|
||||||
|
window.conditionUIAbortController = new AbortController();
|
||||||
|
window.conditionUIAbortSignal = conditionUIAbortController.signal;
|
||||||
|
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
|
||||||
|
|
||||||
|
// Use a lookup table to find the index.
|
||||||
|
const lookup = new Uint8Array(256)
|
||||||
|
for (let i = 0; i < chars.length; i++) {
|
||||||
|
lookup[chars.charCodeAt(i)] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
const encode = function (arraybuffer) {
|
||||||
|
const bytes = new Uint8Array(arraybuffer)
|
||||||
|
let i;
|
||||||
|
const len = bytes.length;
|
||||||
|
let base64url = ''
|
||||||
|
|
||||||
|
for (i = 0; i < len; i += 3) {
|
||||||
|
base64url += chars[bytes[i] >> 2]
|
||||||
|
base64url += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]
|
||||||
|
base64url += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]
|
||||||
|
base64url += chars[bytes[i + 2] & 63]
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((len % 3) === 2) {
|
||||||
|
base64url = base64url.substring(0, base64url.length - 1)
|
||||||
|
} else if (len % 3 === 1) {
|
||||||
|
base64url = base64url.substring(0, base64url.length - 2)
|
||||||
|
}
|
||||||
|
return base64url
|
||||||
|
}
|
||||||
|
|
||||||
|
const decode = function (base64string) {
|
||||||
|
const bufferLength = base64string.length * 0.75
|
||||||
|
const len = base64string.length;
|
||||||
|
let i;
|
||||||
|
let p = 0
|
||||||
|
let encoded1;
|
||||||
|
let encoded2;
|
||||||
|
let encoded3;
|
||||||
|
let encoded4
|
||||||
|
|
||||||
|
const bytes = new Uint8Array(bufferLength)
|
||||||
|
|
||||||
|
for (i = 0; i < len; i += 4) {
|
||||||
|
encoded1 = lookup[base64string.charCodeAt(i)]
|
||||||
|
encoded2 = lookup[base64string.charCodeAt(i + 1)]
|
||||||
|
encoded3 = lookup[base64string.charCodeAt(i + 2)]
|
||||||
|
encoded4 = lookup[base64string.charCodeAt(i + 3)]
|
||||||
|
|
||||||
|
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4)
|
||||||
|
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2)
|
||||||
|
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63)
|
||||||
|
}
|
||||||
|
return bytes.buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkConditionalUI(form) {
|
||||||
|
if (!navigator.credentials) {
|
||||||
|
alert('WebAuthn is not supported in this browser')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (window.PublicKeyCredential && PublicKeyCredential.isConditionalMediationAvailable) {
|
||||||
|
// Check if conditional mediation is available.
|
||||||
|
PublicKeyCredential.isConditionalMediationAvailable().then((result) => {
|
||||||
|
window.conditionalUI = result;
|
||||||
|
if (!window.conditionalUI) {
|
||||||
|
alert("Conditional UI is not available. Please use the legacy UI.");
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKeyCredentialToJSON = (pubKeyCred) => {
|
||||||
|
if (pubKeyCred instanceof Array) {
|
||||||
|
const arr = []
|
||||||
|
for (const i of pubKeyCred) {
|
||||||
|
arr.push(publicKeyCredentialToJSON(i))
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pubKeyCred instanceof ArrayBuffer) {
|
||||||
|
return encode(pubKeyCred)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pubKeyCred instanceof Object) {
|
||||||
|
const obj = {}
|
||||||
|
for (const key in pubKeyCred) {
|
||||||
|
obj[key] = publicKeyCredentialToJSON(pubKeyCred[key])
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
return pubKeyCred
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetAssertReq(getAssert) {
|
||||||
|
getAssert.publicKey.challenge = decode(getAssert.publicKey.challenge)
|
||||||
|
|
||||||
|
for (const allowCred of getAssert.publicKey.allowCredentials) {
|
||||||
|
allowCred.id = decode(allowCred.id)
|
||||||
|
}
|
||||||
|
return getAssert
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAuthn(form, conditionalUI = false) {
|
||||||
|
window.loginForm = form
|
||||||
|
fetch('/api/v1/authentication/passkeys/auth/', {method: 'GET'}).then(function (response) {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json().then(function (req) {
|
||||||
|
return GetAssertReq(req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
throw new Error('No credential available to authenticate!')
|
||||||
|
}).then(function (options) {
|
||||||
|
if (conditionalUI) {
|
||||||
|
options.mediation = 'conditional'
|
||||||
|
options.signal = window.conditionUIAbortSignal
|
||||||
|
} else {
|
||||||
|
window.conditionUIAbortController.abort()
|
||||||
|
}
|
||||||
|
return navigator.credentials.get(options)
|
||||||
|
}).then(function (assertion) {
|
||||||
|
const pk = $('#passkeys')
|
||||||
|
if (pk.length === 0) {
|
||||||
|
retry("Did you add the 'passkeys' hidden input field")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pk.val(JSON.stringify(publicKeyCredentialToJSON(assertion)))
|
||||||
|
const x = document.getElementById(window.loginForm)
|
||||||
|
if (x === null || x === undefined) {
|
||||||
|
console.error('Did you pass the correct form id to auth function')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x.submit()
|
||||||
|
}).catch(function (err) {
|
||||||
|
retry(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeStartAuthn(form) {
|
||||||
|
checkConditionalUI('loginForm')
|
||||||
|
const errorMsg = "{% trans 'This page is not served over HTTPS. Please use HTTPS to ensure security of your credentials.' %}"
|
||||||
|
const isSafe = window.location.protocol === 'https:'
|
||||||
|
if (!isSafe && location.hostname !== 'localhost') {
|
||||||
|
alert(errorMsg)
|
||||||
|
window.location.href = loginUrl
|
||||||
|
} else {
|
||||||
|
setTimeout(() => startAuthn('loginForm'), 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function retry(error) {
|
||||||
|
const fullError = "{% trans 'Error' %}" + ': ' + error + "\n\n " + "{% trans 'Do you want to retry ?' %}"
|
||||||
|
const result = confirm(fullError)
|
||||||
|
if (result) {
|
||||||
|
safeStartAuthn()
|
||||||
|
} else {
|
||||||
|
window.location.href = loginUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{% if not error %}
|
||||||
|
window.onload = function () {
|
||||||
|
safeStartAuthn()
|
||||||
|
}
|
||||||
|
{% else %}
|
||||||
|
const error = "{{ error }}"
|
||||||
|
retry(error)
|
||||||
|
{% endif %}
|
||||||
|
</script>
|
||||||
|
</html>
|
|
@ -4,6 +4,7 @@ from django.urls import path
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
from .. import api
|
from .. import api
|
||||||
|
from ..backends.passkey.urls import urlpatterns as passkey_urlpatterns
|
||||||
|
|
||||||
app_name = 'authentication'
|
app_name = 'authentication'
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
|
@ -13,17 +14,19 @@ router.register('temp-tokens', api.TempTokenViewSet, 'temp-token')
|
||||||
router.register('connection-token', api.ConnectionTokenViewSet, 'connection-token')
|
router.register('connection-token', api.ConnectionTokenViewSet, 'connection-token')
|
||||||
router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'super-connection-token')
|
router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'super-connection-token')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'),
|
path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'),
|
||||||
path('wecom/qr/unbind/<uuid:user_id>/', api.WeComQRUnBindForAdminApi.as_view(), name='wecom-qr-unbind-for-admin'),
|
path('wecom/qr/unbind/<uuid:user_id>/', api.WeComQRUnBindForAdminApi.as_view(), name='wecom-qr-unbind-for-admin'),
|
||||||
|
|
||||||
path('dingtalk/qr/unbind/', api.DingTalkQRUnBindForUserApi.as_view(), name='dingtalk-qr-unbind'),
|
path('dingtalk/qr/unbind/', api.DingTalkQRUnBindForUserApi.as_view(), name='dingtalk-qr-unbind'),
|
||||||
path('dingtalk/qr/unbind/<uuid:user_id>/', api.DingTalkQRUnBindForAdminApi.as_view(), name='dingtalk-qr-unbind-for-admin'),
|
path('dingtalk/qr/unbind/<uuid:user_id>/', api.DingTalkQRUnBindForAdminApi.as_view(),
|
||||||
|
name='dingtalk-qr-unbind-for-admin'),
|
||||||
|
|
||||||
path('feishu/qr/unbind/', api.FeiShuQRUnBindForUserApi.as_view(), name='feishu-qr-unbind'),
|
path('feishu/qr/unbind/', api.FeiShuQRUnBindForUserApi.as_view(), name='feishu-qr-unbind'),
|
||||||
path('feishu/qr/unbind/<uuid:user_id>/', api.FeiShuQRUnBindForAdminApi.as_view(), name='feishu-qr-unbind-for-admin'),
|
path('feishu/qr/unbind/<uuid:user_id>/', api.FeiShuQRUnBindForAdminApi.as_view(),
|
||||||
path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), name='feishu-event-subscription-callback'),
|
name='feishu-qr-unbind-for-admin'),
|
||||||
|
path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(),
|
||||||
|
name='feishu-event-subscription-callback'),
|
||||||
|
|
||||||
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
||||||
path('confirm/', api.ConfirmApi.as_view(), name='user-confirm'),
|
path('confirm/', api.ConfirmApi.as_view(), name='user-confirm'),
|
||||||
|
@ -38,4 +41,4 @@ urlpatterns = [
|
||||||
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
|
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += router.urls
|
urlpatterns += router.urls + passkey_urlpatterns
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# coding:utf-8
|
# coding:utf-8
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.urls import path, include
|
|
||||||
from django.db.transaction import non_atomic_requests
|
from django.db.transaction import non_atomic_requests
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
from .. import views
|
|
||||||
from users import views as users_view
|
from users import views as users_view
|
||||||
|
from .. import views
|
||||||
|
|
||||||
app_name = 'authentication'
|
app_name = 'authentication'
|
||||||
|
|
||||||
|
@ -18,7 +18,8 @@ urlpatterns = [
|
||||||
path('logout/', views.UserLogoutView.as_view(), name='logout'),
|
path('logout/', views.UserLogoutView.as_view(), name='logout'),
|
||||||
|
|
||||||
# 原来在users中的
|
# 原来在users中的
|
||||||
path('password/forget/previewing/', users_view.UserForgotPasswordPreviewingView.as_view(), name='forgot-previewing'),
|
path('password/forget/previewing/', users_view.UserForgotPasswordPreviewingView.as_view(),
|
||||||
|
name='forgot-previewing'),
|
||||||
path('password/forgot/', users_view.UserForgotPasswordView.as_view(), name='forgot-password'),
|
path('password/forgot/', users_view.UserForgotPasswordView.as_view(), name='forgot-password'),
|
||||||
path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'),
|
path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'),
|
||||||
path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'),
|
path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'),
|
||||||
|
@ -26,7 +27,8 @@ urlpatterns = [
|
||||||
path('wecom/bind/start/', views.WeComEnableStartView.as_view(), name='wecom-bind-start'),
|
path('wecom/bind/start/', views.WeComEnableStartView.as_view(), name='wecom-bind-start'),
|
||||||
path('wecom/qr/bind/', views.WeComQRBindView.as_view(), name='wecom-qr-bind'),
|
path('wecom/qr/bind/', views.WeComQRBindView.as_view(), name='wecom-qr-bind'),
|
||||||
path('wecom/qr/login/', views.WeComQRLoginView.as_view(), name='wecom-qr-login'),
|
path('wecom/qr/login/', views.WeComQRLoginView.as_view(), name='wecom-qr-login'),
|
||||||
path('wecom/qr/bind/<uuid:user_id>/callback/', views.WeComQRBindCallbackView.as_view(), name='wecom-qr-bind-callback'),
|
path('wecom/qr/bind/<uuid:user_id>/callback/', views.WeComQRBindCallbackView.as_view(),
|
||||||
|
name='wecom-qr-bind-callback'),
|
||||||
path('wecom/qr/login/callback/', views.WeComQRLoginCallbackView.as_view(), name='wecom-qr-login-callback'),
|
path('wecom/qr/login/callback/', views.WeComQRLoginCallbackView.as_view(), name='wecom-qr-login-callback'),
|
||||||
path('wecom/oauth/login/', views.WeComOAuthLoginView.as_view(), name='wecom-oauth-login'),
|
path('wecom/oauth/login/', views.WeComOAuthLoginView.as_view(), name='wecom-oauth-login'),
|
||||||
path('wecom/oauth/login/callback/', views.WeComOAuthLoginCallbackView.as_view(), name='wecom-oauth-login-callback'),
|
path('wecom/oauth/login/callback/', views.WeComOAuthLoginCallbackView.as_view(), name='wecom-oauth-login-callback'),
|
||||||
|
@ -34,10 +36,12 @@ urlpatterns = [
|
||||||
path('dingtalk/bind/start/', views.DingTalkEnableStartView.as_view(), name='dingtalk-bind-start'),
|
path('dingtalk/bind/start/', views.DingTalkEnableStartView.as_view(), name='dingtalk-bind-start'),
|
||||||
path('dingtalk/qr/bind/', views.DingTalkQRBindView.as_view(), name='dingtalk-qr-bind'),
|
path('dingtalk/qr/bind/', views.DingTalkQRBindView.as_view(), name='dingtalk-qr-bind'),
|
||||||
path('dingtalk/qr/login/', views.DingTalkQRLoginView.as_view(), name='dingtalk-qr-login'),
|
path('dingtalk/qr/login/', views.DingTalkQRLoginView.as_view(), name='dingtalk-qr-login'),
|
||||||
path('dingtalk/qr/bind/<uuid:user_id>/callback/', views.DingTalkQRBindCallbackView.as_view(), name='dingtalk-qr-bind-callback'),
|
path('dingtalk/qr/bind/<uuid:user_id>/callback/', views.DingTalkQRBindCallbackView.as_view(),
|
||||||
|
name='dingtalk-qr-bind-callback'),
|
||||||
path('dingtalk/qr/login/callback/', views.DingTalkQRLoginCallbackView.as_view(), name='dingtalk-qr-login-callback'),
|
path('dingtalk/qr/login/callback/', views.DingTalkQRLoginCallbackView.as_view(), name='dingtalk-qr-login-callback'),
|
||||||
path('dingtalk/oauth/login/', views.DingTalkOAuthLoginView.as_view(), name='dingtalk-oauth-login'),
|
path('dingtalk/oauth/login/', views.DingTalkOAuthLoginView.as_view(), name='dingtalk-oauth-login'),
|
||||||
path('dingtalk/oauth/login/callback/', views.DingTalkOAuthLoginCallbackView.as_view(), name='dingtalk-oauth-login-callback'),
|
path('dingtalk/oauth/login/callback/', views.DingTalkOAuthLoginCallbackView.as_view(),
|
||||||
|
name='dingtalk-oauth-login-callback'),
|
||||||
|
|
||||||
path('feishu/bind/start/', views.FeiShuEnableStartView.as_view(), name='feishu-bind-start'),
|
path('feishu/bind/start/', views.FeiShuEnableStartView.as_view(), name='feishu-bind-start'),
|
||||||
path('feishu/qr/bind/', views.FeiShuQRBindView.as_view(), name='feishu-qr-bind'),
|
path('feishu/qr/bind/', views.FeiShuQRBindView.as_view(), name='feishu-qr-bind'),
|
||||||
|
|
|
@ -12,6 +12,7 @@ from authentication.mixins import AuthMixin
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from common.utils.django import reverse, get_object_or_none
|
from common.utils.django import reverse, get_object_or_none
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
from users.signal_handlers import check_only_allow_exist_user_auth
|
||||||
from .mixins import FlashMessageMixin
|
from .mixins import FlashMessageMixin
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
@ -49,6 +50,11 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, View):
|
||||||
user, create = User.objects.get_or_create(
|
user, create = User.objects.get_or_create(
|
||||||
username=user_attr['username'], defaults=user_attr
|
username=user_attr['username'], defaults=user_attr
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not check_only_allow_exist_user_auth(create):
|
||||||
|
user.delete()
|
||||||
|
return user, (self.msg_client_err, self.request.error_message)
|
||||||
|
|
||||||
setattr(user, f'{self.user_type}_id', user_id)
|
setattr(user, f'{self.user_type}_id', user_id)
|
||||||
if create:
|
if create:
|
||||||
setattr(user, 'source', self.user_type)
|
setattr(user, 'source', self.user_type)
|
||||||
|
|
|
@ -6,6 +6,7 @@ from __future__ import unicode_literals
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||||
|
@ -40,6 +41,7 @@ __all__ = [
|
||||||
class UserLoginContextMixin:
|
class UserLoginContextMixin:
|
||||||
get_user_mfa_context: Callable
|
get_user_mfa_context: Callable
|
||||||
request: HttpRequest
|
request: HttpRequest
|
||||||
|
error_origin: str
|
||||||
|
|
||||||
def get_support_auth_methods(self):
|
def get_support_auth_methods(self):
|
||||||
auth_methods = [
|
auth_methods = [
|
||||||
|
@ -88,6 +90,12 @@ class UserLoginContextMixin:
|
||||||
'enabled': settings.AUTH_FEISHU,
|
'enabled': settings.AUTH_FEISHU,
|
||||||
'url': reverse('authentication:feishu-qr-login'),
|
'url': reverse('authentication:feishu-qr-login'),
|
||||||
'logo': static('img/login_feishu_logo.png')
|
'logo': static('img/login_feishu_logo.png')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': _("Passkey"),
|
||||||
|
'enabled': settings.AUTH_PASSKEY,
|
||||||
|
'url': reverse('api-auth:passkey-login'),
|
||||||
|
'logo': static('img/login_passkey.png')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
return [method for method in auth_methods if method['enabled']]
|
return [method for method in auth_methods if method['enabled']]
|
||||||
|
@ -134,8 +142,27 @@ class UserLoginContextMixin:
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
def set_csrf_error_if_need(self, context):
|
||||||
|
if not self.request.GET.get('csrf_failure'):
|
||||||
|
return context
|
||||||
|
|
||||||
|
http_origin = self.request.META.get('HTTP_ORIGIN')
|
||||||
|
http_referer = self.request.META.get('HTTP_REFERER')
|
||||||
|
http_origin = http_origin or http_referer
|
||||||
|
|
||||||
|
if not http_origin:
|
||||||
|
return context
|
||||||
|
|
||||||
|
try:
|
||||||
|
origin = urlparse(http_origin)
|
||||||
|
context['error_origin'] = str(origin.netloc)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return context
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
self.set_csrf_error_if_need(context)
|
||||||
context.update({
|
context.update({
|
||||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||||
'auth_methods': self.get_support_auth_methods(),
|
'auth_methods': self.get_support_auth_methods(),
|
||||||
|
@ -283,6 +310,12 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
|
||||||
age = self.request.session.get_expiry_age()
|
age = self.request.session.get_expiry_age()
|
||||||
self.request.session.set_expiry(age)
|
self.request.session.set_expiry(age)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
response = super().get(request, *args, **kwargs)
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
response.set_cookie('jms_username', request.user.username)
|
||||||
|
return response
|
||||||
|
|
||||||
def get_redirect_url(self, *args, **kwargs):
|
def get_redirect_url(self, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
user = self.get_user_from_session()
|
user = self.get_user_from_session()
|
||||||
|
|
|
@ -16,12 +16,19 @@ class METAMixin:
|
||||||
|
|
||||||
class FlashMessageMixin:
|
class FlashMessageMixin:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_response(redirect_url, title, msg, m_type='message'):
|
def get_response(redirect_url='', title='', msg='', m_type='message', interval=5):
|
||||||
message_data = {'title': title, 'interval': 5, 'redirect_url': redirect_url, m_type: msg}
|
message_data = {
|
||||||
|
'title': title, 'interval': interval,
|
||||||
|
'redirect_url': redirect_url,
|
||||||
|
}
|
||||||
|
if m_type == 'error':
|
||||||
|
message_data['error'] = msg
|
||||||
|
else:
|
||||||
|
message_data['message'] = msg
|
||||||
return FlashMessageUtil.gen_and_redirect_to(message_data)
|
return FlashMessageUtil.gen_and_redirect_to(message_data)
|
||||||
|
|
||||||
def get_success_response(self, redirect_url, title, msg):
|
def get_success_response(self, redirect_url, title, msg, **kwargs):
|
||||||
return self.get_response(redirect_url, title, msg)
|
return self.get_response(redirect_url, title, msg, m_type='success', **kwargs)
|
||||||
|
|
||||||
def get_failed_response(self, redirect_url, title, msg):
|
def get_failed_response(self, redirect_url, title, msg, interval=10):
|
||||||
return self.get_response(redirect_url, title, msg, 'error')
|
return self.get_response(redirect_url, title, msg, 'error', interval)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from .action import *
|
from .action import *
|
||||||
from .common import *
|
from .common import *
|
||||||
from .filter import *
|
|
||||||
from .generic import *
|
from .generic import *
|
||||||
from .mixin import *
|
from .mixin import *
|
||||||
from .patch import *
|
from .patch import *
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
import logging
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
from rest_framework.settings import api_settings
|
|
||||||
|
|
||||||
from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter
|
|
||||||
|
|
||||||
__all__ = ['ExtraFilterFieldsMixin', 'OrderingFielderFieldsMixin']
|
|
||||||
|
|
||||||
logger = logging.getLogger('jumpserver.common')
|
|
||||||
|
|
||||||
|
|
||||||
class ExtraFilterFieldsMixin:
|
|
||||||
"""
|
|
||||||
额外的 api filter
|
|
||||||
"""
|
|
||||||
default_added_filters = [CustomFilter, IDSpmFilter, IDInFilter]
|
|
||||||
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
|
|
||||||
extra_filter_fields = []
|
|
||||||
extra_filter_backends = []
|
|
||||||
|
|
||||||
def get_filter_backends(self):
|
|
||||||
if self.filter_backends != self.__class__.filter_backends:
|
|
||||||
return self.filter_backends
|
|
||||||
backends = list(chain(
|
|
||||||
self.filter_backends,
|
|
||||||
self.default_added_filters,
|
|
||||||
self.extra_filter_backends
|
|
||||||
))
|
|
||||||
return backends
|
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
|
||||||
for backend in self.get_filter_backends():
|
|
||||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class OrderingFielderFieldsMixin:
|
|
||||||
"""
|
|
||||||
额外的 api ordering
|
|
||||||
"""
|
|
||||||
ordering_fields = None
|
|
||||||
extra_ordering_fields = []
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.ordering_fields = self._get_ordering_fields()
|
|
||||||
|
|
||||||
def _get_ordering_fields(self):
|
|
||||||
if isinstance(self.__class__.ordering_fields, (list, tuple)):
|
|
||||||
return self.__class__.ordering_fields
|
|
||||||
|
|
||||||
try:
|
|
||||||
valid_fields = self.get_valid_ordering_fields()
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug('get_valid_ordering_fields error: %s' % e)
|
|
||||||
# 这里千万不要这么用,会让 logging 重复,至于为什么,我也不知道
|
|
||||||
# logging.debug('get_valid_ordering_fields error: %s' % e)
|
|
||||||
valid_fields = []
|
|
||||||
|
|
||||||
fields = list(chain(
|
|
||||||
valid_fields,
|
|
||||||
self.extra_ordering_fields
|
|
||||||
))
|
|
||||||
return fields
|
|
||||||
|
|
||||||
def get_valid_ordering_fields(self):
|
|
||||||
if getattr(self, 'model', None):
|
|
||||||
model = self.model
|
|
||||||
elif getattr(self, 'queryset', None):
|
|
||||||
model = self.queryset.model
|
|
||||||
else:
|
|
||||||
queryset = self.get_queryset()
|
|
||||||
model = queryset.model
|
|
||||||
|
|
||||||
if not model:
|
|
||||||
return []
|
|
||||||
|
|
||||||
excludes_fields = (
|
|
||||||
models.UUIDField, models.Model, models.ForeignKey,
|
|
||||||
models.FileField, models.JSONField, models.ManyToManyField,
|
|
||||||
models.DurationField,
|
|
||||||
)
|
|
||||||
valid_fields = []
|
|
||||||
for field in model._meta.fields:
|
|
||||||
if isinstance(field, excludes_fields):
|
|
||||||
continue
|
|
||||||
valid_fields.append(field.name)
|
|
||||||
return valid_fields
|
|
|
@ -1,19 +1,29 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from itertools import chain
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
from django.db.models.signals import m2m_changed
|
from django.db.models.signals import m2m_changed
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
from common.drf.filters import (
|
||||||
|
IDSpmFilterBackend, CustomFilterBackend, IDInFilterBackend,
|
||||||
|
IDNotFilterBackend, NotOrRelFilterBackend
|
||||||
|
)
|
||||||
|
from common.utils import get_logger
|
||||||
from .action import RenderToJsonMixin
|
from .action import RenderToJsonMixin
|
||||||
from .filter import ExtraFilterFieldsMixin, OrderingFielderFieldsMixin
|
|
||||||
from .serializer import SerializerMixin
|
from .serializer import SerializerMixin
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'CommonApiMixin', 'PaginatedResponseMixin', 'RelationMixin',
|
'CommonApiMixin', 'PaginatedResponseMixin', 'RelationMixin',
|
||||||
|
'ExtraFilterFieldsMixin',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PaginatedResponseMixin:
|
class PaginatedResponseMixin:
|
||||||
|
|
||||||
|
@ -95,6 +105,100 @@ class QuerySetMixin:
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin, OrderingFielderFieldsMixin,
|
class ExtraFilterFieldsMixin:
|
||||||
QuerySetMixin, RenderToJsonMixin, PaginatedResponseMixin):
|
"""
|
||||||
|
额外的 api filter
|
||||||
|
"""
|
||||||
|
default_added_filters = (
|
||||||
|
CustomFilterBackend, IDSpmFilterBackend, IDInFilterBackend,
|
||||||
|
IDNotFilterBackend,
|
||||||
|
)
|
||||||
|
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
|
||||||
|
extra_filter_fields = []
|
||||||
|
extra_filter_backends = []
|
||||||
|
|
||||||
|
def set_compatible_fields(self):
|
||||||
|
"""
|
||||||
|
兼容老的 filter_fields
|
||||||
|
"""
|
||||||
|
if not hasattr(self, 'filter_fields') and hasattr(self, 'filterset_fields'):
|
||||||
|
self.filter_fields = self.filterset_fields
|
||||||
|
|
||||||
|
def get_filter_backends(self):
|
||||||
|
self.set_compatible_fields()
|
||||||
|
if self.filter_backends != self.__class__.filter_backends:
|
||||||
|
return self.filter_backends
|
||||||
|
backends = list(chain(
|
||||||
|
self.filter_backends,
|
||||||
|
self.default_added_filters,
|
||||||
|
self.extra_filter_backends,
|
||||||
|
))
|
||||||
|
# 这个要放在最后
|
||||||
|
backends.append(NotOrRelFilterBackend)
|
||||||
|
return backends
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
for backend in self.get_filter_backends():
|
||||||
|
queryset = backend().filter_queryset(self.request, queryset, self)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class OrderingFielderFieldsMixin:
|
||||||
|
"""
|
||||||
|
额外的 api ordering
|
||||||
|
"""
|
||||||
|
ordering_fields = None
|
||||||
|
extra_ordering_fields = []
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.ordering_fields = self._get_ordering_fields()
|
||||||
|
|
||||||
|
def _get_ordering_fields(self):
|
||||||
|
if isinstance(self.__class__.ordering_fields, (list, tuple)):
|
||||||
|
return self.__class__.ordering_fields
|
||||||
|
|
||||||
|
try:
|
||||||
|
valid_fields = self.get_valid_ordering_fields()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug('get_valid_ordering_fields error: %s, pass' % e)
|
||||||
|
# 这里千万不要这么用,会让 logging 重复,至于为什么,我也不知道
|
||||||
|
# logging.debug('get_valid_ordering_fields error: %s' % e)
|
||||||
|
valid_fields = []
|
||||||
|
|
||||||
|
fields = list(chain(
|
||||||
|
valid_fields,
|
||||||
|
self.extra_ordering_fields
|
||||||
|
))
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def get_valid_ordering_fields(self):
|
||||||
|
if getattr(self, 'model', None):
|
||||||
|
model = self.model
|
||||||
|
elif getattr(self, 'queryset', None):
|
||||||
|
model = self.queryset.model
|
||||||
|
else:
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
model = queryset.model
|
||||||
|
|
||||||
|
if not model:
|
||||||
|
return []
|
||||||
|
|
||||||
|
excludes_fields = (
|
||||||
|
models.UUIDField, models.Model, models.ForeignKey,
|
||||||
|
models.FileField, models.JSONField, models.ManyToManyField,
|
||||||
|
models.DurationField,
|
||||||
|
)
|
||||||
|
valid_fields = []
|
||||||
|
for field in model._meta.fields:
|
||||||
|
if isinstance(field, excludes_fields):
|
||||||
|
continue
|
||||||
|
valid_fields.append(field.name)
|
||||||
|
return valid_fields
|
||||||
|
|
||||||
|
|
||||||
|
class CommonApiMixin(
|
||||||
|
SerializerMixin, ExtraFilterFieldsMixin, OrderingFielderFieldsMixin,
|
||||||
|
QuerySetMixin, RenderToJsonMixin, PaginatedResponseMixin
|
||||||
|
):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -88,6 +88,7 @@ class AsyncApiMixin(InterceptMixin):
|
||||||
if not self.is_need_async():
|
if not self.is_need_async():
|
||||||
return handler(*args, **kwargs)
|
return handler(*args, **kwargs)
|
||||||
resp = self.do_async(handler, *args, **kwargs)
|
resp = self.do_async(handler, *args, **kwargs)
|
||||||
|
self.async_callback(*args, **kwargs)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def is_need_refresh(self):
|
def is_need_refresh(self):
|
||||||
|
@ -98,6 +99,9 @@ class AsyncApiMixin(InterceptMixin):
|
||||||
def is_need_async(self):
|
def is_need_async(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def async_callback(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
def do_async(self, handler, *args, **kwargs):
|
def do_async(self, handler, *args, **kwargs):
|
||||||
data = self.get_cache_data()
|
data = self.get_cache_data()
|
||||||
if not data:
|
if not data:
|
||||||
|
|
|
@ -13,17 +13,17 @@ from rest_framework.fields import DateTimeField
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
from common import const
|
from common import const
|
||||||
|
from common.db.fields import RelatedManager
|
||||||
|
|
||||||
logger = logging.getLogger('jumpserver.common')
|
logger = logging.getLogger('jumpserver.common')
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DatetimeRangeFilter", "IDSpmFilter",
|
"DatetimeRangeFilterBackend", "IDSpmFilterBackend",
|
||||||
'IDInFilter', "CustomFilter",
|
'IDInFilterBackend', "CustomFilterBackend",
|
||||||
"BaseFilterSet"
|
"BaseFilterSet", 'IDNotFilterBackend',
|
||||||
|
'NotOrRelFilterBackend',
|
||||||
]
|
]
|
||||||
|
|
||||||
from common.db.fields import RelatedManager
|
|
||||||
|
|
||||||
|
|
||||||
class BaseFilterSet(drf_filters.FilterSet):
|
class BaseFilterSet(drf_filters.FilterSet):
|
||||||
def do_nothing(self, queryset, name, value):
|
def do_nothing(self, queryset, name, value):
|
||||||
|
@ -35,7 +35,7 @@ class BaseFilterSet(drf_filters.FilterSet):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
class DatetimeRangeFilter(filters.BaseFilterBackend):
|
class DatetimeRangeFilterBackend(filters.BaseFilterBackend):
|
||||||
def get_schema_fields(self, view):
|
def get_schema_fields(self, view):
|
||||||
ret = []
|
ret = []
|
||||||
fields = self._get_date_range_filter_fields(view)
|
fields = self._get_date_range_filter_fields(view)
|
||||||
|
@ -102,7 +102,7 @@ class DatetimeRangeFilter(filters.BaseFilterBackend):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class IDSpmFilter(filters.BaseFilterBackend):
|
class IDSpmFilterBackend(filters.BaseFilterBackend):
|
||||||
def get_schema_fields(self, view):
|
def get_schema_fields(self, view):
|
||||||
return [
|
return [
|
||||||
coreapi.Field(
|
coreapi.Field(
|
||||||
|
@ -130,7 +130,7 @@ class IDSpmFilter(filters.BaseFilterBackend):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class IDInFilter(filters.BaseFilterBackend):
|
class IDInFilterBackend(filters.BaseFilterBackend):
|
||||||
def get_schema_fields(self, view):
|
def get_schema_fields(self, view):
|
||||||
return [
|
return [
|
||||||
coreapi.Field(
|
coreapi.Field(
|
||||||
|
@ -149,7 +149,26 @@ class IDInFilter(filters.BaseFilterBackend):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class CustomFilter(filters.BaseFilterBackend):
|
class IDNotFilterBackend(filters.BaseFilterBackend):
|
||||||
|
def get_schema_fields(self, view):
|
||||||
|
return [
|
||||||
|
coreapi.Field(
|
||||||
|
name='id!', location='query', required=False,
|
||||||
|
type='string', example='/api/v1/users/users?id!=1,2,3',
|
||||||
|
description='Exclude by id set'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
ids = request.query_params.get('id!')
|
||||||
|
if not ids:
|
||||||
|
return queryset
|
||||||
|
id_list = [i.strip() for i in ids.split(',')]
|
||||||
|
queryset = queryset.exclude(id__in=id_list)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFilterBackend(filters.BaseFilterBackend):
|
||||||
|
|
||||||
def get_schema_fields(self, view):
|
def get_schema_fields(self, view):
|
||||||
fields = []
|
fields = []
|
||||||
|
@ -218,3 +237,25 @@ class AttrRulesFilterBackend(filters.BaseFilterBackend):
|
||||||
logger.debug('attr_rules: %s', attr_rules)
|
logger.debug('attr_rules: %s', attr_rules)
|
||||||
q = RelatedManager.get_to_filter_q(attr_rules, queryset.model)
|
q = RelatedManager.get_to_filter_q(attr_rules, queryset.model)
|
||||||
return queryset.filter(q).distinct()
|
return queryset.filter(q).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
class NotOrRelFilterBackend(filters.BaseFilterBackend):
|
||||||
|
def get_schema_fields(self, view):
|
||||||
|
return [
|
||||||
|
coreapi.Field(
|
||||||
|
name='_rel', location='query', required=False,
|
||||||
|
type='string', example='/api/v1/users/users?name=abc&username=def&_rel=union',
|
||||||
|
description='Filter by rel, or not, default is and'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
_rel = request.query_params.get('_rel')
|
||||||
|
if not _rel or _rel not in ('or', 'not'):
|
||||||
|
return queryset
|
||||||
|
if _rel == 'not':
|
||||||
|
queryset.query.where.negated = True
|
||||||
|
elif _rel == 'or':
|
||||||
|
queryset.query.where.connector = 'OR'
|
||||||
|
queryset._result_cache = None
|
||||||
|
return queryset
|
||||||
|
|
|
@ -8,7 +8,8 @@ class PassthroughRenderer(renderers.BaseRenderer):
|
||||||
"""
|
"""
|
||||||
Return data as-is. View should supply a Response.
|
Return data as-is. View should supply a Response.
|
||||||
"""
|
"""
|
||||||
media_type = ''
|
media_type = 'application/octet-stream'
|
||||||
format = ''
|
format = ''
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.test import Client
|
||||||
|
from django.urls import URLPattern, URLResolver
|
||||||
|
|
||||||
|
from jumpserver.urls import api_v1
|
||||||
|
|
||||||
|
path_uuid_pattern = re.compile(r'<\w+:\w+>', re.IGNORECASE)
|
||||||
|
uuid_pattern = re.compile(r'\(\(\?P<.*>[^)]+\)/\)\?', re.IGNORECASE)
|
||||||
|
uuid2_pattern = re.compile(r'\(\?P<.*>\[\/\.\]\+\)', re.IGNORECASE)
|
||||||
|
uuid3_pattern = re.compile(r'\(\?P<.*>\[/\.]\+\)')
|
||||||
|
|
||||||
|
|
||||||
|
def list_urls(patterns, path=None):
|
||||||
|
""" recursive """
|
||||||
|
if not path:
|
||||||
|
path = []
|
||||||
|
result = []
|
||||||
|
for pattern in patterns:
|
||||||
|
if isinstance(pattern, URLPattern):
|
||||||
|
result.append(''.join(path) + str(pattern.pattern))
|
||||||
|
elif isinstance(pattern, URLResolver):
|
||||||
|
result += list_urls(pattern.url_patterns, path + [str(pattern.pattern)])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def parse_to_url(url):
|
||||||
|
uid = '00000000-0000-0000-0000-000000000000'
|
||||||
|
|
||||||
|
url = url.replace('^', '')
|
||||||
|
url = url.replace('?$', '')
|
||||||
|
url = url.replace('(?P<format>[a-z0-9]+)', '')
|
||||||
|
url = url.replace('((?P<terminal>[/.]{36})/)?', uid + '/')
|
||||||
|
url = url.replace('(?P<pk>[/.]+)', uid)
|
||||||
|
url = url.replace('\.', '')
|
||||||
|
url = url.replace('//', '/')
|
||||||
|
url = url.strip('$')
|
||||||
|
url = re.sub(path_uuid_pattern, uid, url)
|
||||||
|
url = re.sub(uuid2_pattern, uid, url)
|
||||||
|
url = re.sub(uuid_pattern, uid + '/', url)
|
||||||
|
url = re.sub(uuid3_pattern, uid, url)
|
||||||
|
url = url.replace('(00000000-0000-0000-0000-000000000000/)?', uid + '/')
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_urls():
|
||||||
|
urls = []
|
||||||
|
api_urls = list_urls(api_v1)
|
||||||
|
for ourl in api_urls:
|
||||||
|
url = parse_to_url(ourl)
|
||||||
|
if 'render-to-json' in url:
|
||||||
|
continue
|
||||||
|
url = '/api/v1/' + url
|
||||||
|
urls.append((url, ourl))
|
||||||
|
return set(urls)
|
||||||
|
|
||||||
|
|
||||||
|
known_unauth_urls = [
|
||||||
|
"/api/v1/authentication/passkeys/auth/",
|
||||||
|
"/api/v1/prometheus/metrics/",
|
||||||
|
"/api/v1/authentication/auth/",
|
||||||
|
"/api/v1/settings/logo/",
|
||||||
|
"/api/v1/settings/public/open/",
|
||||||
|
"/api/v1/authentication/passkeys/login/",
|
||||||
|
"/api/v1/authentication/tokens/",
|
||||||
|
"/api/v1/authentication/mfa/challenge/",
|
||||||
|
"/api/v1/authentication/password/reset-code/",
|
||||||
|
"/api/v1/authentication/login-confirm-ticket/status/",
|
||||||
|
"/api/v1/authentication/mfa/select/",
|
||||||
|
"/api/v1/authentication/mfa/send-code/",
|
||||||
|
"/api/v1/authentication/sso/login/"
|
||||||
|
]
|
||||||
|
|
||||||
|
known_error_urls = [
|
||||||
|
'/api/v1/terminal/terminals/00000000-0000-0000-0000-000000000000/sessions/00000000-0000-0000-0000-000000000000/replay/download/',
|
||||||
|
'/api/v1/terminal/sessions/00000000-0000-0000-0000-000000000000/replay/download/',
|
||||||
|
]
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Check api if unauthorized'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
settings.LOG_LEVEL = 'ERROR'
|
||||||
|
urls = get_api_urls()
|
||||||
|
client = Client()
|
||||||
|
unauth_urls = []
|
||||||
|
error_urls = []
|
||||||
|
unformat_urls = []
|
||||||
|
|
||||||
|
for url, ourl in urls:
|
||||||
|
if '(' in url or '<' in url:
|
||||||
|
unformat_urls.append([url, ourl])
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = client.get(url, follow=True)
|
||||||
|
if response.status_code != 401:
|
||||||
|
errors[url] = str(response.status_code) + ' ' + str(ourl)
|
||||||
|
unauth_urls.append(url)
|
||||||
|
except Exception as e:
|
||||||
|
errors[url] = str(e)
|
||||||
|
error_urls.append(url)
|
||||||
|
|
||||||
|
unauth_urls = set(unauth_urls) - set(known_unauth_urls)
|
||||||
|
print("\nUnauthorized urls:")
|
||||||
|
if not unauth_urls:
|
||||||
|
print(" Empty, very good!")
|
||||||
|
for url in unauth_urls:
|
||||||
|
print('"{}", {}'.format(url, errors.get(url, '')))
|
||||||
|
|
||||||
|
print("\nError urls:")
|
||||||
|
if not error_urls:
|
||||||
|
print(" Empty, very good!")
|
||||||
|
for url in set(error_urls):
|
||||||
|
print(url, ': ' + errors.get(url))
|
||||||
|
|
||||||
|
print("\nUnformat urls:")
|
||||||
|
if not unformat_urls:
|
||||||
|
print(" Empty, very good!")
|
||||||
|
for url in unformat_urls:
|
||||||
|
print(url)
|
|
@ -12,7 +12,7 @@ from common.utils import get_object_or_none
|
||||||
from orgs.utils import tmp_to_root_org
|
from orgs.utils import tmp_to_root_org
|
||||||
|
|
||||||
|
|
||||||
class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission):
|
class IsValidUser(permissions.IsAuthenticated):
|
||||||
"""Allows access to valid user, is active and not expired"""
|
"""Allows access to valid user, is active and not expired"""
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import struct
|
|
||||||
import random
|
import random
|
||||||
import socket
|
import socket
|
||||||
import string
|
import string
|
||||||
|
import struct
|
||||||
|
|
||||||
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
|
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
|
||||||
|
|
||||||
|
@ -18,35 +17,32 @@ def random_ip():
|
||||||
return socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff)))
|
return socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff)))
|
||||||
|
|
||||||
|
|
||||||
def random_string(length: int, lower=True, upper=True, digit=True, special_char=False):
|
def random_string(length: int, lower=True, upper=True, digit=True, special_char=False, symbols=string_punctuation):
|
||||||
args_names = ['lower', 'upper', 'digit', 'special_char']
|
random.seed()
|
||||||
args_values = [lower, upper, digit, special_char]
|
args_names = ['lower', 'upper', 'digit']
|
||||||
args_string = [string.ascii_lowercase, string.ascii_uppercase, string.digits, string_punctuation]
|
args_values = [lower, upper, digit]
|
||||||
|
args_string = [string.ascii_lowercase, string.ascii_uppercase, string.digits]
|
||||||
args_string_map = dict(zip(args_names, args_string))
|
args_string_map = dict(zip(args_names, args_string))
|
||||||
kwargs = dict(zip(args_names, args_values))
|
kwargs = dict(zip(args_names, args_values))
|
||||||
kwargs_keys = list(kwargs.keys())
|
kwargs_keys = list(kwargs.keys())
|
||||||
kwargs_values = list(kwargs.values())
|
kwargs_values = list(kwargs.values())
|
||||||
args_true_count = len([i for i in kwargs_values if i])
|
args_true_count = len([i for i in kwargs_values if i])
|
||||||
|
|
||||||
assert any(kwargs_values), f'Parameters {kwargs_keys} must have at least one `True`'
|
assert any(kwargs_values), f'Parameters {kwargs_keys} must have at least one `True`'
|
||||||
assert length >= args_true_count, f'Expected length >= {args_true_count}, bug got {length}'
|
assert length >= args_true_count, f'Expected length >= {args_true_count}, bug got {length}'
|
||||||
|
|
||||||
can_startswith_special_char = args_true_count == 1 and special_char
|
|
||||||
|
|
||||||
chars = ''.join([args_string_map[k] for k, v in kwargs.items() if v])
|
chars = ''.join([args_string_map[k] for k, v in kwargs.items() if v])
|
||||||
|
password = list(random.choice(chars) for i in range(length))
|
||||||
|
|
||||||
while True:
|
if special_char:
|
||||||
password = list(random.choice(chars) for i in range(length))
|
special_num = length // 16 + 1
|
||||||
for k, v in kwargs.items():
|
special_index = []
|
||||||
if v and not (set(password) & set(args_string_map[k])):
|
for i in range(special_num):
|
||||||
# 没有包含指定的字符, retry
|
index = random.randint(1, length - 1)
|
||||||
break
|
if index not in special_index:
|
||||||
else:
|
special_index.append(index)
|
||||||
if not can_startswith_special_char and password[0] in args_string_map['special_char']:
|
for i in special_index:
|
||||||
# 首位不能为特殊字符, retry
|
password[i] = random.choice(symbols)
|
||||||
continue
|
|
||||||
else:
|
|
||||||
# 满足要求终止 while 循环
|
|
||||||
break
|
|
||||||
|
|
||||||
password = ''.join(password)
|
password = ''.join(password)
|
||||||
return password
|
return password
|
||||||
|
|
|
@ -90,4 +90,4 @@ class SendAndVerifyCodeUtil(object):
|
||||||
self.__send_with_email()
|
self.__send_with_email()
|
||||||
|
|
||||||
cache.set(self.key, self.code, self.timeout)
|
cache.set(self.key, self.code, self.timeout)
|
||||||
logger.info(f'Send verify code to {self.target}: {code}')
|
logger.debug(f'Send verify code to {self.target}')
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from django.utils import translation
|
|
||||||
from django.utils.translation import gettext_noop
|
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
from django.http.response import JsonResponse
|
from django.http.response import JsonResponse
|
||||||
|
from django.db.models import Model
|
||||||
|
from django.utils import translation
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
||||||
from common.exceptions import UserConfirmRequired
|
from audits.const import ActivityChoices
|
||||||
from common.utils import i18n_fmt
|
|
||||||
from orgs.utils import current_org
|
|
||||||
from audits.handler import create_or_update_operate_log
|
from audits.handler import create_or_update_operate_log
|
||||||
from audits.const import ActionChoices, ActivityChoices
|
|
||||||
from audits.models import ActivityLog
|
from audits.models import ActivityLog
|
||||||
|
from common.exceptions import UserConfirmRequired
|
||||||
|
from orgs.utils import current_org
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"PermissionsMixin",
|
"PermissionsMixin",
|
||||||
|
@ -49,66 +48,20 @@ class PermissionsMixin(UserPassesTestMixin):
|
||||||
|
|
||||||
|
|
||||||
class RecordViewLogMixin:
|
class RecordViewLogMixin:
|
||||||
ACTION = ActionChoices.view
|
model: Model
|
||||||
|
|
||||||
@staticmethod
|
def record_logs(self, ids, action, detail, model=None, **kwargs):
|
||||||
def _filter_params(params):
|
|
||||||
new_params = {}
|
|
||||||
need_pop_params = ('format', 'order')
|
|
||||||
for key, value in params.items():
|
|
||||||
if key in need_pop_params:
|
|
||||||
continue
|
|
||||||
if isinstance(value, list):
|
|
||||||
value = list(filter(None, value))
|
|
||||||
if value:
|
|
||||||
new_params[key] = value
|
|
||||||
return new_params
|
|
||||||
|
|
||||||
def get_resource_display(self, request):
|
|
||||||
query_params = dict(request.query_params)
|
|
||||||
params = self._filter_params(query_params)
|
|
||||||
|
|
||||||
spm_filter = params.pop("spm", None)
|
|
||||||
|
|
||||||
if not params and not spm_filter:
|
|
||||||
display_message = gettext_noop("Export all")
|
|
||||||
elif spm_filter:
|
|
||||||
display_message = gettext_noop("Export only selected items")
|
|
||||||
else:
|
|
||||||
query = ",".join(
|
|
||||||
["%s=%s" % (key, value) for key, value in params.items()]
|
|
||||||
)
|
|
||||||
display_message = i18n_fmt(gettext_noop("Export filtered: %s"), query)
|
|
||||||
return display_message
|
|
||||||
|
|
||||||
def record_logs(self, ids, **kwargs):
|
|
||||||
resource_type = self.model._meta.verbose_name
|
|
||||||
create_or_update_operate_log(
|
|
||||||
self.ACTION, resource_type, force=True, **kwargs
|
|
||||||
)
|
|
||||||
detail = i18n_fmt(
|
|
||||||
gettext_noop('User %s view/export secret'), self.request.user
|
|
||||||
)
|
|
||||||
activities = [
|
|
||||||
ActivityLog(
|
|
||||||
resource_id=getattr(resource_id, 'pk', resource_id),
|
|
||||||
type=ActivityChoices.operate_log, detail=detail, org_id=current_org.id,
|
|
||||||
)
|
|
||||||
for resource_id in ids
|
|
||||||
]
|
|
||||||
ActivityLog.objects.bulk_create(activities)
|
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
|
||||||
response = super().list(request, *args, **kwargs)
|
|
||||||
with translation.override('en'):
|
with translation.override('en'):
|
||||||
resource_display = self.get_resource_display(request)
|
model = model or self.model
|
||||||
ids = [q.id for q in self.get_queryset()]
|
resource_type = model._meta.verbose_name
|
||||||
self.record_logs(ids, resource_display=resource_display)
|
create_or_update_operate_log(
|
||||||
return response
|
action, resource_type, force=True, **kwargs
|
||||||
|
)
|
||||||
def retrieve(self, request, *args, **kwargs):
|
activities = [
|
||||||
response = super().retrieve(request, *args, **kwargs)
|
ActivityLog(
|
||||||
with translation.override('en'):
|
resource_id=resource_id, type=ActivityChoices.operate_log,
|
||||||
resource = self.get_object()
|
detail=detail, org_id=current_org.id,
|
||||||
self.record_logs([resource.id], resource=resource)
|
)
|
||||||
return response
|
for resource_id in ids
|
||||||
|
]
|
||||||
|
ActivityLog.objects.bulk_create(activities)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#
|
#
|
||||||
|
from django.http import HttpResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.cache import never_cache
|
from django.views.decorators.cache import never_cache
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
from common.utils import bulk_get, FlashMessageUtil
|
from common.utils import bulk_get, FlashMessageUtil
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import time
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Count, Max, F
|
from django.db.models import Count, Max, F, CharField
|
||||||
|
from django.db.models.functions import Cast
|
||||||
from django.http.response import JsonResponse, HttpResponse
|
from django.http.response import JsonResponse, HttpResponse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.timesince import timesince
|
from django.utils.timesince import timesince
|
||||||
|
@ -12,6 +14,7 @@ from rest_framework.views import APIView
|
||||||
|
|
||||||
from assets.const import AllTypes
|
from assets.const import AllTypes
|
||||||
from assets.models import Asset
|
from assets.models import Asset
|
||||||
|
from audits.api import OperateLogViewSet
|
||||||
from audits.const import LoginStatusChoices
|
from audits.const import LoginStatusChoices
|
||||||
from audits.models import UserLoginLog, PasswordChangeLog, OperateLog, FTPLog, JobLog
|
from audits.models import UserLoginLog, PasswordChangeLog, OperateLog, FTPLog, JobLog
|
||||||
from common.utils import lazyproperty
|
from common.utils import lazyproperty
|
||||||
|
@ -52,87 +55,82 @@ class DateTimeMixin:
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def date_start_end(self):
|
def date_start_end(self):
|
||||||
return self.days_to_datetime.date(), local_now().date()
|
return self.days_to_datetime.date(), local_now().date() + timezone.timedelta(days=1)
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def dates_list(self):
|
def dates_list(self):
|
||||||
now = local_now()
|
return [
|
||||||
dates = [(now - timezone.timedelta(days=i)).date() for i in range(self.days)]
|
(local_now() - timezone.timedelta(days=i)).date()
|
||||||
dates.reverse()
|
for i in range(self.days - 1, -1, -1)
|
||||||
return dates
|
]
|
||||||
|
|
||||||
def get_dates_metrics_date(self):
|
def get_dates_metrics_date(self):
|
||||||
dates_metrics_date = [d.strftime('%m-%d') for d in self.dates_list] or ['0']
|
return [d.strftime('%m-%d') for d in self.dates_list] or ['0']
|
||||||
return dates_metrics_date
|
|
||||||
|
|
||||||
@lazyproperty
|
def get_logs_queryset_filter(self, qs, query_field, is_timestamp=False):
|
||||||
def users(self):
|
dt = self.days_to_datetime
|
||||||
return self.org.get_members()
|
t = dt.timestamp() if is_timestamp else dt
|
||||||
|
query = {f'{query_field}__gte': t}
|
||||||
@lazyproperty
|
return qs.filter(**query)
|
||||||
def sessions_queryset(self):
|
|
||||||
t = self.days_to_datetime
|
|
||||||
sessions_queryset = Session.objects.filter(date_start__gte=t)
|
|
||||||
return sessions_queryset
|
|
||||||
|
|
||||||
def get_logs_queryset(self, queryset, query_params):
|
def get_logs_queryset(self, queryset, query_params):
|
||||||
query = {}
|
query = {}
|
||||||
|
users = self.org.get_members()
|
||||||
if not self.org.is_root():
|
if not self.org.is_root():
|
||||||
if query_params == 'username':
|
if query_params == 'username':
|
||||||
query = {
|
query = {
|
||||||
f'{query_params}__in': self.users.values_list('username', flat=True)
|
f'{query_params}__in': users.values_list('username', flat=True)
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
query = {
|
query = {
|
||||||
f'{query_params}__in': [str(user) for user in self.users]
|
f'{query_params}__in': [str(user) for user in users]
|
||||||
}
|
}
|
||||||
queryset = queryset.filter(**query)
|
queryset = queryset.filter(**query)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def sessions_queryset(self):
|
||||||
|
return self.get_logs_queryset_filter(Session.objects, 'date_start')
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def login_logs_queryset(self):
|
def login_logs_queryset(self):
|
||||||
t = self.days_to_datetime
|
qs = UserLoginLog.objects.all()
|
||||||
queryset = UserLoginLog.objects.filter(datetime__gte=t)
|
qs = self.get_logs_queryset_filter(qs, 'datetime')
|
||||||
queryset = self.get_logs_queryset(queryset, 'username')
|
queryset = self.get_logs_queryset(qs, 'username')
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def password_change_logs_queryset(self):
|
def password_change_logs_queryset(self):
|
||||||
t = self.days_to_datetime
|
qs = PasswordChangeLog.objects.all()
|
||||||
queryset = PasswordChangeLog.objects.filter(datetime__gte=t)
|
qs = self.get_logs_queryset_filter(qs, 'datetime')
|
||||||
queryset = self.get_logs_queryset(queryset, 'user')
|
queryset = self.get_logs_queryset(qs, 'user')
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def operate_logs_queryset(self):
|
def operate_logs_queryset(self):
|
||||||
from audits.api import OperateLogViewSet
|
qs = OperateLogViewSet().get_queryset()
|
||||||
t = self.days_to_datetime
|
return self.get_logs_queryset_filter(qs, 'datetime')
|
||||||
queryset = OperateLogViewSet().get_queryset().filter(datetime__gte=t)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def ftp_logs_queryset(self):
|
def ftp_logs_queryset(self):
|
||||||
t = self.days_to_datetime
|
qs = FTPLog.objects.all()
|
||||||
queryset = FTPLog.objects.filter(date_start__gte=t)
|
qs = self.get_logs_queryset_filter(qs, 'date_start')
|
||||||
queryset = self.get_logs_queryset(queryset, 'user')
|
return self.get_logs_queryset(qs, 'user')
|
||||||
return queryset
|
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def command_queryset(self):
|
def command_queryset(self):
|
||||||
t = self.days_to_datetime
|
qs = Command.objects.all()
|
||||||
t = t.timestamp()
|
return self.get_logs_queryset_filter(qs, 'timestamp', is_timestamp=True)
|
||||||
queryset = Command.objects.filter(timestamp__gte=t)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def job_logs_queryset(self):
|
def job_logs_queryset(self):
|
||||||
t = self.days_to_datetime
|
qs = JobLog.objects.all()
|
||||||
queryset = JobLog.objects.filter(date_created__gte=t)
|
return self.get_logs_queryset_filter(qs, 'date_start')
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class DatesLoginMetricMixin:
|
class DatesLoginMetricMixin:
|
||||||
dates_list: list
|
dates_list: list
|
||||||
|
date_start_end: tuple
|
||||||
command_queryset: Command.objects
|
command_queryset: Command.objects
|
||||||
sessions_queryset: Session.objects
|
sessions_queryset: Session.objects
|
||||||
ftp_logs_queryset: OperateLog.objects
|
ftp_logs_queryset: OperateLog.objects
|
||||||
|
@ -141,41 +139,6 @@ class DatesLoginMetricMixin:
|
||||||
operate_logs_queryset: OperateLog.objects
|
operate_logs_queryset: OperateLog.objects
|
||||||
password_change_logs_queryset: PasswordChangeLog.objects
|
password_change_logs_queryset: PasswordChangeLog.objects
|
||||||
|
|
||||||
def get_dates_metrics_total_count_login(self):
|
|
||||||
queryset = UserLoginLog.objects \
|
|
||||||
.filter(datetime__range=(self.date_start_end)) \
|
|
||||||
.values('datetime__date').annotate(id__count=Count(id)) \
|
|
||||||
.order_by('datetime__date')
|
|
||||||
map_date_logincount = {i['datetime__date']: i['id__count'] for i in queryset}
|
|
||||||
return [map_date_logincount.get(d, 0) for d in self.dates_list]
|
|
||||||
|
|
||||||
def get_dates_metrics_total_count_active_users(self):
|
|
||||||
queryset = Session.objects \
|
|
||||||
.filter(date_start__range=(self.date_start_end)) \
|
|
||||||
.values('date_start__date') \
|
|
||||||
.annotate(id__count=Count('user_id', distinct=True)) \
|
|
||||||
.order_by('date_start__date')
|
|
||||||
map_date_usercount = {i['date_start__date']: i['id__count'] for i in queryset}
|
|
||||||
return [map_date_usercount.get(d, 0) for d in self.dates_list]
|
|
||||||
|
|
||||||
def get_dates_metrics_total_count_active_assets(self):
|
|
||||||
queryset = Session.objects \
|
|
||||||
.filter(date_start__range=(self.date_start_end)) \
|
|
||||||
.values('date_start__date') \
|
|
||||||
.annotate(id__count=Count('asset_id', distinct=True)) \
|
|
||||||
.order_by('date_start__date')
|
|
||||||
map_date_assetcount = {i['date_start__date']: i['id__count'] for i in queryset}
|
|
||||||
return [map_date_assetcount.get(d, 0) for d in self.dates_list]
|
|
||||||
|
|
||||||
def get_dates_metrics_total_count_sessions(self):
|
|
||||||
queryset = Session.objects \
|
|
||||||
.filter(date_start__range=(self.date_start_end)) \
|
|
||||||
.values('date_start__date') \
|
|
||||||
.annotate(id__count=Count(id)) \
|
|
||||||
.order_by('date_start__date')
|
|
||||||
map_date_usercount = {i['date_start__date']: i['id__count'] for i in queryset}
|
|
||||||
return [map_date_usercount.get(d, 0) for d in self.dates_list]
|
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def get_type_to_assets(self):
|
def get_type_to_assets(self):
|
||||||
result = Asset.objects.annotate(type=F('platform__type')). \
|
result = Asset.objects.annotate(type=F('platform__type')). \
|
||||||
|
@ -187,30 +150,53 @@ class DatesLoginMetricMixin:
|
||||||
i['label'] = all_types_dict.get(tp, tp)
|
i['label'] = all_types_dict.get(tp, tp)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def filter_date_start_end(self, queryset, field_name):
|
||||||
|
query = {f'{field_name}__range': self.date_start_end}
|
||||||
|
return queryset.filter(**query)
|
||||||
|
|
||||||
|
def get_date_metrics(self, queryset, field_name, count_field):
|
||||||
|
queryset = self.filter_date_start_end(queryset, field_name)
|
||||||
|
queryset = queryset.values_list(field_name, count_field)
|
||||||
|
|
||||||
|
date_group_map = defaultdict(set)
|
||||||
|
for datetime, count_field in queryset:
|
||||||
|
date_str = str(datetime.date())
|
||||||
|
date_group_map[date_str].add(count_field)
|
||||||
|
|
||||||
|
return [
|
||||||
|
len(date_group_map.get(str(d), set()))
|
||||||
|
for d in self.dates_list
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_dates_metrics_total_count_login(self):
|
||||||
|
return self.get_date_metrics(UserLoginLog.objects, 'datetime', 'id')
|
||||||
|
|
||||||
|
def get_dates_metrics_total_count_active_users(self):
|
||||||
|
return self.get_date_metrics(Session.objects, 'date_start', 'user_id')
|
||||||
|
|
||||||
|
def get_dates_metrics_total_count_active_assets(self):
|
||||||
|
return self.get_date_metrics(Session.objects, 'date_start', 'asset_id')
|
||||||
|
|
||||||
|
def get_dates_metrics_total_count_sessions(self):
|
||||||
|
return self.get_date_metrics(Session.objects, 'date_start', 'id')
|
||||||
|
|
||||||
def get_dates_login_times_assets(self):
|
def get_dates_login_times_assets(self):
|
||||||
assets = self.sessions_queryset.values("asset") \
|
assets = self.sessions_queryset.values("asset") \
|
||||||
.annotate(total=Count("asset")) \
|
.annotate(total=Count("asset")) \
|
||||||
.annotate(last=Max("date_start")).order_by("-total")
|
.annotate(last=Cast(Max("date_start"), output_field=CharField())) \
|
||||||
assets = assets[:10]
|
.order_by("-total")
|
||||||
for asset in assets:
|
return list(assets[:10])
|
||||||
asset['last'] = str(asset['last'])
|
|
||||||
return list(assets)
|
|
||||||
|
|
||||||
def get_dates_login_times_users(self):
|
def get_dates_login_times_users(self):
|
||||||
users = self.sessions_queryset.values("user_id") \
|
users = self.sessions_queryset.values("user_id") \
|
||||||
.annotate(total=Count("user_id")) \
|
.annotate(total=Count("user_id")) \
|
||||||
.annotate(user=Max('user')) \
|
.annotate(user=Max('user')) \
|
||||||
.annotate(last=Max("date_start")).order_by("-total")
|
.annotate(last=Cast(Max("date_start"), output_field=CharField())) \
|
||||||
users = users[:10]
|
.order_by("-total")
|
||||||
for user in users:
|
return list(users[:10])
|
||||||
user['last'] = str(user['last'])
|
|
||||||
return list(users)
|
|
||||||
|
|
||||||
def get_dates_login_record_sessions(self):
|
def get_dates_login_record_sessions(self):
|
||||||
sessions = self.sessions_queryset.order_by('-date_start')
|
sessions = self.sessions_queryset.order_by('-date_start')
|
||||||
sessions = sessions[:10]
|
|
||||||
for session in sessions:
|
|
||||||
session.avatar_url = User.get_avatar_url("")
|
|
||||||
sessions = [
|
sessions = [
|
||||||
{
|
{
|
||||||
'user': session.user,
|
'user': session.user,
|
||||||
|
@ -219,7 +205,7 @@ class DatesLoginMetricMixin:
|
||||||
'date_start': str(session.date_start),
|
'date_start': str(session.date_start),
|
||||||
'timesince': timesince(session.date_start)
|
'timesince': timesince(session.date_start)
|
||||||
}
|
}
|
||||||
for session in sessions
|
for session in sessions[:10]
|
||||||
]
|
]
|
||||||
return sessions
|
return sessions
|
||||||
|
|
||||||
|
@ -253,12 +239,13 @@ class DatesLoginMetricMixin:
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def job_logs_running_amount(self):
|
def job_logs_running_amount(self):
|
||||||
return self.job_logs_queryset.filter(status__in=[JobStatus.running]).count()
|
return self.job_logs_queryset.filter(status=JobStatus.running).count()
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def job_logs_failed_amount(self):
|
def job_logs_failed_amount(self):
|
||||||
return self.job_logs_queryset.filter(
|
return self.job_logs_queryset.filter(
|
||||||
status__in=[JobStatus.failed, JobStatus.timeout]).count()
|
status__in=[JobStatus.failed, JobStatus.timeout]
|
||||||
|
).count()
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def job_logs_amount(self):
|
def job_logs_amount(self):
|
||||||
|
@ -268,6 +255,10 @@ class DatesLoginMetricMixin:
|
||||||
def sessions_amount(self):
|
def sessions_amount(self):
|
||||||
return self.sessions_queryset.count()
|
return self.sessions_queryset.count()
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def online_sessions_amount(self):
|
||||||
|
return self.sessions_queryset.filter(is_finished=False).count()
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def ftp_logs_amount(self):
|
def ftp_logs_amount(self):
|
||||||
return self.ftp_logs_queryset.count()
|
return self.ftp_logs_queryset.count()
|
||||||
|
@ -275,9 +266,9 @@ class DatesLoginMetricMixin:
|
||||||
|
|
||||||
class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView):
|
class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView):
|
||||||
http_method_names = ['get']
|
http_method_names = ['get']
|
||||||
|
rbac_perms = {
|
||||||
def check_permissions(self, request):
|
'GET': ['rbac.view_audit | rbac.view_console'],
|
||||||
return request.user.has_perm('rbac.view_audit | rbac.view_console')
|
}
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
data = {}
|
data = {}
|
||||||
|
@ -365,7 +356,7 @@ class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView):
|
||||||
|
|
||||||
if _all or query_params.get('total_count') or query_params.get('total_count_history_sessions'):
|
if _all or query_params.get('total_count') or query_params.get('total_count_history_sessions'):
|
||||||
data.update({
|
data.update({
|
||||||
'total_count_history_sessions': self.sessions_amount - caches.total_count_online_sessions,
|
'total_count_history_sessions': self.sessions_amount - self.online_sessions_amount,
|
||||||
})
|
})
|
||||||
|
|
||||||
if _all or query_params.get('total_count') or query_params.get('total_count_ftp_logs'):
|
if _all or query_params.get('total_count') or query_params.get('total_count_ftp_logs'):
|
||||||
|
|
|
@ -333,9 +333,9 @@ class Config(dict):
|
||||||
'CAS_ROOT_PROXIED_AS': 'https://example.com',
|
'CAS_ROOT_PROXIED_AS': 'https://example.com',
|
||||||
'CAS_LOGOUT_COMPLETELY': True,
|
'CAS_LOGOUT_COMPLETELY': True,
|
||||||
'CAS_VERSION': 3,
|
'CAS_VERSION': 3,
|
||||||
'CAS_USERNAME_ATTRIBUTE': 'uid',
|
'CAS_USERNAME_ATTRIBUTE': 'cas:user',
|
||||||
'CAS_APPLY_ATTRIBUTES_TO_USER': False,
|
'CAS_APPLY_ATTRIBUTES_TO_USER': False,
|
||||||
'CAS_RENAME_ATTRIBUTES': {'uid': 'username'},
|
'CAS_RENAME_ATTRIBUTES': {'cas:user': 'username'},
|
||||||
'CAS_CREATE_USER': True,
|
'CAS_CREATE_USER': True,
|
||||||
|
|
||||||
'AUTH_SSO': False,
|
'AUTH_SSO': False,
|
||||||
|
@ -382,6 +382,9 @@ class Config(dict):
|
||||||
'AUTH_OAUTH2_USER_ATTR_MAP': {
|
'AUTH_OAUTH2_USER_ATTR_MAP': {
|
||||||
'name': 'name', 'username': 'username', 'email': 'email'
|
'name': 'name', 'username': 'username', 'email': 'email'
|
||||||
},
|
},
|
||||||
|
'AUTH_PASSKEY': False,
|
||||||
|
'FIDO_SERVER_ID': '',
|
||||||
|
'FIDO_SERVER_NAME': 'JumpServer',
|
||||||
|
|
||||||
# 企业微信
|
# 企业微信
|
||||||
'AUTH_WECOM': False,
|
'AUTH_WECOM': False,
|
||||||
|
@ -457,9 +460,6 @@ class Config(dict):
|
||||||
'TERMINAL_SESSION_KEEP_DURATION': 200,
|
'TERMINAL_SESSION_KEEP_DURATION': 200,
|
||||||
'TERMINAL_HOST_KEY': '',
|
'TERMINAL_HOST_KEY': '',
|
||||||
'TERMINAL_COMMAND_STORAGE': {},
|
'TERMINAL_COMMAND_STORAGE': {},
|
||||||
# Luna 页面
|
|
||||||
# 默认图形化分辨率
|
|
||||||
'TERMINAL_GRAPHICAL_RESOLUTION': 'Auto',
|
|
||||||
# 未来废弃(目前迁移会用)
|
# 未来废弃(目前迁移会用)
|
||||||
'TERMINAL_RDP_ADDR': '',
|
'TERMINAL_RDP_ADDR': '',
|
||||||
# 保留(Luna还在用)
|
# 保留(Luna还在用)
|
||||||
|
@ -578,7 +578,9 @@ class Config(dict):
|
||||||
'FTP_FILE_MAX_STORE': 100,
|
'FTP_FILE_MAX_STORE': 100,
|
||||||
|
|
||||||
# API 请求次数限制
|
# API 请求次数限制
|
||||||
'MAX_LIMIT_PER_PAGE': 100
|
'MAX_LIMIT_PER_PAGE': 100,
|
||||||
|
|
||||||
|
'LIMIT_SUPER_PRIV': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
old_config_map = {
|
old_config_map = {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue