diff --git a/api/app.py b/api/app.py index 1774f0d..cfd0e93 100644 --- a/api/app.py +++ b/api/app.py @@ -23,7 +23,7 @@ from api.extensions import ( rd, ) from api.flask_cas import CAS -from api.models.account import User +from api.models.acl import User HERE = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.join(HERE, os.pardir) diff --git a/api/flask_cas/routing.py b/api/flask_cas/routing.py index 41c0086..9fea4fe 100644 --- a/api/flask_cas/routing.py +++ b/api/flask_cas/routing.py @@ -8,7 +8,7 @@ from flask import current_app, session, request, url_for, redirect from flask_login import login_user, logout_user from six.moves.urllib_request import urlopen -from api.models.account import UserCache +from api.lib.perm.acl.cache import UserCache from .cas_urls import create_cas_login_url from .cas_urls import create_cas_logout_url from .cas_urls import create_cas_validate_url diff --git a/api/lib/cmdb/history.py b/api/lib/cmdb/history.py index ed2ba6f..e21bc08 100644 --- a/api/lib/cmdb/history.py +++ b/api/lib/cmdb/history.py @@ -7,7 +7,7 @@ from flask import g from api.extensions import db from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import RelationTypeCache -from api.models.account import UserCache +from api.lib.perm.acl.cache import UserCache from api.models.cmdb import Attribute from api.models.cmdb import AttributeHistory from api.models.cmdb import CIRelationHistory diff --git a/api/lib/perm/acl/cache.py b/api/lib/perm/acl/cache.py index 030f4d2..bc0cb70 100644 --- a/api/lib/perm/acl/cache.py +++ b/api/lib/perm/acl/cache.py @@ -3,6 +3,39 @@ from api.extensions import cache from api.models.acl import Permission from api.models.acl import Role +from api.models.acl import User + + +class UserCache(object): + PREFIX_ID = "User::uid::{0}" + PREFIX_NAME = "User::username::{0}" + PREFIX_NICK = "User::nickname::{0}" + + @classmethod + def get(cls, key): + user = cache.get(cls.PREFIX_ID.format(key)) or \ + cache.get(cls.PREFIX_NAME.format(key)) or \ + cache.get(cls.PREFIX_NICK.format(key)) + if not user: + user = User.query.get(key) or \ + User.query.get_by_username(key) or \ + User.query.get_by_nickname(key) + if user: + cls.set(user) + + return user + + @classmethod + def set(cls, user): + cache.set(cls.PREFIX_ID.format(user.uid), user) + cache.set(cls.PREFIX_NAME.format(user.username), user) + cache.set(cls.PREFIX_NICK.format(user.nickname), user) + + @classmethod + def clean(cls, user): + cache.delete(cls.PREFIX_ID.format(user.uid)) + cache.delete(cls.PREFIX_NAME.format(user.username)) + cache.delete(cls.PREFIX_NICK.format(user.nickname)) class RoleCache(object): diff --git a/api/lib/perm/acl/role.py b/api/lib/perm/acl/role.py index 8bbc5e0..85a9c87 100644 --- a/api/lib/perm/acl/role.py +++ b/api/lib/perm/acl/role.py @@ -18,13 +18,21 @@ from api.tasks.acl import role_rebuild class RoleRelationCRUD(object): @staticmethod - def get_parents(rids): - rids = [rids] if isinstance(rids, six.integer_types) else rids + def get_parents(rids=None, uids=None): + rid2uid = dict() + if uids is not None: + uids = [uids] if isinstance(uids, six.integer_types) else uids + rids = db.session.query(Role).filter(Role.deleted.is_(False)).filter(Role.uid.in_(uids)) + rid2uid = {i.rid: i.uid for i in rids} + rids = [i.rid for i in rids] + else: + rids = [rids] if isinstance(rids, six.integer_types) else rids + res = db.session.query(RoleRelation).filter( RoleRelation.child_id.in_(rids)).filter(RoleRelation.deleted.is_(False)) id2parents = {} for i in res: - id2parents.setdefault(i.child_id, []).append(RoleCache.get(i.parent_id).to_dict()) + id2parents.setdefault(rid2uid.get(i.child_id, i.child_id), []).append(RoleCache.get(i.parent_id).to_dict()) return id2parents diff --git a/api/lib/perm/acl/user.py b/api/lib/perm/acl/user.py new file mode 100644 index 0000000..5832a47 --- /dev/null +++ b/api/lib/perm/acl/user.py @@ -0,0 +1,45 @@ +# -*- coding:utf-8 -*- + + +from flask import abort + +from api.extensions import db +from api.lib.perm.acl.cache import UserCache +from api.models.acl import User + + +class UserCRUD(object): + @staticmethod + def search(q, page=1, page_size=None): + query = db.session.query(User).filter(User.deleted.is_(False)) + if q: + query = query.filter(User.username.ilike('%{0}%'.format(q))) + + numfound = query.count() + + return numfound, query.offset((page - 1) * page_size).limit(page_size) + + @staticmethod + def add(**kwargs): + existed = User.get_by(username=kwargs['username'], email=kwargs['email']) + existed and abort(400, "User <{0}> is already existed".format(kwargs['username'])) + + kwargs['nickname'] = kwargs['username'] if not kwargs.get('nickname') else kwargs['nickname'] + kwargs['block'] = 0 + return User.create(**kwargs) + + @staticmethod + def update(rid, **kwargs): + user = User.get_by_id(rid) or abort(404, "User <{0}> does not exist".format(rid)) + + UserCache.clean(rid) + + return user.update(**kwargs) + + @classmethod + def delete(cls, uid): + user = User.get_by_id(uid) or abort(404, "User <{0}> does not exist".format(uid)) + + UserCache.clean(user) + + user.soft_delete() diff --git a/api/lib/perm/auth.py b/api/lib/perm/auth.py index 23fe01e..22d82d8 100644 --- a/api/lib/perm/auth.py +++ b/api/lib/perm/auth.py @@ -13,8 +13,8 @@ from flask import request from flask import session from flask_login import login_user -from api.models.account import User -from api.models.account import UserCache +from api.models.acl import User +from api.lib.perm.acl.cache import UserCache def _auth_with_key(): diff --git a/api/models/__init__.py b/api/models/__init__.py index c10c68e..628978a 100644 --- a/api/models/__init__.py +++ b/api/models/__init__.py @@ -1,6 +1,5 @@ # -*- coding:utf-8 -*- -from .account import User from .cmdb import * from .acl import * diff --git a/api/models/account.py b/api/models/account.py deleted file mode 100644 index 5430fdb..0000000 --- a/api/models/account.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- coding:utf-8 -*- - -import copy -import hashlib -from datetime import datetime - -from flask import current_app -from flask_sqlalchemy import BaseQuery - -from api.extensions import cache -from api.extensions import db -from api.lib.database import CRUDModel - - -class UserQuery(BaseQuery): - def _join(self, *args, **kwargs): - super(UserQuery, self)._join(*args, **kwargs) - - def authenticate(self, login, password): - user = self.filter(db.or_(User.username == login, - User.email == login)).first() - if user: - current_app.logger.info(user) - authenticated = user.check_password(password) - else: - authenticated = False - return user, authenticated - - def authenticate_with_key(self, key, secret, args, path): - user = self.filter(User.key == key).filter(User.block == 0).first() - if not user: - return None, False - if user and hashlib.sha1('{0}{1}{2}'.format( - path, user.secret, "".join(args)).encode("utf-8")).hexdigest() == secret: - authenticated = True - else: - authenticated = False - return user, authenticated - - def search(self, key): - query = self.filter(db.or_(User.email == key, - User.nickname.ilike('%' + key + '%'), - User.username.ilike('%' + key + '%'))) - return query - - def get_by_username(self, username): - user = self.filter(User.username == username).first() - return user - - def get_by_nickname(self, nickname): - user = self.filter(User.nickname == nickname).first() - return user - - def get(self, uid): - user = self.filter(User.uid == uid).first() - return copy.deepcopy(user) - - -class User(CRUDModel): - __tablename__ = 'users' - __bind_key__ = "user" - query_class = UserQuery - - uid = db.Column(db.Integer, primary_key=True, autoincrement=True) - username = db.Column(db.String(32), unique=True) - nickname = db.Column(db.String(20), nullable=True) - department = db.Column(db.String(20)) - catalog = db.Column(db.String(64)) - email = db.Column(db.String(100), unique=True, nullable=False) - mobile = db.Column(db.String(14), unique=True) - _password = db.Column("password", db.String(80)) - key = db.Column(db.String(32), nullable=False) - secret = db.Column(db.String(32), nullable=False) - date_joined = db.Column(db.DateTime, default=datetime.utcnow) - last_login = db.Column(db.DateTime, default=datetime.utcnow) - block = db.Column(db.Boolean, default=False) - has_logined = db.Column(db.Boolean, default=False) - wx_id = db.Column(db.String(32)) - avatar = db.Column(db.String(128)) - - def __str__(self): - return self.username - - def is_active(self): - return not self.block - - def get_id(self): - return self.uid - - @staticmethod - def is_authenticated(): - return True - - def _get_password(self): - return self._password - - def _set_password(self, password): - self._password = hashlib.md5(password.encode('utf-8')).hexdigest() - - password = db.synonym("_password", - descriptor=property(_get_password, - _set_password)) - - def check_password(self, password): - if self.password is None: - return False - return self.password == password - - -class UserCache(object): - PREFIX_ID = "User::uid::{0}" - PREFIX_NAME = "User::username::{0}" - PREFIX_NICK = "User::nickname::{0}" - - @classmethod - def get(cls, key): - user = cache.get(cls.PREFIX_ID.format(key)) or \ - cache.get(cls.PREFIX_NAME.format(key)) or \ - cache.get(cls.PREFIX_NICK.format(key)) - if not user: - user = User.query.get(key) or \ - User.query.get_by_username(key) or \ - User.query.get_by_nickname(key) - if user: - cls.set(user) - - return user - - @classmethod - def set(cls, user): - cache.set(cls.PREFIX_ID.format(user.uid), user) - cache.set(cls.PREFIX_NAME.format(user.username), user) - cache.set(cls.PREFIX_NICK.format(user.nickname), user) - - @classmethod - def clean(cls, user): - cache.delete(cls.PREFIX_ID.format(user.uid)) - cache.delete(cls.PREFIX_NAME.format(user.username)) - cache.delete(cls.PREFIX_NICK.format(user.nickname)) diff --git a/api/models/acl.py b/api/models/acl.py index fea5845..d1b3056 100644 --- a/api/models/acl.py +++ b/api/models/acl.py @@ -1,6 +1,15 @@ # -*- coding:utf-8 -*- + +import copy +import hashlib +from datetime import datetime + +from flask import current_app +from flask_sqlalchemy import BaseQuery + from api.extensions import db +from api.lib.database import CRUDModel from api.lib.database import Model @@ -13,6 +22,101 @@ class App(Model): secret_key = db.Column(db.Text) +class UserQuery(BaseQuery): + def _join(self, *args, **kwargs): + super(UserQuery, self)._join(*args, **kwargs) + + def authenticate(self, login, password): + user = self.filter(db.or_(User.username == login, + User.email == login)).first() + if user: + current_app.logger.info(user) + authenticated = user.check_password(password) + else: + authenticated = False + return user, authenticated + + def authenticate_with_key(self, key, secret, args, path): + user = self.filter(User.key == key).filter(User.block == 0).first() + if not user: + return None, False + if user and hashlib.sha1('{0}{1}{2}'.format( + path, user.secret, "".join(args)).encode("utf-8")).hexdigest() == secret: + authenticated = True + else: + authenticated = False + return user, authenticated + + def search(self, key): + query = self.filter(db.or_(User.email == key, + User.nickname.ilike('%' + key + '%'), + User.username.ilike('%' + key + '%'))) + return query + + def get_by_username(self, username): + user = self.filter(User.username == username).first() + return user + + def get_by_nickname(self, nickname): + user = self.filter(User.nickname == nickname).first() + return user + + def get(self, uid): + user = self.filter(User.uid == uid).first() + return copy.deepcopy(user) + + +class User(CRUDModel): + __tablename__ = 'users' + __bind_key__ = "user" + query_class = UserQuery + + uid = db.Column(db.Integer, primary_key=True, autoincrement=True) + username = db.Column(db.String(32), unique=True) + nickname = db.Column(db.String(20), nullable=True) + department = db.Column(db.String(20)) + catalog = db.Column(db.String(64)) + email = db.Column(db.String(100), unique=True, nullable=False) + mobile = db.Column(db.String(14), unique=True) + _password = db.Column("password", db.String(80)) + key = db.Column(db.String(32), nullable=False) + secret = db.Column(db.String(32), nullable=False) + date_joined = db.Column(db.DateTime, default=datetime.utcnow) + last_login = db.Column(db.DateTime, default=datetime.utcnow) + block = db.Column(db.Boolean, default=False) + has_logined = db.Column(db.Boolean, default=False) + wx_id = db.Column(db.String(32)) + avatar = db.Column(db.String(128)) + + def __str__(self): + return self.username + + def is_active(self): + return not self.block + + def get_id(self): + return self.uid + + @staticmethod + def is_authenticated(): + return True + + def _get_password(self): + return self._password + + def _set_password(self, password): + self._password = hashlib.md5(password.encode('utf-8')).hexdigest() + + password = db.synonym("_password", + descriptor=property(_get_password, + _set_password)) + + def check_password(self, password): + if self.password is None: + return False + return self.password == password + + class Role(Model): __tablename__ = "acl_roles" diff --git a/api/views/__init__.py b/api/views/__init__.py index c507414..4cde7ce 100644 --- a/api/views/__init__.py +++ b/api/views/__init__.py @@ -6,7 +6,6 @@ from flask import Blueprint from flask_restful import Api from api.resource import register_resources -from .permission import GetResourcesView, HasPermissionView, GetUserInfoView from .account import LoginView, LogoutView HERE = os.path.abspath(os.path.dirname(__file__)) @@ -17,13 +16,6 @@ account_rest = Api(blueprint_account) account_rest.add_resource(LoginView, LoginView.url_prefix) account_rest.add_resource(LogoutView, LogoutView.url_prefix) -# permission -blueprint_perm_v01 = Blueprint('permission_api', __name__, url_prefix='/api/v1/perms') -perm_rest = Api(blueprint_perm_v01) -perm_rest.add_resource(GetResourcesView, GetResourcesView.url_prefix) -perm_rest.add_resource(HasPermissionView, HasPermissionView.url_prefix) -perm_rest.add_resource(GetUserInfoView, GetUserInfoView.url_prefix) - # cmdb blueprint_cmdb_v01 = Blueprint('cmdb_api_v01', __name__, url_prefix='/api/v0.1') diff --git a/api/views/account.py b/api/views/account.py index 21cc2f1..cd133a8 100644 --- a/api/views/account.py +++ b/api/views/account.py @@ -10,7 +10,7 @@ from flask_login import login_user, logout_user from api.lib.decorator import args_required from api.lib.perm.auth import auth_abandoned -from api.models.account import User +from api.models.acl import User from api.resource import APIView @@ -24,6 +24,8 @@ class LoginView(APIView): username = request.values.get("username") or request.values.get("email") password = request.values.get("password") user, authenticated = User.query.authenticate(username, password) + if not user: + return abort(403, "User <{0}> does not exist".format(username)) if not authenticated: return abort(403, "invalid username or password") diff --git a/api/views/acl/user.py b/api/views/acl/user.py new file mode 100644 index 0000000..9e0d576 --- /dev/null +++ b/api/views/acl/user.py @@ -0,0 +1,60 @@ +# -*- coding:utf-8 -*- + + +from flask import request +from flask import session +from flask_login import current_user + +from api.lib.decorator import args_required +from api.lib.perm.acl.user import UserCRUD +from api.lib.perm.acl.role import RoleRelationCRUD +from api.lib.utils import get_page +from api.lib.utils import get_page_size +from api.resource import APIView + + +class GetUserInfoView(APIView): + url_prefix = "/users/info" + + def get(self): + name = session.get("acl", {}).get("nickName") or session.get("CAS_USERNAME") or current_user.nickname + role = dict(permissions=session.get("acl", {}).get("parentRoles", []) or ["admin"]) + avatar = session.get("acl", {}).get("avatar") or current_user.avatar + return self.jsonify(result=dict(name=name, + role=role, + avatar=avatar)) + + +class UserView(APIView): + url_prefix = ("/users", "/users/") + + def get(self): + page = get_page(request.values.get('page', 1)) + page_size = get_page_size(request.values.get('page_size')) + q = request.values.get("q") + numfound, users = UserCRUD.search(q, page, page_size) + + id2parents = RoleRelationCRUD.get_parents(uids=[i.uid for i in users]) + + return self.jsonify(numfound=numfound, + page=page, + page_size=page_size, + id2parents=id2parents, + users=[i.to_dict() for i in users]) + + @args_required('username') + @args_required('email') + def post(self): + user = UserCRUD.add(**request.values) + + return self.jsonify(user.to_dict()) + + def put(self, uid): + user = UserCRUD.update(uid, **request.values) + + return self.jsonify(user.to_dict()) + + def delete(self, uid): + UserCRUD.delete(uid) + + return self.jsonify(uid=uid) diff --git a/api/views/permission.py b/api/views/permission.py deleted file mode 100644 index 3eb67d8..0000000 --- a/api/views/permission.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding:utf-8 -*- - -from flask import request -from flask import session -from flask_login import current_user - -from api.lib.decorator import args_required -from api.lib.perm.acl.acl import ACLManager -from api.lib.perm.acl.acl import validate_permission -from api.resource import APIView - - -class HasPermissionView(APIView): - url_prefix = "/validate" - - @args_required("resource") - @args_required("resource_type") - @args_required("perm") - def get(self): - resource = request.values.get("resource") - resource_type = request.values.get("resource_type") - perm = request.values.get("perm") - validate_permission(resource, resource_type, perm) - return self.jsonify(is_valid=True) - - def post(self): - self.get() - - -class GetResourcesView(APIView): - url_prefix = "/resources" - - @args_required("resource_type") - def get(self): - resource_type = request.values.get("resource_type") - res = ACLManager().get_resources(resource_type) - return self.jsonify(res) - - -class GetUserInfoView(APIView): - url_prefix = "/user/info" - - def get(self): - name = session.get("acl", {}).get("nickName") or session.get("CAS_USERNAME") or current_user.nickname - role = dict(permissions=session.get("acl", {}).get("parentRoles", []) or ["admin"]) - avatar = session.get("acl", {}).get("avatar") or current_user.avatar - return self.jsonify(result=dict(name=name, - role=role, - avatar=avatar))