pref(api): authentication and login log (#308)

* pref(api): authentication and login log

* feat(api): ldap and OAuth2.0
This commit is contained in:
pycook
2023-12-14 19:53:08 +08:00
committed by GitHub
parent d4a37af183
commit 73d53f0440
16 changed files with 312 additions and 296 deletions

View File

@@ -21,7 +21,6 @@ from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager,
from api.extensions import inner_secrets
from api.lib.perm.authentication.cas import CAS
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.models.acl import User
@@ -98,7 +97,6 @@ def create_app(config_object="settings"):
register_shell_context(app)
register_commands(app)
CAS(app)
OIDC(app)
OAuth2(app)
app.wsgi_app = ReverseProxy(app.wsgi_app)
configure_upload_dir(app)

View File

@@ -94,7 +94,7 @@ class CRUDMixin(FormatMixin):
if any((isinstance(_id, six.string_types) and _id.isdigit(),
isinstance(_id, (six.integer_types, float))), ):
obj = getattr(cls, "query").get(int(_id))
if obj and not obj.deleted:
if obj and not getattr(obj, 'deleted', False):
return obj
@classmethod

View File

@@ -1,14 +1,19 @@
# -*- coding:utf-8 -*-
import datetime
import itertools
import json
from enum import Enum
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 sqlalchemy import func
from api.extensions import db
from api.lib.perm.acl import AppCache
from api.models.acl import AuditLoginLog
from api.models.acl import AuditPermissionLog
from api.models.acl import AuditResourceLog
from api.models.acl import AuditRoleLog
@@ -283,6 +288,27 @@ class AuditCRUD(object):
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
def add_role_log(cls, app_id, operate_type: AuditOperateType,
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,
operate_type=operate_type.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

View File

@@ -4,6 +4,9 @@ from api.lib.resp_format import CommonErrFormat
class ErrFormat(CommonErrFormat):
login_succeed = "登录成功"
ldap_connection_failed = "连接LDAP服务失败"
invalid_password = "密码验证失败"
auth_only_with_app_token_failed = "应用 Token验证失败"
session_invalid = "您不是应用管理员 或者 session失效(尝试一下退出重新登录)"
@@ -17,11 +20,11 @@ class ErrFormat(CommonErrFormat):
role_exists = "角色 {} 已经存在!"
global_role_not_found = "全局角色 {} 不存在!"
global_role_exists = "全局角色 {} 已经存在!"
user_role_delete_invalid = "删除用户角色, 请在 用户管理 页面操作!"
resource_no_permission = "您没有资源: {}{} 权限"
admin_required = "需要管理员权限"
role_required = "需要角色: {}"
user_role_delete_invalid = "删除用户角色, 请在 用户管理 页面操作!"
app_is_ready_existed = "应用 {} 已经存在"
app_not_found = "应用 {} 不存在!"

View File

@@ -93,6 +93,9 @@ def _auth_with_token():
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
key = request.values.get('_key')
secret = request.values.get('_secret')

View File

@@ -1,4 +1,5 @@
# -*- coding:utf-8 -*-
import datetime
import uuid
import bs4
@@ -12,7 +13,11 @@ from flask_login import login_user
from flask_login import logout_user
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.resp_format import ErrFormat
from .cas_urls import create_cas_login_url
from .cas_urls import create_cas_logout_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/sso/login')
@blueprint.route('/api/sso/login')
def login():
"""
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
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']
if request.values.get("next"):
@@ -41,8 +47,8 @@ def login():
_service = url_for('cas.login', _external=True)
redirect_url = create_cas_login_url(
current_app.config['CAS_SERVER'],
current_app.config['CAS_LOGIN_ROUTE'],
config['cas_server'],
config['cas_login_route'],
_service)
if 'ticket' in request.args:
@@ -51,30 +57,38 @@ def login():
if request.args.get('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")
user = UserCache.get(username)
login_user(user)
session.permanent = True
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
else:
del session[cas_token_session_key]
redirect_url = create_cas_login_url(
current_app.config['CAS_SERVER'],
current_app.config['CAS_LOGIN_ROUTE'],
config['cas_server'],
config['cas_login_route'],
url_for('cas.login', _external=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))
return redirect(redirect_url)
@blueprint.route('/api/cas/logout')
# @blueprint.route('/api/sso/logout')
@blueprint.route('/api/sso/logout')
def logout():
"""
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_token_session_key = current_app.config['CAS_TOKEN_SESSION_KEY']
@@ -86,12 +100,14 @@ def logout():
"next" in session and session.pop("next")
redirect_url = create_cas_logout_url(
current_app.config['CAS_SERVER'],
current_app.config['CAS_LOGOUT_ROUTE'],
config['cas_server'],
config['cas_logout_route'],
url_for('cas.login', _external=True, next=request.referrer))
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))
return redirect(redirect_url)
@@ -104,14 +120,15 @@ def validate(ticket):
and the validated username is saved in the session under the
key `CAS_USERNAME_SESSION_KEY`.
"""
config = AuthenticateDataCRUD(AuthenticateType.CAS).get()
cas_username_session_key = current_app.config['CAS_USERNAME_SESSION_KEY']
current_app.logger.debug("validating token {0}".format(ticket))
cas_validate_url = create_cas_validate_url(
current_app.config['CAS_VALIDATE_SERVER'],
current_app.config['CAS_VALIDATE_ROUTE'],
config['cas_validate_server'],
config['cas_validate_route'],
url_for('cas.login', _external=True),
ticket)
@@ -138,14 +155,12 @@ def validate(ticket):
current_app.logger.info("create user: {}".format(username))
from api.lib.perm.acl.user import UserCRUD
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()
for k in cas_user_map:
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['password'] = uuid.uuid4().hex
UserCRUD.add(**user_dict)
from api.lib.perm.acl.acl import ACLManager

View File

@@ -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)

View File

@@ -18,6 +18,10 @@ class OAuth2(object):
app.config.setdefault('OAUTH2_RESPONSE_TYPE', 'code')
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
app.register_blueprint(routing.blueprint, url_prefix=url_prefix)

View File

@@ -1,5 +1,6 @@
# -*- coding:utf-8 -*-
import datetime
import secrets
import uuid
@@ -14,69 +15,80 @@ from flask import url_for
from flask_login import login_user, logout_user
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.resp_format import ErrFormat
blueprint = Blueprint('oauth2', __name__)
@blueprint.route('/api/oauth2/login')
@blueprint.route('/api/sso/login')
def login():
@blueprint.route('/api/<string:auth_type>/login')
def login(auth_type):
config = AuthenticateDataCRUD(auth_type.upper()).get()
if 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({
'client_id': current_app.config['OAUTH2_CLIENT_ID'],
'redirect_uri': url_for('oauth2.callback', _external=True),
'response_type': current_app.config['OAUTH2_RESPONSE_TYPE'],
'scope': ' '.join(current_app.config['OAUTH2_SCOPES'] or []),
'state': session['oauth2_state'],
'client_id': config['client_id'],
'redirect_uri': url_for('oauth2.callback', auth_type=auth_type.lower(), _external=True),
'response_type': current_app.config[f'{auth_type}_RESPONSE_TYPE'],
'scope': ' '.join(config['scopes'] or []),
'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')
def callback():
redirect_url = session.get("next") or current_app.config.get("OAUTH2_AFTER_LOGIN")
@blueprint.route('/api/<string:auth_type>/callback')
def callback(auth_type):
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")
if 'code' not in request.values:
return abort(401, 'code is invalid')
response = requests.post(current_app.config['OAUTH2_TOKEN_URL'], data={
'client_id': current_app.config['OAUTH2_CLIENT_ID'],
'client_secret': current_app.config['OAUTH2_CLIENT_SECRET'],
response = requests.post(config['token_url'], data={
'client_id': config['client_id'],
'client_secret': config['client_secret'],
'code': request.values['code'],
'grant_type': current_app.config['OAUTH2_GRANT_TYPE'],
'redirect_uri': url_for('oauth2.callback', _external=True),
'grant_type': current_app.config[f'{auth_type}_GRANT_TYPE'],
'redirect_uri': url_for('oauth2.callback', auth_type=auth_type.lower(), _external=True),
}, headers={'Accept': 'application/json'})
if response.status_code != 200:
current_app.logger.error(response.text)
return abort(401)
oauth2_token = response.json().get('access_token')
if not oauth2_token:
access_token = response.json().get('access_token')
if not access_token:
return abort(401)
response = requests.get(current_app.config['OAUTH2_USER_INFO']['url'], headers={
'Authorization': 'Bearer {}'.format(oauth2_token),
response = requests.get(config['user_info']['url'], headers={
'Authorization': 'Bearer {}'.format(access_token),
'Accept': 'application/json',
})
if response.status_code != 200:
return abort(401)
email = current_app.config['OAUTH2_USER_INFO']['email'](response.json())
username = current_app.config['OAUTH2_USER_INFO']['username'](response.json())
res = 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)
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 = dict(username=username, email=email, avatar=avatar)
user_dict['password'] = uuid.uuid4().hex
user = UserCRUD.add(**user_dict)
@@ -98,21 +110,25 @@ def callback():
roleName=user_info.get("role"))
session["uid"] = user_info.get("uid")
_id = AuditCRUD.add_login_log(username, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
return redirect(redirect_url)
@blueprint.route('/api/oauth2/logout')
@blueprint.route('/api/sso/logout')
def logout():
@blueprint.route('/api/<string:auth_type>/logout')
def logout(auth_type):
"acl" in session and session.pop("acl")
"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")
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()
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)

View File

@@ -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

View File

@@ -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)

View File

@@ -5,17 +5,18 @@ import copy
import hashlib
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 session
from flask_sqlalchemy import BaseQuery
from api.extensions import db
from api.lib.database import CRUDModel
from api.lib.database import Model
from api.lib.database import Model2
from api.lib.database import SoftDeleteMixin
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.const import OperateType
from api.lib.perm.acl.resp_format import ErrFormat
class App(Model):
@@ -28,21 +29,26 @@ class App(Model):
class UserQuery(BaseQuery):
def _join(self, *args, **kwargs):
super(UserQuery, self)._join(*args, **kwargs)
def authenticate(self, login, password):
from api.lib.perm.acl.audit import AuditCRUD
user = self.filter(db.or_(User.username == login,
User.email == login)).filter(User.deleted.is_(False)).filter(User.block == 0).first()
if user:
current_app.logger.info(user)
authenticated = user.check_password(password)
if authenticated:
from api.tasks.acl import op_record
op_record.apply_async(args=(None, login, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
_id = AuditCRUD.add_login_log(login, True, ErrFormat.login_succeed)
session['LOGIN_ID'] = _id
else:
AuditCRUD.add_login_log(login, False, ErrFormat.invalid_password)
else:
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
def authenticate_with_key(self, key, secret, args, path):
@@ -57,38 +63,6 @@ class UserQuery(BaseQuery):
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):
query = self.filter(db.or_(User.email == key,
User.nickname.ilike('%' + key + '%'),
@@ -138,6 +112,7 @@ class User(CRUDModel, SoftDeleteMixin):
wx_id = db.Column(db.String(32))
employee_id = db.Column(db.String(16), index=True)
avatar = db.Column(db.String(128))
# apps = db.Column(db.JSON)
def __str__(self):
@@ -168,8 +143,6 @@ class User(CRUDModel, SoftDeleteMixin):
class RoleQuery(BaseQuery):
def _join(self, *args, **kwargs):
super(RoleQuery, self)._join(*args, **kwargs)
def authenticate(self, login, password):
role = self.filter(Role.name == login).first()
@@ -377,3 +350,16 @@ class AuditTriggerLog(Model):
current = db.Column(db.JSON, default=dict(), comment='当前数据')
extra = db.Column(db.JSON, default=dict(), 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)

View File

@@ -24,6 +24,7 @@ class AuditLogView(APIView):
'role': AuditCRUD.search_role,
'trigger': AuditCRUD.search_trigger,
'resource': AuditCRUD.search_resource,
'login': AuditCRUD.search_login,
}
if name not in func_map:
abort(400, f'wrong {name}, please use {func_map.keys()}')

View File

@@ -8,11 +8,15 @@ from flask import abort
from flask import current_app
from flask import request
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_validate
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 User
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")
password = request.values.get("password")
_role = None
if current_app.config.get('AUTH_WITH_LDAP'):
user, authenticated = User.query.authenticate_with_ldap(username, password)
config = AuthenticateDataCRUD(AuthenticateType.LDAP).get()
if config.get('LDAP', {}).get('enabled'):
from api.lib.perm.authentication.ldap import authenticate_with_ldap
user, authenticated = authenticate_with_ldap(username, password)
else:
user, authenticated = User.query.authenticate(username, password)
if not user:
@@ -176,4 +182,7 @@ class LogoutView(APIView):
@auth_abandoned
def post(self):
logout_user()
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
self.jsonify(code=200)