Merge pull request #7373 from jumpserver/dev

v2.17.0 rc2
pull/7408/head
Jiangjie.Bai 2021-12-13 19:47:33 +08:00 committed by GitHub
commit d6aad41d05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 116 additions and 100 deletions

View File

@ -17,8 +17,7 @@ logger = get_logger(__file__)
class SAML2Backend(ModelBackend): class SAML2Backend(ModelBackend):
@staticmethod def user_can_authenticate(self, user):
def user_can_authenticate(user):
is_valid = getattr(user, 'is_valid', None) is_valid = getattr(user, 'is_valid', None)
return is_valid or is_valid is None return is_valid or is_valid is None
@ -42,9 +41,10 @@ class SAML2Backend(ModelBackend):
log_prompt = "Process authenticate [SAML2AuthCodeBackend]: {}" log_prompt = "Process authenticate [SAML2AuthCodeBackend]: {}"
logger.debug(log_prompt.format('Start')) logger.debug(log_prompt.format('Start'))
if saml_user_data is None: if saml_user_data is None:
logger.debug(log_prompt.format('saml_user_data is missing')) logger.error(log_prompt.format('saml_user_data is missing'))
return None return None
logger.debug(log_prompt.format('saml data, {}'.format(saml_user_data)))
username = saml_user_data.get('username') username = saml_user_data.get('username')
if not username: if not username:
logger.debug(log_prompt.format('username is missing')) logger.debug(log_prompt.format('username is missing'))

View File

@ -1,5 +1,4 @@
import json import copy
import os
from django.views import View from django.views import View
from django.contrib import auth as auth from django.contrib import auth as auth
@ -40,18 +39,20 @@ class PrepareRequestMixin:
idp_metadata_url = settings.SAML2_IDP_METADATA_URL idp_metadata_url = settings.SAML2_IDP_METADATA_URL
logger.debug('Start getting IDP configuration') logger.debug('Start getting IDP configuration')
xml_idp_settings = None
try: try:
xml_idp_settings = IdPMetadataParse.parse(idp_metadata_xml) if idp_metadata_xml.strip():
xml_idp_settings = IdPMetadataParse.parse(idp_metadata_xml)
except Exception as err: except Exception as err:
xml_idp_settings = None
logger.warning('Failed to get IDP metadata XML settings, error: %s', str(err)) logger.warning('Failed to get IDP metadata XML settings, error: %s', str(err))
url_idp_settings = None
try: try:
url_idp_settings = IdPMetadataParse.parse_remote( if idp_metadata_url.strip():
idp_metadata_url, timeout=20 url_idp_settings = IdPMetadataParse.parse_remote(
) idp_metadata_url, timeout=20
)
except Exception as err: except Exception as err:
url_idp_settings = None
logger.warning('Failed to get IDP metadata URL settings, error: %s', str(err)) logger.warning('Failed to get IDP metadata URL settings, error: %s', str(err))
idp_settings = url_idp_settings or xml_idp_settings idp_settings = url_idp_settings or xml_idp_settings
@ -92,14 +93,19 @@ class PrepareRequestMixin:
@staticmethod @staticmethod
def get_advanced_settings(): def get_advanced_settings():
other_settings = {} try:
other_settings_path = settings.SAML2_OTHER_SETTINGS_PATH other_settings = dict(settings.SAML2_SP_ADVANCED_SETTINGS)
if os.path.exists(other_settings_path): other_settings = copy.deepcopy(other_settings)
with open(other_settings_path, 'r') as json_data: except Exception as error:
try: logger.error('Get other settings error: %s', error)
other_settings = json.loads(json_data.read()) other_settings = {}
except Exception as error:
logger.error('Get other settings error: %s', error) security_default = {
'wantAttributeStatement': False,
'allowRepeatAttributeName': True
}
security = other_settings.get('security', {})
security_default.update(security)
default = { default = {
"organization": { "organization": {
@ -108,9 +114,10 @@ class PrepareRequestMixin:
"displayname": "JumpServer", "displayname": "JumpServer",
"url": "https://jumpserver.org/" "url": "https://jumpserver.org/"
} }
} },
} }
default.update(other_settings) default.update(other_settings)
default['security'] = security_default
return default return default
def get_sp_settings(self): def get_sp_settings(self):
@ -157,9 +164,12 @@ class PrepareRequestMixin:
user_attrs = {} user_attrs = {}
real_key_index = len(settings.SITE_URL) + 1 real_key_index = len(settings.SITE_URL) + 1
attrs = saml_instance.get_attributes() attrs = saml_instance.get_attributes()
valid_attrs = ['username', 'name', 'email', 'comment', 'phone']
for attr, value in attrs.items(): for attr, value in attrs.items():
attr = attr[real_key_index:] attr = attr[real_key_index:]
if attr not in valid_attrs:
continue
user_attrs[attr] = self.value_to_str(value) user_attrs[attr] = self.value_to_str(value)
return user_attrs return user_attrs
@ -167,7 +177,7 @@ class PrepareRequestMixin:
class Saml2AuthRequestView(View, PrepareRequestMixin): class Saml2AuthRequestView(View, PrepareRequestMixin):
def get(self, request): def get(self, request):
log_prompt = "Process GET requests [SAML2AuthRequestView]: {}" log_prompt = "Process SAML GET requests: {}"
logger.debug(log_prompt.format('Start')) logger.debug(log_prompt.format('Start'))
try: try:
@ -186,12 +196,12 @@ class Saml2EndSessionView(View, PrepareRequestMixin):
http_method_names = ['get', 'post', ] http_method_names = ['get', 'post', ]
def get(self, request): def get(self, request):
log_prompt = "Process GET requests [SAML2EndSessionView]: {}" log_prompt = "Process SAML GET requests: {}"
logger.debug(log_prompt.format('Start')) logger.debug(log_prompt.format('Start'))
return self.post(request) return self.post(request)
def post(self, request): def post(self, request):
log_prompt = "Process POST requests [SAML2EndSessionView]: {}" log_prompt = "Process SAML POST requests: {}"
logger.debug(log_prompt.format('Start')) logger.debug(log_prompt.format('Start'))
logout_url = settings.LOGOUT_REDIRECT_URL or '/' logout_url = settings.LOGOUT_REDIRECT_URL or '/'
@ -212,7 +222,7 @@ class Saml2EndSessionView(View, PrepareRequestMixin):
class Saml2AuthCallbackView(View, PrepareRequestMixin): class Saml2AuthCallbackView(View, PrepareRequestMixin):
def post(self, request): def post(self, request):
log_prompt = "Process POST requests [SAML2AuthCallbackView]: {}" log_prompt = "Process SAML2 POST requests: {}"
post_data = request.POST post_data = request.POST
try: try:
@ -227,24 +237,25 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin):
logger.debug(log_prompt.format('Process saml response')) logger.debug(log_prompt.format('Process saml response'))
saml_instance.process_response(request_id=request_id) saml_instance.process_response(request_id=request_id)
errors = saml_instance.get_errors() errors = saml_instance.get_last_error_reason()
if not errors: if errors:
if 'AuthNRequestID' in request.session: logger.error(log_prompt.format('Saml response has error: %s' % str(errors)))
del request.session['AuthNRequestID'] return HttpResponseRedirect(settings.AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI)
logger.debug(log_prompt.format('Process authenticate')) if 'AuthNRequestID' in request.session:
saml_user_data = self.get_attributes(saml_instance) del request.session['AuthNRequestID']
user = auth.authenticate(request=request, saml_user_data=saml_user_data)
if user and user.is_valid:
logger.debug(log_prompt.format('Login: {}'.format(user)))
auth.login(self.request, user)
logger.debug(log_prompt.format('Redirect')) logger.debug(log_prompt.format('Process authenticate'))
next_url = saml_instance.redirect_to(post_data.get('RelayState', '/')) saml_user_data = self.get_attributes(saml_instance)
return HttpResponseRedirect(next_url) user = auth.authenticate(request=request, saml_user_data=saml_user_data)
logger.error(log_prompt.format('Saml response has error: %s' % str(errors))) if user and user.is_valid:
return HttpResponseRedirect(settings.AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI) logger.debug(log_prompt.format('Login: {}'.format(user)))
auth.login(self.request, user)
logger.debug(log_prompt.format('Redirect'))
next_url = saml_instance.redirect_to(post_data.get('RelayState', '/'))
return HttpResponseRedirect(next_url)
@csrf_exempt @csrf_exempt
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):

View File

@ -46,57 +46,35 @@ class UserLoginView(mixins.AuthMixin, FormView):
# show jumpserver login page if request http://{JUMP-SERVER}/?admin=1 # show jumpserver login page if request http://{JUMP-SERVER}/?admin=1
if self.request.GET.get("admin", 0): if self.request.GET.get("admin", 0):
return None return None
auth_types = [m for m in self.get_support_auth_methods() if m.get('auto_redirect')]
if not auth_types:
return None
# 明确直接登录哪个
login_to = settings.LOGIN_REDIRECT_TO_BACKEND.upper()
if login_to == 'DIRECT':
return None
auth_method = next(filter(lambda x: x['name'] == login_to, auth_types), None)
if not auth_method:
auth_method = auth_types[0]
auth_name, redirect_url = auth_method['name'], auth_method['url']
next_url = request.GET.get('next') or '/' next_url = request.GET.get('next') or '/'
auth_type = '' query_string = request.GET.urlencode()
if settings.AUTH_OPENID: redirect_url = '{}?next={}&{}'.format(redirect_url, next_url, query_string)
auth_type = 'OIDC'
openid_auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME)
openid_auth_url = openid_auth_url + f'?next={next_url}'
else:
openid_auth_url = None
if settings.AUTH_CAS: if settings.LOGIN_REDIRECT_MSG_ENABLED:
auth_type = 'CAS'
cas_auth_url = reverse(settings.CAS_LOGIN_URL_NAME) + f'?next={next_url}'
else:
cas_auth_url = None
if settings.AUTH_SAML2:
auth_type = 'saml2'
saml2_auth_url = reverse(settings.SAML2_LOGIN_URL_NAME) + f'?next={next_url}'
else:
saml2_auth_url = None
if not any([openid_auth_url, cas_auth_url, saml2_auth_url]):
return None
login_redirect = settings.LOGIN_REDIRECT_TO_BACKEND.lower()
if login_redirect in ['direct']:
return None
if login_redirect in ['cas'] and cas_auth_url:
auth_url = cas_auth_url
elif login_redirect in ['openid', 'oidc'] and openid_auth_url:
auth_url = openid_auth_url
elif login_redirect in ['saml2'] and saml2_auth_url:
auth_url = saml2_auth_url
else:
auth_url = openid_auth_url or cas_auth_url or saml2_auth_url
if settings.LOGIN_REDIRECT_TO_BACKEND or not settings.LOGIN_REDIRECT_MSG_ENABLED:
redirect_url = auth_url
else:
message_data = { message_data = {
'title': _('Redirecting'), 'title': _('Redirecting'),
'message': _("Redirecting to {} authentication").format(auth_type), 'message': _("Redirecting to {} authentication").format(auth_name),
'redirect_url': auth_url, 'redirect_url': redirect_url,
'interval': 3, 'interval': 3,
'has_cancel': True, 'has_cancel': True,
'cancel_url': reverse('authentication:login') + '?admin=1' 'cancel_url': reverse('authentication:login') + '?admin=1'
} }
redirect_url = FlashMessageUtil.gen_message_url(message_data) redirect_url = FlashMessageUtil.gen_message_url(message_data)
query_string = request.GET.urlencode()
redirect_url = "{}&{}".format(redirect_url, query_string)
return redirect_url return redirect_url
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -165,25 +143,28 @@ class UserLoginView(mixins.AuthMixin, FormView):
'name': 'OpenID', 'name': 'OpenID',
'enabled': settings.AUTH_OPENID, 'enabled': settings.AUTH_OPENID,
'url': reverse('authentication:openid:login'), 'url': reverse('authentication:openid:login'),
'logo': static('img/login_oidc_logo.png') 'logo': static('img/login_oidc_logo.png'),
'auto_redirect': True # 是否支持自动重定向
}, },
{ {
'name': 'CAS', 'name': 'CAS',
'enabled': settings.AUTH_CAS, 'enabled': settings.AUTH_CAS,
'url': reverse('authentication:cas:cas-login'), 'url': reverse('authentication:cas:cas-login'),
'logo': static('img/login_cas_logo.png') 'logo': static('img/login_cas_logo.png'),
'auto_redirect': True
}, },
{ {
'name': 'SAML2', 'name': 'SAML2',
'enabled': settings.AUTH_SAML2, 'enabled': settings.AUTH_SAML2,
'url': reverse('authentication:saml2:saml2-login'), 'url': reverse('authentication:saml2:saml2-login'),
'logo': static('img/login_cas_logo.png') 'logo': static('img/login_cas_logo.png'),
'auto_redirect': True
}, },
{ {
'name': _('WeCom'), 'name': _('WeCom'),
'enabled': settings.AUTH_WECOM, 'enabled': settings.AUTH_WECOM,
'url': reverse('authentication:wecom-qr-login'), 'url': reverse('authentication:wecom-qr-login'),
'logo': static('img/login_wecom_logo.png') 'logo': static('img/login_wecom_logo.png'),
}, },
{ {
'name': _('DingTalk'), 'name': _('DingTalk'),

View File

@ -29,7 +29,7 @@ def set_default(data: dict, default: dict):
class DictWrapper: class DictWrapper:
def __init__(self, data:dict): def __init__(self, data: dict):
self.raw_data = data self.raw_data = data
def __getitem__(self, item): def __getitem__(self, item):
@ -51,7 +51,7 @@ class DictWrapper:
return str(self.raw_data) return str(self.raw_data)
def __repr__(self): def __repr__(self):
return str(self.raw_data) return repr(self.raw_data)
def as_request(func): def as_request(func):

View File

@ -124,6 +124,9 @@ class WeCom(RequestMixin):
return users return users
self._requests.check_errcode_is_0(data) self._requests.check_errcode_is_0(data)
if 'invaliduser' not in data:
return ()
invaliduser = data['invaliduser'] invaliduser = data['invaliduser']
if not invaliduser: if not invaliduser:
return () return ()

View File

@ -281,7 +281,6 @@ def get_docker_mem_usage_if_limit():
return ((usage_in_bytes - inactive_file) / limit_in_bytes) * 100 return ((usage_in_bytes - inactive_file) / limit_in_bytes) * 100
except Exception as e: except Exception as e:
logger.debug(f'Get memory usage by docker limit: {e}')
return None return None

View File

@ -234,7 +234,18 @@ class Config(dict):
'SAML2_LOGOUT_COMPLETELY': True, 'SAML2_LOGOUT_COMPLETELY': True,
'AUTH_SAML2_ALWAYS_UPDATE_USER': True, 'AUTH_SAML2_ALWAYS_UPDATE_USER': True,
'SAML2_RENAME_ATTRIBUTES': {'uid': 'username', 'email': 'email'}, 'SAML2_RENAME_ATTRIBUTES': {'uid': 'username', 'email': 'email'},
'SAML2_OTHER_SETTINGS_PATH': '', 'SAML2_SP_ADVANCED_SETTINGS': {
"organization": {
"en": {
"name": "JumpServer",
"displayname": "JumpServer",
"url": "https://jumpserver.org/"
}
},
"strict": True,
"security": {
}
},
'SAML2_IDP_METADATA_URL': '', 'SAML2_IDP_METADATA_URL': '',
'SAML2_IDP_METADATA_XML': '', 'SAML2_IDP_METADATA_XML': '',
'SAML2_SP_KEY_CONTENT': '', 'SAML2_SP_KEY_CONTENT': '',

View File

@ -129,7 +129,7 @@ AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI = CONFIG.AUTH_SAML2_AUTHENTICATIO
AUTH_SAML2_ALWAYS_UPDATE_USER = CONFIG.AUTH_SAML2_ALWAYS_UPDATE_USER AUTH_SAML2_ALWAYS_UPDATE_USER = CONFIG.AUTH_SAML2_ALWAYS_UPDATE_USER
SAML2_LOGOUT_COMPLETELY = CONFIG.SAML2_LOGOUT_COMPLETELY SAML2_LOGOUT_COMPLETELY = CONFIG.SAML2_LOGOUT_COMPLETELY
SAML2_RENAME_ATTRIBUTES = CONFIG.SAML2_RENAME_ATTRIBUTES SAML2_RENAME_ATTRIBUTES = CONFIG.SAML2_RENAME_ATTRIBUTES
SAML2_OTHER_SETTINGS_PATH = CONFIG.SAML2_OTHER_SETTINGS_PATH SAML2_SP_ADVANCED_SETTINGS = CONFIG.SAML2_SP_ADVANCED_SETTINGS
SAML2_LOGIN_URL_NAME = "authentication:saml2:saml2-login" SAML2_LOGIN_URL_NAME = "authentication:saml2:saml2-login"
SAML2_LOGOUT_URL_NAME = "authentication:saml2:saml2-logout" SAML2_LOGOUT_URL_NAME = "authentication:saml2:saml2-logout"

View File

@ -2857,7 +2857,7 @@ msgstr "服务端地址"
#: settings/serializers/auth/cas.py:13 #: settings/serializers/auth/cas.py:13
msgid "Proxy server url" msgid "Proxy server url"
msgstr "代理服务地址" msgstr "回调地址"
#: settings/serializers/auth/cas.py:14 settings/serializers/auth/saml2.py:29 #: settings/serializers/auth/cas.py:14 settings/serializers/auth/saml2.py:29
msgid "Logout completely" msgid "Logout completely"

View File

@ -72,7 +72,7 @@ class OrgResourceStatisticsCache(OrgRelatedCache):
self.org = org self.org = org
def get_key_suffix(self): def get_key_suffix(self):
return f'<org:{self.org.id}>' return f'org_{self.org.id}'
def get_current_org(self): def get_current_org(self):
return self.org return self.org

View File

@ -17,6 +17,9 @@ class SAML2SettingSerializer(serializers.Serializer):
SAML2_IDP_METADATA_XML = serializers.CharField( SAML2_IDP_METADATA_XML = serializers.CharField(
allow_blank=True, required=False, label=_('IDP Metadata XML') allow_blank=True, required=False, label=_('IDP Metadata XML')
) )
SAML2_SP_ADVANCED_SETTINGS = serializers.JSONField(
required=False, label=_('SP ADVANCED SETTINGS')
)
SAML2_SP_KEY_CONTENT = serializers.CharField( SAML2_SP_KEY_CONTENT = serializers.CharField(
allow_blank=True, required=False, allow_blank=True, required=False,
write_only=True, label=_('SP Private Key') write_only=True, label=_('SP Private Key')

View File

@ -121,8 +121,11 @@ class CommandViewSet(JMSBulkModelViewSet):
qs = storage.get_command_queryset() qs = storage.get_command_queryset()
commands = self.filter_queryset(qs) commands = self.filter_queryset(qs)
merged_commands.extend(commands[:]) # ES 默认只取 10 条数据 merged_commands.extend(commands[:]) # ES 默认只取 10 条数据
order = self.request.query_params.get('order', None)
merged_commands.sort(key=lambda command: command.timestamp, reverse=True) if order == 'timestamp':
merged_commands.sort(key=lambda command: command.timestamp)
else:
merged_commands.sort(key=lambda command: command.timestamp, reverse=True)
page = self.paginate_queryset(merged_commands) page = self.paginate_queryset(merged_commands)
if page is not None: if page is not None:
serializer = self.get_serializer(page, many=True) serializer = self.get_serializer(page, many=True)

View File

@ -7,7 +7,7 @@ from common.mixins.models import CommonModelMixin
from common.db.encoder import ModelJSONFieldEncoder from common.db.encoder import ModelJSONFieldEncoder
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from orgs.models import Organization from orgs.models import Organization
from orgs.utils import tmp_to_root_org from orgs.utils import tmp_to_root_org, tmp_to_org
from ..const import TicketType, TicketApprovalLevel, TicketApprovalStrategy from ..const import TicketType, TicketApprovalLevel, TicketApprovalStrategy
from ..signals import post_or_update_change_ticket_flow_approval from ..signals import post_or_update_change_ticket_flow_approval
@ -64,9 +64,13 @@ class TicketFlow(CommonModelMixin, OrgModelMixin):
return '{}'.format(self.type) return '{}'.format(self.type)
@classmethod @classmethod
def get_org_related_flows(cls): def get_org_related_flows(cls, org_id=None):
flows = cls.objects.all() if org_id:
with tmp_to_org(org_id):
flows = cls.objects.all()
else:
flows = cls.objects.all()
cur_flow_types = flows.values_list('type', flat=True) cur_flow_types = flows.values_list('type', flat=True)
with tmp_to_root_org(): with tmp_to_root_org():
diff_global_flows = cls.objects.exclude(type__in=cur_flow_types).filter(org_id=Organization.ROOT_ID) diff_global_flows = cls.objects.exclude(type__in=cur_flow_types)
return flows | diff_global_flows return flows | diff_global_flows

View File

@ -108,7 +108,8 @@ class TicketApplySerializer(TicketSerializer):
def validate(self, attrs): def validate(self, attrs):
ticket_type = attrs.get('type') ticket_type = attrs.get('type')
flow = TicketFlow.get_org_related_flows().filter(type=ticket_type).first() org_id = attrs.get('org_id')
flow = TicketFlow.get_org_related_flows(org_id=org_id).filter(type=ticket_type).first()
if flow: if flow:
attrs['flow'] = flow attrs['flow'] = flow
else: else:

View File

@ -62,7 +62,7 @@ pytz==2018.3
PyYAML==5.4 PyYAML==5.4
redis==3.5.3 redis==3.5.3
requests==2.25.1 requests==2.25.1
jms-storage==0.0.39 jms-storage==0.0.40
s3transfer==0.5.0 s3transfer==0.5.0
simplejson==3.13.2 simplejson==3.13.2
six==1.11.0 six==1.11.0