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

View File

@ -1,5 +1,4 @@
import json
import os
import copy
from django.views import View
from django.contrib import auth as auth
@ -40,18 +39,20 @@ class PrepareRequestMixin:
idp_metadata_url = settings.SAML2_IDP_METADATA_URL
logger.debug('Start getting IDP configuration')
xml_idp_settings = None
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:
xml_idp_settings = None
logger.warning('Failed to get IDP metadata XML settings, error: %s', str(err))
url_idp_settings = None
try:
url_idp_settings = IdPMetadataParse.parse_remote(
idp_metadata_url, timeout=20
)
if idp_metadata_url.strip():
url_idp_settings = IdPMetadataParse.parse_remote(
idp_metadata_url, timeout=20
)
except Exception as err:
url_idp_settings = None
logger.warning('Failed to get IDP metadata URL settings, error: %s', str(err))
idp_settings = url_idp_settings or xml_idp_settings
@ -92,14 +93,19 @@ class PrepareRequestMixin:
@staticmethod
def get_advanced_settings():
other_settings = {}
other_settings_path = settings.SAML2_OTHER_SETTINGS_PATH
if os.path.exists(other_settings_path):
with open(other_settings_path, 'r') as json_data:
try:
other_settings = json.loads(json_data.read())
except Exception as error:
logger.error('Get other settings error: %s', error)
try:
other_settings = dict(settings.SAML2_SP_ADVANCED_SETTINGS)
other_settings = copy.deepcopy(other_settings)
except Exception as error:
logger.error('Get other settings error: %s', error)
other_settings = {}
security_default = {
'wantAttributeStatement': False,
'allowRepeatAttributeName': True
}
security = other_settings.get('security', {})
security_default.update(security)
default = {
"organization": {
@ -108,9 +114,10 @@ class PrepareRequestMixin:
"displayname": "JumpServer",
"url": "https://jumpserver.org/"
}
}
},
}
default.update(other_settings)
default['security'] = security_default
return default
def get_sp_settings(self):
@ -157,9 +164,12 @@ class PrepareRequestMixin:
user_attrs = {}
real_key_index = len(settings.SITE_URL) + 1
attrs = saml_instance.get_attributes()
valid_attrs = ['username', 'name', 'email', 'comment', 'phone']
for attr, value in attrs.items():
attr = attr[real_key_index:]
if attr not in valid_attrs:
continue
user_attrs[attr] = self.value_to_str(value)
return user_attrs
@ -167,7 +177,7 @@ class PrepareRequestMixin:
class Saml2AuthRequestView(View, PrepareRequestMixin):
def get(self, request):
log_prompt = "Process GET requests [SAML2AuthRequestView]: {}"
log_prompt = "Process SAML GET requests: {}"
logger.debug(log_prompt.format('Start'))
try:
@ -186,12 +196,12 @@ class Saml2EndSessionView(View, PrepareRequestMixin):
http_method_names = ['get', 'post', ]
def get(self, request):
log_prompt = "Process GET requests [SAML2EndSessionView]: {}"
log_prompt = "Process SAML GET requests: {}"
logger.debug(log_prompt.format('Start'))
return self.post(request)
def post(self, request):
log_prompt = "Process POST requests [SAML2EndSessionView]: {}"
log_prompt = "Process SAML POST requests: {}"
logger.debug(log_prompt.format('Start'))
logout_url = settings.LOGOUT_REDIRECT_URL or '/'
@ -212,7 +222,7 @@ class Saml2EndSessionView(View, PrepareRequestMixin):
class Saml2AuthCallbackView(View, PrepareRequestMixin):
def post(self, request):
log_prompt = "Process POST requests [SAML2AuthCallbackView]: {}"
log_prompt = "Process SAML2 POST requests: {}"
post_data = request.POST
try:
@ -227,24 +237,25 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin):
logger.debug(log_prompt.format('Process saml response'))
saml_instance.process_response(request_id=request_id)
errors = saml_instance.get_errors()
errors = saml_instance.get_last_error_reason()
if not errors:
if 'AuthNRequestID' in request.session:
del request.session['AuthNRequestID']
if errors:
logger.error(log_prompt.format('Saml response has error: %s' % str(errors)))
return HttpResponseRedirect(settings.AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI)
logger.debug(log_prompt.format('Process authenticate'))
saml_user_data = self.get_attributes(saml_instance)
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)
if 'AuthNRequestID' in request.session:
del request.session['AuthNRequestID']
logger.debug(log_prompt.format('Redirect'))
next_url = saml_instance.redirect_to(post_data.get('RelayState', '/'))
return HttpResponseRedirect(next_url)
logger.error(log_prompt.format('Saml response has error: %s' % str(errors)))
return HttpResponseRedirect(settings.AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI)
logger.debug(log_prompt.format('Process authenticate'))
saml_user_data = self.get_attributes(saml_instance)
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'))
next_url = saml_instance.redirect_to(post_data.get('RelayState', '/'))
return HttpResponseRedirect(next_url)
@csrf_exempt
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
if self.request.GET.get("admin", 0):
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 '/'
auth_type = ''
if settings.AUTH_OPENID:
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
query_string = request.GET.urlencode()
redirect_url = '{}?next={}&{}'.format(redirect_url, next_url, query_string)
if settings.AUTH_CAS:
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:
if settings.LOGIN_REDIRECT_MSG_ENABLED:
message_data = {
'title': _('Redirecting'),
'message': _("Redirecting to {} authentication").format(auth_type),
'redirect_url': auth_url,
'message': _("Redirecting to {} authentication").format(auth_name),
'redirect_url': redirect_url,
'interval': 3,
'has_cancel': True,
'cancel_url': reverse('authentication:login') + '?admin=1'
}
redirect_url = FlashMessageUtil.gen_message_url(message_data)
query_string = request.GET.urlencode()
redirect_url = "{}&{}".format(redirect_url, query_string)
return redirect_url
def get(self, request, *args, **kwargs):
@ -165,25 +143,28 @@ class UserLoginView(mixins.AuthMixin, FormView):
'name': 'OpenID',
'enabled': settings.AUTH_OPENID,
'url': reverse('authentication:openid:login'),
'logo': static('img/login_oidc_logo.png')
'logo': static('img/login_oidc_logo.png'),
'auto_redirect': True # 是否支持自动重定向
},
{
'name': 'CAS',
'enabled': settings.AUTH_CAS,
'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',
'enabled': settings.AUTH_SAML2,
'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'),
'enabled': settings.AUTH_WECOM,
'url': reverse('authentication:wecom-qr-login'),
'logo': static('img/login_wecom_logo.png')
'logo': static('img/login_wecom_logo.png'),
},
{
'name': _('DingTalk'),

View File

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

View File

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

View File

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

View File

@ -234,7 +234,18 @@ class Config(dict):
'SAML2_LOGOUT_COMPLETELY': True,
'AUTH_SAML2_ALWAYS_UPDATE_USER': True,
'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_XML': '',
'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
SAML2_LOGOUT_COMPLETELY = CONFIG.SAML2_LOGOUT_COMPLETELY
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_LOGOUT_URL_NAME = "authentication:saml2:saml2-logout"

View File

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

View File

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

View File

@ -17,6 +17,9 @@ class SAML2SettingSerializer(serializers.Serializer):
SAML2_IDP_METADATA_XML = serializers.CharField(
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(
allow_blank=True, required=False,
write_only=True, label=_('SP Private Key')

View File

@ -121,8 +121,11 @@ class CommandViewSet(JMSBulkModelViewSet):
qs = storage.get_command_queryset()
commands = self.filter_queryset(qs)
merged_commands.extend(commands[:]) # ES 默认只取 10 条数据
merged_commands.sort(key=lambda command: command.timestamp, reverse=True)
order = self.request.query_params.get('order', None)
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)
if page is not None:
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 orgs.mixins.models import OrgModelMixin
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 ..signals import post_or_update_change_ticket_flow_approval
@ -64,9 +64,13 @@ class TicketFlow(CommonModelMixin, OrgModelMixin):
return '{}'.format(self.type)
@classmethod
def get_org_related_flows(cls):
flows = cls.objects.all()
def get_org_related_flows(cls, org_id=None):
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)
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

View File

@ -108,7 +108,8 @@ class TicketApplySerializer(TicketSerializer):
def validate(self, attrs):
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:
attrs['flow'] = flow
else:

View File

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