mirror of https://github.com/jumpserver/jumpserver
commit
d6aad41d05
|
@ -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'))
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 ()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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': '',
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue