diff --git a/README.md b/README.md index e0e475f..5a168e5 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ - username: admin - password: admin +> **重要提示**: `master` 分支在开发过程中可能处于 *不稳定的状态* 。 +请通过[releases](https://github.com/pycook/cmdb/releases)获取 + Overview ---- ![基础资源视图](https://raw.githubusercontent.com/pycook/cmdb/master/ui/public/cmdb01.jpeg) diff --git a/api/lib/database.py b/api/lib/database.py index 82c6c62..cc43a56 100644 --- a/api/lib/database.py +++ b/api/lib/database.py @@ -10,9 +10,14 @@ from api.lib.exception import CommitException class FormatMixin(object): def to_dict(self): - return dict([(k.name, - getattr(self, k.name) if not isinstance(getattr(self, k.name), datetime.datetime) else getattr( - self, k.name).strftime('%Y-%m-%d %H:%M:%S')) for k in getattr(self, "__table__").columns]) + res = dict() + for k in getattr(self, "__table__").columns: + if not isinstance(getattr(self, k.name), datetime.datetime): + res[k.name] = getattr(self, k.name) + else: + res[k.name] = getattr(self, k.name).strftime('%Y-%m-%d %H:%M:%S') + + return res @classmethod def get_columns(cls): diff --git a/api/lib/perm/acl/__init__.py b/api/lib/perm/acl/__init__.py index 380474e..543604e 100644 --- a/api/lib/perm/acl/__init__.py +++ b/api/lib/perm/acl/__init__.py @@ -1 +1,23 @@ # -*- coding:utf-8 -*- + + +from functools import wraps + +from flask import request +from flask import abort + +from api.lib.perm.acl.cache import AppCache + + +def validate_app(func): + @wraps(func) + def wrapper(*args, **kwargs): + app_id = request.values.get('app_id') + app = AppCache.get(app_id) + if app is None: + return abort(400, "App <{0}> does not exist".format(app_id)) + request.values['app_id'] = app.id + + return func(*args, **kwargs) + + return wrapper diff --git a/api/lib/perm/acl/cache.py b/api/lib/perm/acl/cache.py index bc0cb70..d2b8a4f 100644 --- a/api/lib/perm/acl/cache.py +++ b/api/lib/perm/acl/cache.py @@ -1,11 +1,37 @@ # -*- coding:utf-8 -*- from api.extensions import cache +from api.models.acl import App from api.models.acl import Permission from api.models.acl import Role from api.models.acl import User +class AppCache(object): + PREFIX_ID = "App::id::{0}" + PREFIX_NAME = "App::name::{0}" + + @classmethod + def get(cls, key): + app = cache.get(cls.PREFIX_ID.format(key)) or cache.get(cls.PREFIX_NAME.format(key)) + if app is None: + app = App.get_by_id(key) or App.get_by(name=key, to_dict=False, first=True) + if app is not None: + cls.set(app) + + return app + + @classmethod + def set(cls, app): + cache.set(cls.PREFIX_ID.format(app.id), app) + cache.set(cls.PREFIX_NAME.format(app.name), app) + + @classmethod + def clean(cls, app): + cache.delete(cls.PREFIX_ID.format(app.id)) + cache.delete(cls.PREFIX_NAME.format(app.name)) + + class UserCache(object): PREFIX_ID = "User::uid::{0}" PREFIX_NAME = "User::username::{0}" diff --git a/api/lib/perm/acl/resource.py b/api/lib/perm/acl/resource.py index 9826d65..6800820 100644 --- a/api/lib/perm/acl/resource.py +++ b/api/lib/perm/acl/resource.py @@ -156,6 +156,3 @@ class ResourceCRUD(object): resource = Resource.get_by_id(_id) or abort(404, "Resource <{0}> is not found".format(_id)) resource.soft_delete() - - - diff --git a/api/lib/perm/acl/user.py b/api/lib/perm/acl/user.py index 5832a47..178502c 100644 --- a/api/lib/perm/acl/user.py +++ b/api/lib/perm/acl/user.py @@ -1,7 +1,12 @@ # -*- coding:utf-8 -*- +import uuid +import string +import random + from flask import abort +from flask import g from api.extensions import db from api.lib.perm.acl.cache import UserCache @@ -20,12 +25,21 @@ class UserCRUD(object): return numfound, query.offset((page - 1) * page_size).limit(page_size) @staticmethod - def add(**kwargs): + def _gen_key_secret(): + key = uuid.uuid4().hex + secret = ''.join(random.sample(string.ascii_letters + string.digits + '~!@#$%^&*?', 32)) + + return key, secret + + @classmethod + def add(cls, **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['nickname'] = kwargs.get('nickname') or kwargs['username'] kwargs['block'] = 0 + kwargs['key'], kwargs['secret'] = cls._gen_key_secret() + return User.create(**kwargs) @staticmethod @@ -36,6 +50,13 @@ class UserCRUD(object): return user.update(**kwargs) + @classmethod + def reset_key_secret(cls): + key, secret = cls._gen_key_secret() + g.user.update(key=key, secret=secret) + + return key, secret + @classmethod def delete(cls, uid): user = User.get_by_id(uid) or abort(404, "User <{0}> does not exist".format(uid)) diff --git a/api/models/acl.py b/api/models/acl.py index 79328f7..ec374b2 100644 --- a/api/models/acl.py +++ b/api/models/acl.py @@ -11,6 +11,7 @@ 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 SoftDeleteMixin class App(Model): @@ -34,6 +35,7 @@ class UserQuery(BaseQuery): authenticated = user.check_password(password) else: authenticated = False + return user, authenticated def authenticate_with_key(self, key, secret, args, path): @@ -45,6 +47,7 @@ class UserQuery(BaseQuery): authenticated = True else: authenticated = False + return user, authenticated def search(self, key): @@ -55,18 +58,21 @@ class UserQuery(BaseQuery): 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): +class User(CRUDModel, SoftDeleteMixin): __tablename__ = 'users' # __bind_key__ = "user" query_class = UserQuery @@ -107,9 +113,7 @@ class User(CRUDModel): def _set_password(self, password): self._password = hashlib.md5(password.encode('utf-8')).hexdigest() - password = db.synonym("_password", - descriptor=property(_get_password, - _set_password)) + password = db.synonym("_password", descriptor=property(_get_password, _set_password)) def check_password(self, password): if self.password is None: diff --git a/api/views/acl/__init__.py b/api/views/acl/__init__.py index e3c651d..380474e 100644 --- a/api/views/acl/__init__.py +++ b/api/views/acl/__init__.py @@ -1,3 +1 @@ # -*- coding:utf-8 -*- - -__author__ = 'pycook' diff --git a/api/views/acl/resource.py b/api/views/acl/resources.py similarity index 96% rename from api/views/acl/resource.py rename to api/views/acl/resources.py index 91f9c84..3b7bae8 100644 --- a/api/views/acl/resource.py +++ b/api/views/acl/resources.py @@ -3,6 +3,7 @@ from flask import request from api.lib.decorator import args_required +from api.lib.perm.acl import validate_app from api.lib.perm.acl.resource import ResourceCRUD from api.lib.perm.acl.resource import ResourceGroupCRUD from api.lib.utils import get_page @@ -15,6 +16,7 @@ class ResourceView(APIView): url_prefix = ("/resources", "/resources/") @args_required('app_id') + @validate_app def get(self): page = get_page(request.values.get("page", 1)) page_size = get_page_size(request.values.get("page_size")) @@ -31,6 +33,7 @@ class ResourceView(APIView): @args_required('name') @args_required('type_id') @args_required('app_id') + @validate_app def post(self): name = request.values.get('name') type_id = request.values.get('type_id') @@ -57,6 +60,7 @@ class ResourceView(APIView): class ResourceGroupView(APIView): url_prefix = ("/resource_groups", "/resource_groups/") + @validate_app def get(self): page = get_page(request.values.get("page", 1)) page_size = get_page_size(request.values.get("page_size")) @@ -73,6 +77,7 @@ class ResourceGroupView(APIView): @args_required('name') @args_required('type_id') @args_required('app_id') + @validate_app def post(self): name = request.values.get('name') type_id = request.values.get('type_id') diff --git a/api/views/acl/role.py b/api/views/acl/role.py index ca59e5a..6015d37 100644 --- a/api/views/acl/role.py +++ b/api/views/acl/role.py @@ -3,6 +3,7 @@ from flask import request from api.lib.decorator import args_required +from api.lib.perm.acl import validate_app from api.lib.perm.acl.role import RoleCRUD from api.lib.perm.acl.role import RoleRelationCRUD from api.lib.utils import get_page @@ -14,6 +15,7 @@ class RoleView(APIView): url_prefix = ("/roles", "/roles/") @args_required('app_id') + @validate_app def get(self): page = get_page(request.values.get("page", 1)) page_size = get_page_size(request.values.get("page_size")) @@ -32,6 +34,7 @@ class RoleView(APIView): @args_required('name') @args_required('app_id') + @validate_app def post(self): name = request.values.get('name') app_id = request.values.get('app_id') diff --git a/api/views/acl/user.py b/api/views/acl/user.py index 9e0d576..e564535 100644 --- a/api/views/acl/user.py +++ b/api/views/acl/user.py @@ -6,8 +6,8 @@ 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.perm.acl.user import UserCRUD from api.lib.utils import get_page from api.lib.utils import get_page_size from api.resource import APIView @@ -36,11 +36,17 @@ class UserView(APIView): id2parents = RoleRelationCRUD.get_parents(uids=[i.uid for i in users]) + users = [i.to_dict() for i in users] + for u in users: + u.pop('password', None) + u.pop('key', None) + u.pop('secret', None) + return self.jsonify(numfound=numfound, page=page, page_size=page_size, id2parents=id2parents, - users=[i.to_dict() for i in users]) + users=users) @args_required('username') @args_required('email') @@ -58,3 +64,15 @@ class UserView(APIView): UserCRUD.delete(uid) return self.jsonify(uid=uid) + + +class UserResetKeySecretView(APIView): + url_prefix = "/users/reset_key_secret" + + def post(self): + key, secret = UserCRUD.reset_key_secret() + + return self.jsonify(key=key, secret=secret) + + def put(self): + return self.post()