mirror of https://github.com/veops/cmdb.git
pref(api): authentication and login log (#308)
* pref(api): authentication and login log * feat(api): ldap and OAuth2.0
This commit is contained in:
parent
ee0b74bec7
commit
6aef26b82c
|
@ -21,7 +21,6 @@ from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager,
|
||||||
from api.extensions import inner_secrets
|
from api.extensions import inner_secrets
|
||||||
from api.lib.perm.authentication.cas import CAS
|
from api.lib.perm.authentication.cas import CAS
|
||||||
from api.lib.perm.authentication.oauth2 import OAuth2
|
from api.lib.perm.authentication.oauth2 import OAuth2
|
||||||
from api.lib.perm.authentication.oidc import OIDC
|
|
||||||
from api.lib.secrets.secrets import InnerKVManger
|
from api.lib.secrets.secrets import InnerKVManger
|
||||||
from api.models.acl import User
|
from api.models.acl import User
|
||||||
|
|
||||||
|
@ -98,7 +97,6 @@ def create_app(config_object="settings"):
|
||||||
register_shell_context(app)
|
register_shell_context(app)
|
||||||
register_commands(app)
|
register_commands(app)
|
||||||
CAS(app)
|
CAS(app)
|
||||||
OIDC(app)
|
|
||||||
OAuth2(app)
|
OAuth2(app)
|
||||||
app.wsgi_app = ReverseProxy(app.wsgi_app)
|
app.wsgi_app = ReverseProxy(app.wsgi_app)
|
||||||
configure_upload_dir(app)
|
configure_upload_dir(app)
|
||||||
|
|
|
@ -94,7 +94,7 @@ class CRUDMixin(FormatMixin):
|
||||||
if any((isinstance(_id, six.string_types) and _id.isdigit(),
|
if any((isinstance(_id, six.string_types) and _id.isdigit(),
|
||||||
isinstance(_id, (six.integer_types, float))), ):
|
isinstance(_id, (six.integer_types, float))), ):
|
||||||
obj = getattr(cls, "query").get(int(_id))
|
obj = getattr(cls, "query").get(int(_id))
|
||||||
if obj and not obj.deleted:
|
if obj and not getattr(obj, 'deleted', False):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
# -*- coding:utf-8 -*-
|
# -*- coding:utf-8 -*-
|
||||||
|
|
||||||
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from flask import has_request_context, request
|
from flask import has_request_context
|
||||||
|
from flask import request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
from api.extensions import db
|
||||||
from api.lib.perm.acl import AppCache
|
from api.lib.perm.acl import AppCache
|
||||||
|
from api.models.acl import AuditLoginLog
|
||||||
from api.models.acl import AuditPermissionLog
|
from api.models.acl import AuditPermissionLog
|
||||||
from api.models.acl import AuditResourceLog
|
from api.models.acl import AuditResourceLog
|
||||||
from api.models.acl import AuditRoleLog
|
from api.models.acl import AuditRoleLog
|
||||||
|
@ -283,6 +288,27 @@ class AuditCRUD(object):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def search_login(_, q=None, page=1, page_size=10, start=None, end=None):
|
||||||
|
query = db.session.query(AuditLoginLog)
|
||||||
|
|
||||||
|
if start:
|
||||||
|
query = query.filter(AuditLoginLog.login_at >= start)
|
||||||
|
if end:
|
||||||
|
query = query.filter(AuditLoginLog.login_at <= end)
|
||||||
|
|
||||||
|
if q:
|
||||||
|
query = query.filter(AuditLoginLog.username == q)
|
||||||
|
|
||||||
|
records = query.order_by(
|
||||||
|
AuditLoginLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'data': [r.to_dict() for r in records],
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_role_log(cls, app_id, operate_type: AuditOperateType,
|
def add_role_log(cls, app_id, operate_type: AuditOperateType,
|
||||||
scope: AuditScope, link_id: int, origin: dict, current: dict, extra: dict,
|
scope: AuditScope, link_id: int, origin: dict, current: dict, extra: dict,
|
||||||
|
@ -348,3 +374,24 @@ class AuditCRUD(object):
|
||||||
AuditTriggerLog.create(app_id=app_id, trigger_id=trigger_id, operate_uid=user_id,
|
AuditTriggerLog.create(app_id=app_id, trigger_id=trigger_id, operate_uid=user_id,
|
||||||
operate_type=operate_type.value,
|
operate_type=operate_type.value,
|
||||||
origin=origin, current=current, extra=extra, source=source.value)
|
origin=origin, current=current, extra=extra, source=source.value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None):
|
||||||
|
if _id is not None:
|
||||||
|
existed = AuditLoginLog.get_by_id(_id)
|
||||||
|
if existed is not None:
|
||||||
|
existed.update(logout_at=logout_at)
|
||||||
|
return
|
||||||
|
|
||||||
|
payload = dict(username=username,
|
||||||
|
is_ok=is_ok,
|
||||||
|
description=description,
|
||||||
|
logout_at=logout_at,
|
||||||
|
ip=request.headers.get('X-Real-IP') or request.remote_addr,
|
||||||
|
browser=request.headers.get('User-Agent'),
|
||||||
|
)
|
||||||
|
|
||||||
|
if logout_at is None:
|
||||||
|
payload['login_at'] = datetime.datetime.now()
|
||||||
|
|
||||||
|
return AuditLoginLog.create(**payload).id
|
||||||
|
|
|
@ -4,6 +4,9 @@ from api.lib.resp_format import CommonErrFormat
|
||||||
|
|
||||||
|
|
||||||
class ErrFormat(CommonErrFormat):
|
class ErrFormat(CommonErrFormat):
|
||||||
|
login_succeed = "登录成功"
|
||||||
|
ldap_connection_failed = "连接LDAP服务失败"
|
||||||
|
invalid_password = "密码验证失败"
|
||||||
auth_only_with_app_token_failed = "应用 Token验证失败"
|
auth_only_with_app_token_failed = "应用 Token验证失败"
|
||||||
session_invalid = "您不是应用管理员 或者 session失效(尝试一下退出重新登录)"
|
session_invalid = "您不是应用管理员 或者 session失效(尝试一下退出重新登录)"
|
||||||
|
|
||||||
|
@ -17,11 +20,11 @@ class ErrFormat(CommonErrFormat):
|
||||||
role_exists = "角色 {} 已经存在!"
|
role_exists = "角色 {} 已经存在!"
|
||||||
global_role_not_found = "全局角色 {} 不存在!"
|
global_role_not_found = "全局角色 {} 不存在!"
|
||||||
global_role_exists = "全局角色 {} 已经存在!"
|
global_role_exists = "全局角色 {} 已经存在!"
|
||||||
user_role_delete_invalid = "删除用户角色, 请在 用户管理 页面操作!"
|
|
||||||
|
|
||||||
resource_no_permission = "您没有资源: {} 的 {} 权限"
|
resource_no_permission = "您没有资源: {} 的 {} 权限"
|
||||||
admin_required = "需要管理员权限"
|
admin_required = "需要管理员权限"
|
||||||
role_required = "需要角色: {}"
|
role_required = "需要角色: {}"
|
||||||
|
user_role_delete_invalid = "删除用户角色, 请在 用户管理 页面操作!"
|
||||||
|
|
||||||
app_is_ready_existed = "应用 {} 已经存在"
|
app_is_ready_existed = "应用 {} 已经存在"
|
||||||
app_not_found = "应用 {} 不存在!"
|
app_not_found = "应用 {} 不存在!"
|
||||||
|
|
|
@ -93,6 +93,9 @@ def _auth_with_token():
|
||||||
|
|
||||||
|
|
||||||
def _auth_with_ip_white_list():
|
def _auth_with_ip_white_list():
|
||||||
|
if request.url.endswith("acl/users/info"):
|
||||||
|
return False
|
||||||
|
|
||||||
ip = request.headers.get('X-Real-IP') or request.remote_addr
|
ip = request.headers.get('X-Real-IP') or request.remote_addr
|
||||||
key = request.values.get('_key')
|
key = request.values.get('_key')
|
||||||
secret = request.values.get('_secret')
|
secret = request.values.get('_secret')
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# -*- coding:utf-8 -*-
|
# -*- coding:utf-8 -*-
|
||||||
|
import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import bs4
|
import bs4
|
||||||
|
@ -12,7 +13,11 @@ from flask_login import login_user
|
||||||
from flask_login import logout_user
|
from flask_login import logout_user
|
||||||
from six.moves.urllib_request import urlopen
|
from six.moves.urllib_request import urlopen
|
||||||
|
|
||||||
|
from api.lib.common_setting.common_data import AuthenticateDataCRUD
|
||||||
|
from api.lib.common_setting.const import AuthenticateType
|
||||||
|
from api.lib.perm.acl.audit import AuditCRUD
|
||||||
from api.lib.perm.acl.cache import UserCache
|
from api.lib.perm.acl.cache import UserCache
|
||||||
|
from api.lib.perm.acl.resp_format import ErrFormat
|
||||||
from .cas_urls import create_cas_login_url
|
from .cas_urls import create_cas_login_url
|
||||||
from .cas_urls import create_cas_logout_url
|
from .cas_urls import create_cas_logout_url
|
||||||
from .cas_urls import create_cas_validate_url
|
from .cas_urls import create_cas_validate_url
|
||||||
|
@ -21,7 +26,7 @@ blueprint = Blueprint('cas', __name__)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/api/cas/login')
|
@blueprint.route('/api/cas/login')
|
||||||
# @blueprint.route('/api/sso/login')
|
@blueprint.route('/api/sso/login')
|
||||||
def login():
|
def login():
|
||||||
"""
|
"""
|
||||||
This route has two purposes. First, it is used by the user
|
This route has two purposes. First, it is used by the user
|
||||||
|
@ -34,6 +39,7 @@ def login():
|
||||||
If validation was successful the logged in username is saved in
|
If validation was successful the logged in username is saved in
|
||||||
the user's session under the key `CAS_USERNAME_SESSION_KEY`.
|
the user's session under the key `CAS_USERNAME_SESSION_KEY`.
|
||||||
"""
|
"""
|
||||||
|
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
|
||||||
|
|
||||||
cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
|
cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
|
||||||
if request.values.get("next"):
|
if request.values.get("next"):
|
||||||
|
@ -41,8 +47,8 @@ def login():
|
||||||
|
|
||||||
_service = url_for('cas.login', _external=True)
|
_service = url_for('cas.login', _external=True)
|
||||||
redirect_url = create_cas_login_url(
|
redirect_url = create_cas_login_url(
|
||||||
current_app.config['CAS_SERVER'],
|
config['cas_server'],
|
||||||
current_app.config['CAS_LOGIN_ROUTE'],
|
config['cas_login_route'],
|
||||||
_service)
|
_service)
|
||||||
|
|
||||||
if 'ticket' in request.args:
|
if 'ticket' in request.args:
|
||||||
|
@ -51,30 +57,38 @@ def login():
|
||||||
if request.args.get('ticket'):
|
if request.args.get('ticket'):
|
||||||
|
|
||||||
if validate(request.args['ticket']):
|
if validate(request.args['ticket']):
|
||||||
redirect_url = session.get("next") or current_app.config.get("CAS_AFTER_LOGIN")
|
redirect_url = session.get("next") or config.get("cas_after_login") or "/"
|
||||||
username = session.get("CAS_USERNAME")
|
username = session.get("CAS_USERNAME")
|
||||||
user = UserCache.get(username)
|
user = UserCache.get(username)
|
||||||
login_user(user)
|
login_user(user)
|
||||||
|
|
||||||
session.permanent = True
|
session.permanent = True
|
||||||
|
|
||||||
|
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
|
||||||
|
session['LOGIN_ID'] = _id
|
||||||
|
|
||||||
else:
|
else:
|
||||||
del session[cas_token_session_key]
|
del session[cas_token_session_key]
|
||||||
redirect_url = create_cas_login_url(
|
redirect_url = create_cas_login_url(
|
||||||
current_app.config['CAS_SERVER'],
|
config['cas_server'],
|
||||||
current_app.config['CAS_LOGIN_ROUTE'],
|
config['cas_login_route'],
|
||||||
url_for('cas.login', _external=True),
|
url_for('cas.login', _external=True),
|
||||||
renew=True)
|
renew=True)
|
||||||
|
|
||||||
|
AuditCRUD.add_login_log(session.get("CAS_USERNAME"), False, ErrFormat.invalid_password)
|
||||||
|
|
||||||
current_app.logger.info("redirect to: {0}".format(redirect_url))
|
current_app.logger.info("redirect to: {0}".format(redirect_url))
|
||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/api/cas/logout')
|
@blueprint.route('/api/cas/logout')
|
||||||
# @blueprint.route('/api/sso/logout')
|
@blueprint.route('/api/sso/logout')
|
||||||
def logout():
|
def logout():
|
||||||
"""
|
"""
|
||||||
When the user accesses this route they are logged out.
|
When the user accesses this route they are logged out.
|
||||||
"""
|
"""
|
||||||
|
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
|
||||||
|
current_app.logger.info(config)
|
||||||
|
|
||||||
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
|
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
|
||||||
cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
|
cas_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
|
||||||
|
@ -86,12 +100,14 @@ def logout():
|
||||||
"next" in session and session.pop("next")
|
"next" in session and session.pop("next")
|
||||||
|
|
||||||
redirect_url = create_cas_logout_url(
|
redirect_url = create_cas_logout_url(
|
||||||
current_app.config['CAS_SERVER'],
|
config['cas_server'],
|
||||||
current_app.config['CAS_LOGOUT_ROUTE'],
|
config['cas_logout_route'],
|
||||||
url_for('cas.login', _external=True, next=request.referrer))
|
url_for('cas.login', _external=True, next=request.referrer))
|
||||||
|
|
||||||
logout_user()
|
logout_user()
|
||||||
|
|
||||||
|
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
|
||||||
|
|
||||||
current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
|
current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
|
||||||
|
|
||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
@ -104,14 +120,15 @@ def validate(ticket):
|
||||||
and the validated username is saved in the session under the
|
and the validated username is saved in the session under the
|
||||||
key `CAS_USERNAME_SESSION_KEY`.
|
key `CAS_USERNAME_SESSION_KEY`.
|
||||||
"""
|
"""
|
||||||
|
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
|
||||||
|
|
||||||
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
|
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
|
||||||
|
|
||||||
current_app.logger.debug("validating token {0}".format(ticket))
|
current_app.logger.debug("validating token {0}".format(ticket))
|
||||||
|
|
||||||
cas_validate_url = create_cas_validate_url(
|
cas_validate_url = create_cas_validate_url(
|
||||||
current_app.config['CAS_VALIDATE_SERVER'],
|
config['cas_validate_server'],
|
||||||
current_app.config['CAS_VALIDATE_ROUTE'],
|
config['cas_validate_route'],
|
||||||
url_for('cas.login', _external=True),
|
url_for('cas.login', _external=True),
|
||||||
ticket)
|
ticket)
|
||||||
|
|
||||||
|
@ -138,14 +155,12 @@ def validate(ticket):
|
||||||
current_app.logger.info("create user: {}".format(username))
|
current_app.logger.info("create user: {}".format(username))
|
||||||
from api.lib.perm.acl.user import UserCRUD
|
from api.lib.perm.acl.user import UserCRUD
|
||||||
soup = bs4.BeautifulSoup(response)
|
soup = bs4.BeautifulSoup(response)
|
||||||
cas_user_map = current_app.config.get('CAS_USER_MAP')
|
cas_user_map = config.get('cas_user_map')
|
||||||
|
|
||||||
user_dict = dict()
|
user_dict = dict()
|
||||||
for k in cas_user_map:
|
for k in cas_user_map:
|
||||||
v = soup.find(cas_user_map[k]['tag'], cas_user_map[k].get('attrs', {}))
|
v = soup.find(cas_user_map[k]['tag'], cas_user_map[k].get('attrs', {}))
|
||||||
user_dict[k] = v and v.text or None
|
user_dict[k] = v and v.text or None
|
||||||
user_dict['password'] = uuid.uuid4().hex
|
user_dict['password'] = uuid.uuid4().hex
|
||||||
|
|
||||||
UserCRUD.add(**user_dict)
|
UserCRUD.add(**user_dict)
|
||||||
|
|
||||||
from api.lib.perm.acl.acl import ACLManager
|
from api.lib.perm.acl.acl import ACLManager
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from flask import abort
|
||||||
|
from flask import current_app
|
||||||
|
from flask import session
|
||||||
|
from ldap3 import ALL
|
||||||
|
from ldap3 import AUTO_BIND_NO_TLS
|
||||||
|
from ldap3 import Connection
|
||||||
|
from ldap3 import Server
|
||||||
|
from ldap3.core.exceptions import LDAPBindError
|
||||||
|
from ldap3.core.exceptions import LDAPCertificateError
|
||||||
|
from ldap3.core.exceptions import LDAPSocketOpenError
|
||||||
|
|
||||||
|
from api.lib.common_setting.common_data import AuthenticateDataCRUD
|
||||||
|
from api.lib.common_setting.const import AuthenticateType
|
||||||
|
from api.lib.perm.acl.audit import AuditCRUD
|
||||||
|
from api.lib.perm.acl.resp_format import ErrFormat
|
||||||
|
from api.models.acl import User
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate_with_ldap(username, password):
|
||||||
|
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
|
||||||
|
|
||||||
|
server = Server(config.get('LDAP').get('ldap_server'), get_info=ALL, connect_timeout=3)
|
||||||
|
if '@' in username:
|
||||||
|
email = username
|
||||||
|
who = config['LDAP'].get('ldap_user_dn').format(username.split('@')[0])
|
||||||
|
else:
|
||||||
|
who = config['LDAP'].get('ldap_user_dn').format(username)
|
||||||
|
email = "{}@{}".format(who, config['LDAP'].get('ldap_domain'))
|
||||||
|
|
||||||
|
username = username.split('@')[0]
|
||||||
|
user = User.query.get_by_username(username)
|
||||||
|
try:
|
||||||
|
if not password:
|
||||||
|
raise LDAPCertificateError
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = Connection(server, user=who, password=password, auto_bind=AUTO_BIND_NO_TLS)
|
||||||
|
except LDAPBindError:
|
||||||
|
conn = Connection(server,
|
||||||
|
user=f"{username}@{config['LDAP'].get('ldap_domain')}",
|
||||||
|
password=password,
|
||||||
|
auto_bind=AUTO_BIND_NO_TLS)
|
||||||
|
|
||||||
|
if conn.result['result'] != 0:
|
||||||
|
AuditCRUD.add_login_log(username, False, ErrFormat.invalid_password)
|
||||||
|
raise LDAPBindError
|
||||||
|
else:
|
||||||
|
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
|
||||||
|
session['LOGIN_ID'] = _id
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
from api.lib.perm.acl.user import UserCRUD
|
||||||
|
user = UserCRUD.add(username=username, email=email, password=uuid.uuid4().hex)
|
||||||
|
|
||||||
|
return user, True
|
||||||
|
|
||||||
|
except LDAPBindError as e:
|
||||||
|
current_app.logger.info(e)
|
||||||
|
return user, False
|
||||||
|
|
||||||
|
except LDAPSocketOpenError as e:
|
||||||
|
current_app.logger.info(e)
|
||||||
|
return abort(403, ErrFormat.ldap_connection_failed)
|
|
@ -18,6 +18,10 @@ class OAuth2(object):
|
||||||
app.config.setdefault('OAUTH2_RESPONSE_TYPE', 'code')
|
app.config.setdefault('OAUTH2_RESPONSE_TYPE', 'code')
|
||||||
app.config.setdefault('OAUTH2_AFTER_LOGIN', '/')
|
app.config.setdefault('OAUTH2_AFTER_LOGIN', '/')
|
||||||
|
|
||||||
|
app.config.setdefault('OIDC_GRANT_TYPE', 'authorization_code')
|
||||||
|
app.config.setdefault('OIDC_RESPONSE_TYPE', 'code')
|
||||||
|
app.config.setdefault('OIDC_AFTER_LOGIN', '/')
|
||||||
|
|
||||||
# Register Blueprint
|
# Register Blueprint
|
||||||
app.register_blueprint(routing.blueprint, url_prefix=url_prefix)
|
app.register_blueprint(routing.blueprint, url_prefix=url_prefix)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding:utf-8 -*-
|
# -*- coding:utf-8 -*-
|
||||||
|
|
||||||
|
import datetime
|
||||||
import secrets
|
import secrets
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
@ -14,69 +15,80 @@ from flask import url_for
|
||||||
from flask_login import login_user, logout_user
|
from flask_login import login_user, logout_user
|
||||||
from six.moves.urllib.parse import urlencode
|
from six.moves.urllib.parse import urlencode
|
||||||
|
|
||||||
|
from api.lib.common_setting.common_data import AuthenticateDataCRUD
|
||||||
|
from api.lib.perm.acl.audit import AuditCRUD
|
||||||
from api.lib.perm.acl.cache import UserCache
|
from api.lib.perm.acl.cache import UserCache
|
||||||
|
from api.lib.perm.acl.resp_format import ErrFormat
|
||||||
|
|
||||||
blueprint = Blueprint('oauth2', __name__)
|
blueprint = Blueprint('oauth2', __name__)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/api/oauth2/login')
|
@blueprint.route('/api/<string:auth_type>/login')
|
||||||
@blueprint.route('/api/sso/login')
|
def login(auth_type):
|
||||||
def login():
|
config = AuthenticateDataCRUD(auth_type.upper()).get()
|
||||||
|
|
||||||
if request.values.get("next"):
|
if request.values.get("next"):
|
||||||
session["next"] = request.values.get("next")
|
session["next"] = request.values.get("next")
|
||||||
|
|
||||||
session['oauth2_state'] = secrets.token_urlsafe(16)
|
session[f'{auth_type}_state'] = secrets.token_urlsafe(16)
|
||||||
|
|
||||||
|
auth_type = auth_type.upper()
|
||||||
|
|
||||||
qs = urlencode({
|
qs = urlencode({
|
||||||
'client_id': current_app.config['OAUTH2_CLIENT_ID'],
|
'client_id': config['client_id'],
|
||||||
'redirect_uri': url_for('oauth2.callback', _external=True),
|
'redirect_uri': url_for('oauth2.callback', auth_type=auth_type.lower(), _external=True),
|
||||||
'response_type': current_app.config['OAUTH2_RESPONSE_TYPE'],
|
'response_type': current_app.config[f'{auth_type}_RESPONSE_TYPE'],
|
||||||
'scope': ' '.join(current_app.config['OAUTH2_SCOPES'] or []),
|
'scope': ' '.join(config['scopes'] or []),
|
||||||
'state': session['oauth2_state'],
|
'state': session[f'{auth_type.lower()}_state'],
|
||||||
})
|
})
|
||||||
|
|
||||||
return redirect("{}?{}".format(current_app.config['OAUTH2_AUTHORIZE_URL'].split('?')[0], qs))
|
return redirect("{}?{}".format(config['authorize_url'].split('?')[0], qs))
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/api/oauth2/callback')
|
@blueprint.route('/api/<string:auth_type>/callback')
|
||||||
def callback():
|
def callback(auth_type):
|
||||||
redirect_url = session.get("next") or current_app.config.get("OAUTH2_AFTER_LOGIN")
|
auth_type = auth_type.upper()
|
||||||
|
config = AuthenticateDataCRUD(auth_type).get()
|
||||||
|
|
||||||
if request.values['state'] != session.get('oauth2_state'):
|
redirect_url = session.get("next") or config.get('after_login') or '/'
|
||||||
|
|
||||||
|
if request.values['state'] != session.get(f'{auth_type.lower()}_state'):
|
||||||
return abort(401, "state is invalid")
|
return abort(401, "state is invalid")
|
||||||
|
|
||||||
if 'code' not in request.values:
|
if 'code' not in request.values:
|
||||||
return abort(401, 'code is invalid')
|
return abort(401, 'code is invalid')
|
||||||
|
|
||||||
response = requests.post(current_app.config['OAUTH2_TOKEN_URL'], data={
|
response = requests.post(config['token_url'], data={
|
||||||
'client_id': current_app.config['OAUTH2_CLIENT_ID'],
|
'client_id': config['client_id'],
|
||||||
'client_secret': current_app.config['OAUTH2_CLIENT_SECRET'],
|
'client_secret': config['client_secret'],
|
||||||
'code': request.values['code'],
|
'code': request.values['code'],
|
||||||
'grant_type': current_app.config['OAUTH2_GRANT_TYPE'],
|
'grant_type': current_app.config[f'{auth_type}_GRANT_TYPE'],
|
||||||
'redirect_uri': url_for('oauth2.callback', _external=True),
|
'redirect_uri': url_for('oauth2.callback', auth_type=auth_type.lower(), _external=True),
|
||||||
}, headers={'Accept': 'application/json'})
|
}, headers={'Accept': 'application/json'})
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
current_app.logger.error(response.text)
|
current_app.logger.error(response.text)
|
||||||
return abort(401)
|
return abort(401)
|
||||||
oauth2_token = response.json().get('access_token')
|
access_token = response.json().get('access_token')
|
||||||
if not oauth2_token:
|
if not access_token:
|
||||||
return abort(401)
|
return abort(401)
|
||||||
|
|
||||||
response = requests.get(current_app.config['OAUTH2_USER_INFO']['url'], headers={
|
response = requests.get(config['user_info']['url'], headers={
|
||||||
'Authorization': 'Bearer {}'.format(oauth2_token),
|
'Authorization': 'Bearer {}'.format(access_token),
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
})
|
})
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return abort(401)
|
return abort(401)
|
||||||
|
|
||||||
email = current_app.config['OAUTH2_USER_INFO']['email'](response.json())
|
res = response.json()
|
||||||
username = current_app.config['OAUTH2_USER_INFO']['username'](response.json())
|
email = res.get(config['user_info']['email'])
|
||||||
|
username = res.get(config['user_info']['username'])
|
||||||
|
avatar = res.get(config['user_info'].get('avatar'))
|
||||||
user = UserCache.get(username)
|
user = UserCache.get(username)
|
||||||
if user is None:
|
if user is None:
|
||||||
current_app.logger.info("create user: {}".format(username))
|
current_app.logger.info("create user: {}".format(username))
|
||||||
from api.lib.perm.acl.user import UserCRUD
|
from api.lib.perm.acl.user import UserCRUD
|
||||||
|
|
||||||
user_dict = dict(username=username, email=email)
|
user_dict = dict(username=username, email=email, avatar=avatar)
|
||||||
user_dict['password'] = uuid.uuid4().hex
|
user_dict['password'] = uuid.uuid4().hex
|
||||||
|
|
||||||
user = UserCRUD.add(**user_dict)
|
user = UserCRUD.add(**user_dict)
|
||||||
|
@ -98,21 +110,25 @@ def callback():
|
||||||
roleName=user_info.get("role"))
|
roleName=user_info.get("role"))
|
||||||
session["uid"] = user_info.get("uid")
|
session["uid"] = user_info.get("uid")
|
||||||
|
|
||||||
|
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
|
||||||
|
session['LOGIN_ID'] = _id
|
||||||
|
|
||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/api/oauth2/logout')
|
@blueprint.route('/api/<string:auth_type>/logout')
|
||||||
@blueprint.route('/api/sso/logout')
|
def logout(auth_type):
|
||||||
def logout():
|
|
||||||
"acl" in session and session.pop("acl")
|
"acl" in session and session.pop("acl")
|
||||||
"uid" in session and session.pop("uid")
|
"uid" in session and session.pop("uid")
|
||||||
'oauth2_state' in session and session.pop('oauth2_state')
|
f'{auth_type}_state' in session and session.pop(f'{auth_type}_state')
|
||||||
"next" in session and session.pop("next")
|
"next" in session and session.pop("next")
|
||||||
|
|
||||||
redirect_url = url_for('oauth2.login', _external=True, next=request.referrer)
|
redirect_url = url_for('oauth2.login', auth_type=auth_type, _external=True, next=request.referrer)
|
||||||
|
|
||||||
logout_user()
|
logout_user()
|
||||||
|
|
||||||
current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
|
current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
|
||||||
|
|
||||||
|
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
|
||||||
|
|
||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
# -*- coding:utf-8 -*-
|
|
||||||
|
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
from . import routing
|
|
||||||
|
|
||||||
|
|
||||||
class OIDC(object):
|
|
||||||
def __init__(self, app=None, url_prefix=None):
|
|
||||||
self._app = app
|
|
||||||
if app is not None:
|
|
||||||
self.init_app(app, url_prefix)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def init_app(app, url_prefix=None):
|
|
||||||
# Configuration defaults
|
|
||||||
app.config.setdefault('OIDC_GRANT_TYPE', 'authorization_code')
|
|
||||||
app.config.setdefault('OIDC_RESPONSE_TYPE', 'code')
|
|
||||||
app.config.setdefault('OIDC_AFTER_LOGIN', '/')
|
|
||||||
# Register Blueprint
|
|
||||||
app.register_blueprint(routing.blueprint, url_prefix=url_prefix)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def app(self):
|
|
||||||
return self._app or current_app
|
|
|
@ -1,118 +0,0 @@
|
||||||
# -*- coding:utf-8 -*-
|
|
||||||
|
|
||||||
import secrets
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from flask import Blueprint
|
|
||||||
from flask import abort
|
|
||||||
from flask import current_app
|
|
||||||
from flask import redirect
|
|
||||||
from flask import request
|
|
||||||
from flask import session
|
|
||||||
from flask import url_for
|
|
||||||
from flask_login import login_user, logout_user
|
|
||||||
from six.moves.urllib.parse import urlencode
|
|
||||||
|
|
||||||
from api.lib.perm.acl.cache import UserCache
|
|
||||||
|
|
||||||
blueprint = Blueprint('oidc', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/api/oidc/login')
|
|
||||||
@blueprint.route('/api/sso/login')
|
|
||||||
def login():
|
|
||||||
if request.values.get("next"):
|
|
||||||
session["next"] = request.values.get("next")
|
|
||||||
|
|
||||||
session['oidc_state'] = secrets.token_urlsafe(16)
|
|
||||||
|
|
||||||
qs = urlencode({
|
|
||||||
'client_id': current_app.config['OIDC_CLIENT_ID'],
|
|
||||||
'redirect_uri': url_for('oidc.callback', _external=True),
|
|
||||||
'response_type': current_app.config['OIDC_RESPONSE_TYPE'],
|
|
||||||
'scope': ' '.join(current_app.config['OIDC_SCOPES'] or []),
|
|
||||||
'state': session['oidc_state'],
|
|
||||||
})
|
|
||||||
|
|
||||||
return redirect("{}?{}".format(current_app.config['OIDC_AUTHORIZE_URL'].split('?')[0], qs))
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/api/oidc/callback')
|
|
||||||
def callback():
|
|
||||||
redirect_url = session.get("next") or current_app.config.get("OIDC_AFTER_LOGIN")
|
|
||||||
|
|
||||||
if request.values['state'] != session.get('oidc_state'):
|
|
||||||
return abort(401, "state is invalid")
|
|
||||||
|
|
||||||
if 'code' not in request.values:
|
|
||||||
return abort(401, 'code is invalid')
|
|
||||||
|
|
||||||
response = requests.post(current_app.config['OIDC_TOKEN_URL'], data={
|
|
||||||
'client_id': current_app.config['OIDC_CLIENT_ID'],
|
|
||||||
'client_secret': current_app.config['OIDC_CLIENT_SECRET'],
|
|
||||||
'code': request.values['code'],
|
|
||||||
'grant_type': current_app.config['OIDC_GRANT_TYPE'],
|
|
||||||
'redirect_uri': url_for('oidc.callback', _external=True),
|
|
||||||
}, headers={'Accept': 'application/json'})
|
|
||||||
if response.status_code != 200:
|
|
||||||
current_app.logger.error(response.text)
|
|
||||||
return abort(401)
|
|
||||||
oidc_token = response.json().get('access_token')
|
|
||||||
if not oidc_token:
|
|
||||||
return abort(401)
|
|
||||||
|
|
||||||
response = requests.get(current_app.config['OIDC_USER_INFO']['url'], headers={
|
|
||||||
'Authorization': 'Bearer {}'.format(oidc_token),
|
|
||||||
'Accept': 'application/json',
|
|
||||||
})
|
|
||||||
if response.status_code != 200:
|
|
||||||
return abort(401)
|
|
||||||
|
|
||||||
email = current_app.config['OIDC_USER_INFO']['email'](response.json())
|
|
||||||
username = current_app.config['OIDC_USER_INFO']['username'](response.json())
|
|
||||||
user = UserCache.get(username)
|
|
||||||
if user is None:
|
|
||||||
current_app.logger.info("create user: {}".format(username))
|
|
||||||
from api.lib.perm.acl.user import UserCRUD
|
|
||||||
|
|
||||||
user_dict = dict(username=username, email=email)
|
|
||||||
user_dict['password'] = uuid.uuid4().hex
|
|
||||||
|
|
||||||
user = UserCRUD.add(**user_dict)
|
|
||||||
|
|
||||||
# log the user in
|
|
||||||
login_user(user)
|
|
||||||
|
|
||||||
from api.lib.perm.acl.acl import ACLManager
|
|
||||||
user_info = ACLManager.get_user_info(username)
|
|
||||||
|
|
||||||
session["acl"] = dict(uid=user_info.get("uid"),
|
|
||||||
avatar=user.avatar if user else user_info.get("avatar"),
|
|
||||||
userId=user_info.get("uid"),
|
|
||||||
rid=user_info.get("rid"),
|
|
||||||
userName=user_info.get("username"),
|
|
||||||
nickName=user_info.get("nickname") or user_info.get("username"),
|
|
||||||
parentRoles=user_info.get("parents"),
|
|
||||||
childRoles=user_info.get("children"),
|
|
||||||
roleName=user_info.get("role"))
|
|
||||||
session["uid"] = user_info.get("uid")
|
|
||||||
|
|
||||||
return redirect(redirect_url)
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/api/oidc/logout')
|
|
||||||
@blueprint.route('/api/sso/logout')
|
|
||||||
def logout():
|
|
||||||
"acl" in session and session.pop("acl")
|
|
||||||
"uid" in session and session.pop("uid")
|
|
||||||
'oidc_state' in session and session.pop('oidc_state')
|
|
||||||
"next" in session and session.pop("next")
|
|
||||||
|
|
||||||
redirect_url = url_for('oidc.login', _external=True, next=request.referrer)
|
|
||||||
|
|
||||||
logout_user()
|
|
||||||
|
|
||||||
current_app.logger.debug('Redirecting to: {0}'.format(redirect_url))
|
|
||||||
|
|
||||||
return redirect(redirect_url)
|
|
|
@ -5,17 +5,18 @@ import copy
|
||||||
import hashlib
|
import hashlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from ldap3 import Server, Connection, ALL
|
|
||||||
from ldap3.core.exceptions import LDAPBindError, LDAPCertificateError
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
from flask import session
|
||||||
from flask_sqlalchemy import BaseQuery
|
from flask_sqlalchemy import BaseQuery
|
||||||
|
|
||||||
from api.extensions import db
|
from api.extensions import db
|
||||||
from api.lib.database import CRUDModel
|
from api.lib.database import CRUDModel
|
||||||
from api.lib.database import Model
|
from api.lib.database import Model
|
||||||
|
from api.lib.database import Model2
|
||||||
from api.lib.database import SoftDeleteMixin
|
from api.lib.database import SoftDeleteMixin
|
||||||
from api.lib.perm.acl.const import ACL_QUEUE
|
from api.lib.perm.acl.const import ACL_QUEUE
|
||||||
from api.lib.perm.acl.const import OperateType
|
from api.lib.perm.acl.const import OperateType
|
||||||
|
from api.lib.perm.acl.resp_format import ErrFormat
|
||||||
|
|
||||||
|
|
||||||
class App(Model):
|
class App(Model):
|
||||||
|
@ -28,21 +29,26 @@ class App(Model):
|
||||||
|
|
||||||
|
|
||||||
class UserQuery(BaseQuery):
|
class UserQuery(BaseQuery):
|
||||||
def _join(self, *args, **kwargs):
|
|
||||||
super(UserQuery, self)._join(*args, **kwargs)
|
|
||||||
|
|
||||||
def authenticate(self, login, password):
|
def authenticate(self, login, password):
|
||||||
|
from api.lib.perm.acl.audit import AuditCRUD
|
||||||
|
|
||||||
user = self.filter(db.or_(User.username == login,
|
user = self.filter(db.or_(User.username == login,
|
||||||
User.email == login)).filter(User.deleted.is_(False)).filter(User.block == 0).first()
|
User.email == login)).filter(User.deleted.is_(False)).filter(User.block == 0).first()
|
||||||
if user:
|
if user:
|
||||||
current_app.logger.info(user)
|
|
||||||
authenticated = user.check_password(password)
|
authenticated = user.check_password(password)
|
||||||
if authenticated:
|
if authenticated:
|
||||||
from api.tasks.acl import op_record
|
_id = AuditCRUD.add_login_log(login, True, ErrFormat.login_succeed)
|
||||||
op_record.apply_async(args=(None, login, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
|
session['LOGIN_ID'] = _id
|
||||||
|
else:
|
||||||
|
AuditCRUD.add_login_log(login, False, ErrFormat.invalid_password)
|
||||||
else:
|
else:
|
||||||
authenticated = False
|
authenticated = False
|
||||||
|
|
||||||
|
AuditCRUD.add_login_log(login, False, ErrFormat.user_not_found.format(login))
|
||||||
|
|
||||||
|
current_app.logger.info(("login", login, user, authenticated))
|
||||||
|
|
||||||
return user, authenticated
|
return user, authenticated
|
||||||
|
|
||||||
def authenticate_with_key(self, key, secret, args, path):
|
def authenticate_with_key(self, key, secret, args, path):
|
||||||
|
@ -57,38 +63,6 @@ class UserQuery(BaseQuery):
|
||||||
|
|
||||||
return user, authenticated
|
return user, authenticated
|
||||||
|
|
||||||
def authenticate_with_ldap(self, username, password):
|
|
||||||
server = Server(current_app.config.get('LDAP_SERVER'), get_info=ALL)
|
|
||||||
if '@' in username:
|
|
||||||
email = username
|
|
||||||
who = current_app.config.get('LDAP_USER_DN').format(username.split('@')[0])
|
|
||||||
else:
|
|
||||||
who = current_app.config.get('LDAP_USER_DN').format(username)
|
|
||||||
email = "{}@{}".format(who, current_app.config.get('LDAP_DOMAIN'))
|
|
||||||
|
|
||||||
username = username.split('@')[0]
|
|
||||||
user = self.get_by_username(username)
|
|
||||||
try:
|
|
||||||
if not password:
|
|
||||||
raise LDAPCertificateError
|
|
||||||
|
|
||||||
conn = Connection(server, user=who, password=password)
|
|
||||||
conn.bind()
|
|
||||||
if conn.result['result'] != 0:
|
|
||||||
raise LDAPBindError
|
|
||||||
conn.unbind()
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
from api.lib.perm.acl.user import UserCRUD
|
|
||||||
user = UserCRUD.add(username=username, email=email)
|
|
||||||
|
|
||||||
from api.tasks.acl import op_record
|
|
||||||
op_record.apply_async(args=(None, username, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
|
|
||||||
|
|
||||||
return user, True
|
|
||||||
except LDAPBindError:
|
|
||||||
return user, False
|
|
||||||
|
|
||||||
def search(self, key):
|
def search(self, key):
|
||||||
query = self.filter(db.or_(User.email == key,
|
query = self.filter(db.or_(User.email == key,
|
||||||
User.nickname.ilike('%' + key + '%'),
|
User.nickname.ilike('%' + key + '%'),
|
||||||
|
@ -138,6 +112,7 @@ class User(CRUDModel, SoftDeleteMixin):
|
||||||
wx_id = db.Column(db.String(32))
|
wx_id = db.Column(db.String(32))
|
||||||
employee_id = db.Column(db.String(16), index=True)
|
employee_id = db.Column(db.String(16), index=True)
|
||||||
avatar = db.Column(db.String(128))
|
avatar = db.Column(db.String(128))
|
||||||
|
|
||||||
# apps = db.Column(db.JSON)
|
# apps = db.Column(db.JSON)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -168,8 +143,6 @@ class User(CRUDModel, SoftDeleteMixin):
|
||||||
|
|
||||||
|
|
||||||
class RoleQuery(BaseQuery):
|
class RoleQuery(BaseQuery):
|
||||||
def _join(self, *args, **kwargs):
|
|
||||||
super(RoleQuery, self)._join(*args, **kwargs)
|
|
||||||
|
|
||||||
def authenticate(self, login, password):
|
def authenticate(self, login, password):
|
||||||
role = self.filter(Role.name == login).first()
|
role = self.filter(Role.name == login).first()
|
||||||
|
@ -377,3 +350,16 @@ class AuditTriggerLog(Model):
|
||||||
current = db.Column(db.JSON, default=dict(), comment='当前数据')
|
current = db.Column(db.JSON, default=dict(), comment='当前数据')
|
||||||
extra = db.Column(db.JSON, default=dict(), comment='权限名')
|
extra = db.Column(db.JSON, default=dict(), comment='权限名')
|
||||||
source = db.Column(db.String(16), default='', comment='来源')
|
source = db.Column(db.String(16), default='', comment='来源')
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLoginLog(Model2):
|
||||||
|
__tablename__ = "acl_audit_login_logs"
|
||||||
|
|
||||||
|
username = db.Column(db.String(64), index=True)
|
||||||
|
channel = db.Column(db.Enum('web', 'api'), default="web")
|
||||||
|
ip = db.Column(db.String(15))
|
||||||
|
browser = db.Column(db.String(256))
|
||||||
|
description = db.Column(db.String(128))
|
||||||
|
is_ok = db.Column(db.Boolean)
|
||||||
|
login_at = db.Column(db.DateTime)
|
||||||
|
logout_at = db.Column(db.DateTime)
|
||||||
|
|
|
@ -24,6 +24,7 @@ class AuditLogView(APIView):
|
||||||
'role': AuditCRUD.search_role,
|
'role': AuditCRUD.search_role,
|
||||||
'trigger': AuditCRUD.search_trigger,
|
'trigger': AuditCRUD.search_trigger,
|
||||||
'resource': AuditCRUD.search_resource,
|
'resource': AuditCRUD.search_resource,
|
||||||
|
'login': AuditCRUD.search_login,
|
||||||
}
|
}
|
||||||
if name not in func_map:
|
if name not in func_map:
|
||||||
abort(400, f'wrong {name}, please use {func_map.keys()}')
|
abort(400, f'wrong {name}, please use {func_map.keys()}')
|
||||||
|
|
|
@ -8,11 +8,15 @@ from flask import abort
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask import session
|
from flask import session
|
||||||
from flask_login import login_user, logout_user
|
from flask_login import login_user
|
||||||
|
from flask_login import logout_user
|
||||||
|
|
||||||
|
from api.lib.common_setting.common_data import AuthenticateDataCRUD
|
||||||
|
from api.lib.common_setting.const import AuthenticateType
|
||||||
from api.lib.decorator import args_required
|
from api.lib.decorator import args_required
|
||||||
from api.lib.decorator import args_validate
|
from api.lib.decorator import args_validate
|
||||||
from api.lib.perm.acl.acl import ACLManager
|
from api.lib.perm.acl.acl import ACLManager
|
||||||
|
from api.lib.perm.acl.audit import AuditCRUD
|
||||||
from api.lib.perm.acl.cache import RoleCache
|
from api.lib.perm.acl.cache import RoleCache
|
||||||
from api.lib.perm.acl.cache import User
|
from api.lib.perm.acl.cache import User
|
||||||
from api.lib.perm.acl.cache import UserCache
|
from api.lib.perm.acl.cache import UserCache
|
||||||
|
@ -34,8 +38,10 @@ class LoginView(APIView):
|
||||||
username = request.values.get("username") or request.values.get("email")
|
username = request.values.get("username") or request.values.get("email")
|
||||||
password = request.values.get("password")
|
password = request.values.get("password")
|
||||||
_role = None
|
_role = None
|
||||||
if current_app.config.get('AUTH_WITH_LDAP'):
|
config = AuthenticateDataCRUD(AuthenticateType.LDAP).get()
|
||||||
user, authenticated = User.query.authenticate_with_ldap(username, password)
|
if config.get('LDAP', {}).get('enabled'):
|
||||||
|
from api.lib.perm.authentication.ldap import authenticate_with_ldap
|
||||||
|
user, authenticated = authenticate_with_ldap(username, password)
|
||||||
else:
|
else:
|
||||||
user, authenticated = User.query.authenticate(username, password)
|
user, authenticated = User.query.authenticate(username, password)
|
||||||
if not user:
|
if not user:
|
||||||
|
@ -176,4 +182,7 @@ class LogoutView(APIView):
|
||||||
@auth_abandoned
|
@auth_abandoned
|
||||||
def post(self):
|
def post(self):
|
||||||
logout_user()
|
logout_user()
|
||||||
|
|
||||||
|
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
|
||||||
|
|
||||||
self.jsonify(code=200)
|
self.jsonify(code=200)
|
||||||
|
|
|
@ -11,10 +11,10 @@ from environs import Env
|
||||||
env = Env()
|
env = Env()
|
||||||
env.read_env()
|
env.read_env()
|
||||||
|
|
||||||
ENV = env.str("FLASK_ENV", default="production")
|
ENV = env.str('FLASK_ENV', default='production')
|
||||||
DEBUG = ENV == "development"
|
DEBUG = ENV == 'development'
|
||||||
SECRET_KEY = env.str("SECRET_KEY")
|
SECRET_KEY = env.str('SECRET_KEY')
|
||||||
BCRYPT_LOG_ROUNDS = env.int("BCRYPT_LOG_ROUNDS", default=13)
|
BCRYPT_LOG_ROUNDS = env.int('BCRYPT_LOG_ROUNDS', default=13)
|
||||||
DEBUG_TB_ENABLED = DEBUG
|
DEBUG_TB_ENABLED = DEBUG
|
||||||
DEBUG_TB_INTERCEPT_REDIRECTS = False
|
DEBUG_TB_INTERCEPT_REDIRECTS = False
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ ERROR_CODES = [400, 401, 403, 404, 405, 500, 502]
|
||||||
# # database
|
# # database
|
||||||
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
|
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
|
||||||
SQLALCHEMY_BINDS = {
|
SQLALCHEMY_BINDS = {
|
||||||
"user": 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
|
'user': 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
|
||||||
}
|
}
|
||||||
SQLALCHEMY_ECHO = False
|
SQLALCHEMY_ECHO = False
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
@ -32,11 +32,11 @@ SQLALCHEMY_ENGINE_OPTIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
# # cache
|
# # cache
|
||||||
CACHE_TYPE = "redis"
|
CACHE_TYPE = 'redis'
|
||||||
CACHE_REDIS_HOST = "127.0.0.1"
|
CACHE_REDIS_HOST = '127.0.0.1'
|
||||||
CACHE_REDIS_PORT = 6379
|
CACHE_REDIS_PORT = 6379
|
||||||
CACHE_REDIS_PASSWORD = ""
|
CACHE_REDIS_PASSWORD = ''
|
||||||
CACHE_KEY_PREFIX = "CMDB::"
|
CACHE_KEY_PREFIX = 'CMDB::'
|
||||||
CACHE_DEFAULT_TIMEOUT = 3000
|
CACHE_DEFAULT_TIMEOUT = 3000
|
||||||
|
|
||||||
# # log
|
# # log
|
||||||
|
@ -55,10 +55,10 @@ DEFAULT_MAIL_SENDER = ''
|
||||||
|
|
||||||
# # queue
|
# # queue
|
||||||
CELERY = {
|
CELERY = {
|
||||||
"broker_url": 'redis://127.0.0.1:6379/2',
|
'broker_url': 'redis://127.0.0.1:6379/2',
|
||||||
"result_backend": "redis://127.0.0.1:6379/2",
|
'result_backend': 'redis://127.0.0.1:6379/2',
|
||||||
"broker_vhost": "/",
|
'broker_vhost': '/',
|
||||||
"broker_connection_retry_on_startup": True
|
'broker_connection_retry_on_startup': True
|
||||||
}
|
}
|
||||||
ONCE = {
|
ONCE = {
|
||||||
'backend': 'celery_once.backends.Redis',
|
'backend': 'celery_once.backends.Redis',
|
||||||
|
@ -70,68 +70,78 @@ ONCE = {
|
||||||
# =============================== Authentication ===========================================================
|
# =============================== Authentication ===========================================================
|
||||||
|
|
||||||
# # CAS
|
# # CAS
|
||||||
AUTH_WITH_CAS = False
|
CAS = dict(
|
||||||
CAS_SERVER = "https://{your-casdoor-hostname}"
|
enabled=False,
|
||||||
CAS_VALIDATE_SERVER = "https://{your-casdoor-hostname}"
|
cas_server='https://{your-CASServer-hostname}',
|
||||||
CAS_LOGIN_ROUTE = "/cas/built-in/cas/login"
|
cas_validate_server='https://{your-CASServer-hostname}',
|
||||||
CAS_LOGOUT_ROUTE = "/cas/built-in/cas/logout"
|
cas_login_route='/cas/built-in/cas/login',
|
||||||
CAS_VALIDATE_ROUTE = "/cas/built-in/cas/serviceValidate"
|
cas_logout_route='/cas/built-in/cas/logout',
|
||||||
CAS_AFTER_LOGIN = "/"
|
cas_validate_route='/cas/built-in/cas/serviceValidate',
|
||||||
CAS_USER_MAP = {
|
cas_after_login='/',
|
||||||
"username": {"tag": "cas:user"},
|
cas_user_map={
|
||||||
"nickname": {"tag": "cas:attribute", "attrs": {"name": "displayName"}},
|
'username': {'tag': 'cas:user'},
|
||||||
"email": {"tag": "cas:attribute", "attrs": {"name": "email"}},
|
'nickname': {'tag': 'cas:attribute', 'attrs': {'name': 'displayName'}},
|
||||||
"mobile": {"tag": "cas:attribute", "attrs": {"name": "phone"}},
|
'email': {'tag': 'cas:attribute', 'attrs': {'name': 'email'}},
|
||||||
"avatar": {"tag": "cas:attribute", "attrs": {"name": "avatar"}},
|
'mobile': {'tag': 'cas:attribute', 'attrs': {'name': 'phone'}},
|
||||||
}
|
'avatar': {'tag': 'cas:attribute', 'attrs': {'name': 'avatar'}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# # OAuth2.0
|
# # OAuth2.0
|
||||||
AUTH_WITH_OAUTH2 = False
|
OAUTH2 = dict(
|
||||||
OAUTH2_CLIENT_ID = ""
|
enabled=False,
|
||||||
OAUTH2_CLIENT_SECRET = ""
|
client_id='',
|
||||||
OAUTH2_AUTHORIZE_URL = "https://{your-casdoor-hostname}/login/oauth/authorize"
|
client_secret='',
|
||||||
OAUTH2_TOKEN_URL = "https://{your-casdoor-hostname}/api/login/oauth/access_token"
|
authorize_url='https://{your-OAuth2Server-hostname}/login/oauth/authorize',
|
||||||
OAUTH2_USER_INFO = {
|
token_url='https://{your-OAuth2Server-hostname}/api/login/oauth/access_token',
|
||||||
"url": "https://{your-casdoor-hostname}/api/userinfo",
|
scopes=['profile', 'email'],
|
||||||
"email": lambda x: x['email'],
|
user_info={
|
||||||
"username": lambda x: x['name']
|
'url': 'https://{your-OAuth2Server-hostname}/api/userinfo',
|
||||||
}
|
'email': 'email',
|
||||||
OAUTH2_SCOPES = ["profile email"]
|
'username': 'name',
|
||||||
OAUTH2_AFTER_LOGIN = "/"
|
'avatar': 'picture'
|
||||||
|
},
|
||||||
|
after_login='/'
|
||||||
|
)
|
||||||
|
|
||||||
# # OIDC
|
# # OIDC
|
||||||
AUTH_WITH_OIDC = False
|
OIDC = dict(
|
||||||
OIDC_CLIENT_ID = ""
|
enabled=False,
|
||||||
OIDC_CLIENT_SECRET = ""
|
client_id='',
|
||||||
OIDC_AUTHORIZE_URL = "https://{your-casdoor-hostname}/login/oauth/authorize"
|
client_secret='',
|
||||||
OIDC_TOKEN_URL = "https://{your-casdoor-hostname}/api/login/oauth/access_token"
|
authorize_url='https://{your-OIDCServer-hostname}/login/oauth/authorize',
|
||||||
OIDC_USER_INFO = {
|
token_url='https://{your-OIDCServer-hostname}/api/login/oauth/access_token',
|
||||||
"url": "https://{your-casdoor-hostname}/api/userinfo",
|
scopes=['openid', 'profile', 'email'],
|
||||||
"email": lambda x: x['email'],
|
user_info={
|
||||||
"username": lambda x: x['name']
|
'url': 'https://{your-OIDCServer-hostname}/api/userinfo',
|
||||||
}
|
'email': 'email',
|
||||||
OIDC_SCOPES = ["openid profile email"]
|
'username': 'name',
|
||||||
OIDC_AFTER_LOGIN = "/"
|
'avatar': 'picture'
|
||||||
|
},
|
||||||
|
after_login='/'
|
||||||
|
)
|
||||||
|
|
||||||
# # LDAP
|
# # LDAP
|
||||||
AUTH_WITH_LDAP = False
|
LDAP = dict(
|
||||||
LDAP_SERVER = ''
|
enabled=False,
|
||||||
LDAP_DOMAIN = ''
|
ldap_server='',
|
||||||
LDAP_USER_DN = 'cn={},ou=users,dc=xxx,dc=com'
|
ldap_domain='',
|
||||||
|
ldap_user_dn='cn={},ou=users,dc=xxx,dc=com'
|
||||||
|
)
|
||||||
# ==========================================================================================================
|
# ==========================================================================================================
|
||||||
|
|
||||||
# # pagination
|
# # pagination
|
||||||
DEFAULT_PAGE_COUNT = 50
|
DEFAULT_PAGE_COUNT = 50
|
||||||
|
|
||||||
# # permission
|
# # permission
|
||||||
WHITE_LIST = ["127.0.0.1"]
|
WHITE_LIST = ['127.0.0.1']
|
||||||
USE_ACL = True
|
USE_ACL = True
|
||||||
|
|
||||||
# # elastic search
|
# # elastic search
|
||||||
ES_HOST = '127.0.0.1'
|
ES_HOST = '127.0.0.1'
|
||||||
USE_ES = False
|
USE_ES = False
|
||||||
|
|
||||||
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y']
|
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, 'Yes', 'YES', 'yes', 'Y', 'y']
|
||||||
|
|
||||||
# # messenger
|
# # messenger
|
||||||
USE_MESSENGER = True
|
USE_MESSENGER = True
|
||||||
|
|
|
@ -2,7 +2,7 @@ version: '3.5'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
cmdb-db:
|
cmdb-db:
|
||||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-db:3.0
|
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-db:2.3
|
||||||
container_name: cmdb-db
|
container_name: cmdb-db
|
||||||
environment:
|
environment:
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
|
@ -22,7 +22,7 @@ services:
|
||||||
- '23306:3306'
|
- '23306:3306'
|
||||||
|
|
||||||
cmdb-cache:
|
cmdb-cache:
|
||||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-cache:3.0
|
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-cache:2.3
|
||||||
container_name: cmdb-cache
|
container_name: cmdb-cache
|
||||||
environment:
|
environment:
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
|
|
Loading…
Reference in New Issue