From cb0eb7eadd8a6df6fe5dd59b7d0d486e25d95e40 Mon Sep 17 00:00:00 2001 From: pycook Date: Wed, 13 Nov 2019 13:25:42 +0800 Subject: [PATCH] update acl --- Dockerfile | 12 +-- api/lib/perm/acl/cache.py | 31 ++++++- api/lib/perm/acl/permission.py | 41 ++++++++ api/lib/perm/acl/resource.py | 3 + api/lib/perm/acl/role.py | 165 +++++++++++++++++++-------------- api/views/__init__.py | 2 +- api/views/acl/permission.py | 40 ++++++++ api/views/acl/role.py | 23 +++++ docker-compose.yml | 2 +- 9 files changed, 239 insertions(+), 80 deletions(-) create mode 100644 api/lib/perm/acl/permission.py create mode 100644 api/views/acl/permission.py diff --git a/Dockerfile b/Dockerfile index 939b012..a01ae41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,19 +18,19 @@ COPY --from=builder /data/apps/cmdb-ui/dist /etc/nginx/html/ # ================================= API ================================ -FROM centos:7.6.1810 AS cmdb-api +FROM python:3.7-alpine AS cmdb-api -LABEL description="Python2.7.5,cmdb" +LABEL description="Python3.7,cmdb" COPY . /data/apps/cmdb WORKDIR /data/apps/cmdb -RUN yum install -y epel-release && yum clean all \ - && yum install -y python-pip \ - && pip install --no-cache-dir -r docs/requirements.txt \ +RUN apk add --no-cache gcc musl-dev libffi-dev + +RUN pip install --no-cache-dir -r docs/requirements.txt \ && cp ./api/settings.py.example ./api/settings.py \ && sed -i "s#{user}:{password}@127.0.0.1:3306/{db}#cmdb:123456@mysql:3306/cmdb#g" api/settings.py \ && sed -i "s/127.0.0.1/redis/g" api/settings.py -CMD ["bash", "-c", "flask run"] +CMD ["bash", "-c", "flask run"] \ No newline at end of file diff --git a/api/lib/perm/acl/cache.py b/api/lib/perm/acl/cache.py index 0de541f..030f4d2 100644 --- a/api/lib/perm/acl/cache.py +++ b/api/lib/perm/acl/cache.py @@ -1,6 +1,7 @@ # -*- coding:utf-8 -*- from api.extensions import cache +from api.models.acl import Permission from api.models.acl import Role @@ -46,8 +47,8 @@ class RoleRelationCache(object): def get_parent_ids(cls, rid): parent_ids = cache.get(cls.PREFIX_PARENT.format(rid)) if not parent_ids: - from api.lib.perm.acl.role import RoleCRUD - parent_ids = RoleCRUD.get_parent_ids(rid) + from api.lib.perm.acl.role import RoleRelationCRUD + parent_ids = RoleRelationCRUD.get_parent_ids(rid) cache.set(cls.PREFIX_PARENT.format(rid), parent_ids, timeout=0) return parent_ids @@ -56,8 +57,8 @@ class RoleRelationCache(object): def get_child_ids(cls, rid): child_ids = cache.get(cls.PREFIX_CHILDREN.format(rid)) if not child_ids: - from api.lib.perm.acl.role import RoleCRUD - child_ids = RoleCRUD.get_child_ids(rid) + from api.lib.perm.acl.role import RoleRelationCRUD + child_ids = RoleRelationCRUD.get_child_ids(rid) cache.set(cls.PREFIX_CHILDREN.format(rid), child_ids, timeout=0) return child_ids @@ -89,3 +90,25 @@ class RoleRelationCache(object): cache.delete(cls.PREFIX_PARENT.format(rid)) cache.delete(cls.PREFIX_CHILDREN.format(rid)) cache.delete(cls.PREFIX_RESOURCES.format(rid)) + + +class PermissionCache(object): + PREFIX_ID = "Permission::id::{0}" + PREFIX_NAME = "Permission::name::{0}" + + @classmethod + def get(cls, key): + perm = cache.get(cls.PREFIX_ID.format(key)) + perm = perm or cache.get(cls.PREFIX_NAME.format(key)) + if perm is None: + perm = Permission.get_by_id(key) + perm = perm or Permission.get_by(name=key, first=True, to_dict=False) + if perm is not None: + cache.set(cls.PREFIX_ID.format(key), perm) + + return perm + + @classmethod + def clean(cls, key): + cache.delete(cls.PREFIX_ID.format(key)) + cache.delete(cls.PREFIX_NAME.format(key)) diff --git a/api/lib/perm/acl/permission.py b/api/lib/perm/acl/permission.py new file mode 100644 index 0000000..c06a09a --- /dev/null +++ b/api/lib/perm/acl/permission.py @@ -0,0 +1,41 @@ +# -*- coding:utf-8 -*- + + +from api.lib.perm.acl.cache import PermissionCache +from api.lib.perm.acl.cache import RoleCache +from api.models.acl import RolePermission + + +class PermissionCRUD(object): + @staticmethod + def get_all(resource_id=None, group_id=None): + result = dict() + if resource_id is not None: + perms = RolePermission.get_by(resource_id=resource_id, to_dict=False) + else: + perms = RolePermission.get_by(group_id=group_id, to_dict=False) + + for perm in perms: + result.setdefault((perm.rid, RoleCache.get(perm.rid).name), []).append( + PermissionCache.get(perm.perm_id).to_dict()) + + return result + + @staticmethod + def grant(rid, perms, resource_id=None, group_id=None): + for perm in perms: + perm = PermissionCache.get(perm) + existed = RolePermission.get_by(rid=rid, perm_id=perm.id, group_id=group_id, resource_id=resource_id) + existed or RolePermission.create(rid=rid, perm_id=perm.id, group_id=group_id, resource_id=resource_id) + + @staticmethod + def revoke(rid, perms, resource_id=None, group_id=None): + for perm in perms: + perm = PermissionCache.get(perm) + existed = RolePermission.get_by(rid=rid, + perm_id=perm.id, + group_id=group_id, + resource_id=resource_id, + first=True, + to_dict=False) + existed and existed.soft_delete() diff --git a/api/lib/perm/acl/resource.py b/api/lib/perm/acl/resource.py index 6800820..9826d65 100644 --- a/api/lib/perm/acl/resource.py +++ b/api/lib/perm/acl/resource.py @@ -156,3 +156,6 @@ 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/role.py b/api/lib/perm/acl/role.py index 76f4462..8bbc5e0 100644 --- a/api/lib/perm/acl/role.py +++ b/api/lib/perm/acl/role.py @@ -1,6 +1,7 @@ # -*- coding:utf-8 -*- +import six from flask import abort from api.extensions import db @@ -16,75 +17,16 @@ from api.tasks.acl import role_rebuild class RoleRelationCRUD(object): - pass - - -class RoleCRUD(object): @staticmethod - def search(q, app_id, page=1, page_size=None): - query = db.session.query(Role).filter(Role.deleted.is_(False)).filter( - Role.app_id == app_id).filter(Role.uid.is_(None)) - if q: - query = query.filter(Role.name.ilike('%{0}%'.format(q))) - - numfound = query.count() - - return numfound, query.offset((page - 1) * page_size).limit(page_size) - - @staticmethod - def add_role(name, app_id, is_app_admin=False, uid=None): - Role.get_by(name=name, app_id=app_id) and abort(400, "Role <{0}> is already existed".format(name)) - - return Role.create(name=name, - app_id=app_id, - is_app_admin=is_app_admin, - uid=uid) - - @staticmethod - def update_role(rid, **kwargs): - role = Role.get_by_id(rid) or abort(404, "Role <{0}> does not exist".format(rid)) - - RoleCache.clean(rid) - - return role.update(**kwargs) - - @classmethod - def delete_role(cls, rid): - role = Role.get_by_id(rid) or abort(404, "Role <{0}> does not exist".format(rid)) - - parent_ids = cls.get_parent_ids(rid) - child_ids = cls.get_child_ids(rid) - - for i in RoleRelation.get_by(parent_id=rid, to_dict=False): - i.soft_delete() - for i in RoleRelation.get_by(child_id=rid, to_dict=False): - i.soft_delete() - - for i in RolePermission.get_by(rid=rid, to_dict=False): - i.soft_delete() - - role_rebuild.apply_async(args=(parent_ids + child_ids,), queue=ACL_QUEUE) - - RoleCache.clean(rid) - RoleRelationCache.clean(rid) - - role.soft_delete() - - @staticmethod - def get_resources(rid): - res = RolePermission.get_by(rid=rid, to_dict=False) - id2perms = dict(id2perms={}, group2perms={}) + def get_parents(rids): + 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: - if i.resource_id: - id2perms['id2perms'].setdefault(i.resource_id, []).append(i.perm.name) - elif i.group_id: - id2perms['group2perms'].setdefault(i.group_id, []).append(i.perm.name) + id2parents.setdefault(i.child_id, []).append(RoleCache.get(i.parent_id).to_dict()) - return id2perms - - @staticmethod - def get_group_ids(resource_id): - return [i.group_id for i in ResourceGroupItems.get_by(resource_id, to_dict=False)] + return id2parents @staticmethod def get_parent_ids(rid): @@ -126,12 +68,99 @@ class RoleCRUD(object): return all_child_ids + @staticmethod + def add(parent_id, child_id): + RoleRelation.get_by(parent_id=parent_id, child_id=child_id) and abort(400, "It's already existed") + + return RoleRelation.create(parent_id=parent_id, child_id=child_id) + + @staticmethod + def delete(_id): + existed = RoleRelation.get_by_id(_id) or abort(400, "RoleRelation <{0}> does not exist".format(_id)) + + existed.soft_delete() + + @staticmethod + def delete2(parent_id, child_id): + existed = RoleRelation.get_by(parent_id=parent_id, child_id=child_id) + existed or abort(400, "RoleRelation < {0} -> {1} > does not exist".format(parent_id, child_id)) + + existed.soft_delete() + + +class RoleCRUD(object): + @staticmethod + def search(q, app_id, page=1, page_size=None): + query = db.session.query(Role).filter(Role.deleted.is_(False)).filter( + Role.app_id == app_id).filter(Role.uid.is_(None)) + if q: + query = query.filter(Role.name.ilike('%{0}%'.format(q))) + + numfound = query.count() + + return numfound, query.offset((page - 1) * page_size).limit(page_size) + + @staticmethod + def add_role(name, app_id, is_app_admin=False, uid=None): + Role.get_by(name=name, app_id=app_id) and abort(400, "Role <{0}> is already existed".format(name)) + + return Role.create(name=name, + app_id=app_id, + is_app_admin=is_app_admin, + uid=uid) + + @staticmethod + def update_role(rid, **kwargs): + role = Role.get_by_id(rid) or abort(404, "Role <{0}> does not exist".format(rid)) + + RoleCache.clean(rid) + + return role.update(**kwargs) + + @classmethod + def delete_role(cls, rid): + role = Role.get_by_id(rid) or abort(404, "Role <{0}> does not exist".format(rid)) + + parent_ids = RoleRelationCRUD.get_parent_ids(rid) + child_ids = RoleRelationCRUD.get_child_ids(rid) + + for i in RoleRelation.get_by(parent_id=rid, to_dict=False): + i.soft_delete() + for i in RoleRelation.get_by(child_id=rid, to_dict=False): + i.soft_delete() + + for i in RolePermission.get_by(rid=rid, to_dict=False): + i.soft_delete() + + role_rebuild.apply_async(args=(parent_ids + child_ids,), queue=ACL_QUEUE) + + RoleCache.clean(rid) + RoleRelationCache.clean(rid) + + role.soft_delete() + + @staticmethod + def get_resources(rid): + res = RolePermission.get_by(rid=rid, to_dict=False) + id2perms = dict(id2perms={}, group2perms={}) + for i in res: + if i.resource_id: + id2perms['id2perms'].setdefault(i.resource_id, []).append(i.perm.name) + elif i.group_id: + id2perms['group2perms'].setdefault(i.group_id, []).append(i.perm.name) + + return id2perms + + @staticmethod + def get_group_ids(resource_id): + return [i.group_id for i in ResourceGroupItems.get_by(resource_id, to_dict=False)] + @classmethod def has_permission(cls, rid, resource_name, perm): resource = Resource.get_by(name=resource_name, first=True, to_dict=False) resource = resource or abort(403, "Resource <{0}> is not in ACL".format(resource_name)) - parent_ids = cls.recursive_parent_ids(rid) + parent_ids = RoleRelationCRUD.recursive_parent_ids(rid) group_ids = cls.get_group_ids(resource.id) @@ -154,7 +183,7 @@ class RoleCRUD(object): resource = Resource.get_by(name=resource_name, first=True, to_dict=False) resource = resource or abort(403, "Resource <{0}> is not in ACL".format(resource_name)) - parent_ids = cls.recursive_parent_ids(rid) + parent_ids = RoleRelationCRUD.recursive_parent_ids(rid) group_ids = cls.get_group_ids(resource.id) perms = [] diff --git a/api/views/__init__.py b/api/views/__init__.py index 4d8a2ee..c507414 100644 --- a/api/views/__init__.py +++ b/api/views/__init__.py @@ -32,6 +32,6 @@ register_resources(os.path.join(HERE, "cmdb"), rest) # acl -blueprint_acl_v1 = Blueprint('cmdb_acl_v1', __name__, url_prefix='/api/v1/acl') +blueprint_acl_v1 = Blueprint('acl_api_v1', __name__, url_prefix='/api/v1/acl') rest = Api(blueprint_acl_v1) register_resources(os.path.join(HERE, "acl"), rest) diff --git a/api/views/acl/permission.py b/api/views/acl/permission.py new file mode 100644 index 0000000..e750712 --- /dev/null +++ b/api/views/acl/permission.py @@ -0,0 +1,40 @@ +# -*- coding:utf-8 -*- + + +from flask import request + +from api.lib.decorator import args_required +from api.lib.perm.acl.permission import PermissionCRUD +from api.lib.utils import handle_arg_list +from api.resource import APIView + + +class ResourcePermissionView(APIView): + url_prefix = ("/resources//permissions", "/resource_groups//permissions") + + def get(self, resource_id=None, group_id=None): + return self.jsonify(PermissionCRUD.get_all(resource_id, group_id)) + + +class RolePermissionGrantView(APIView): + url_prefix = ('/roles//resources//grant', + '/roles//resource_groups//grant') + + @args_required('perms') + def post(self, rid, resource_id=None, group_id=None): + perms = handle_arg_list(request.values.get("perms")) + PermissionCRUD.grant(rid, perms, resource_id=resource_id, group_id=group_id) + + return self.jsonify(rid=rid, resource_id=resource_id, group_id=group_id, perms=perms) + + +class RolePermissionRevokeView(APIView): + url_prefix = ('/roles//resources//revoke', + '/roles//resource_groups//revoke') + + @args_required('perms') + def post(self, rid, resource_id=None, group_id=None): + perms = handle_arg_list(request.values.get("perms")) + PermissionCRUD.revoke(rid, perms, resource_id=resource_id, group_id=group_id) + + return self.jsonify(rid=rid, resource_id=resource_id, group_id=group_id, perms=perms) diff --git a/api/views/acl/role.py b/api/views/acl/role.py index 9a2b5cc..ca59e5a 100644 --- a/api/views/acl/role.py +++ b/api/views/acl/role.py @@ -4,6 +4,7 @@ from flask import request from api.lib.decorator import args_required from api.lib.perm.acl.role import RoleCRUD +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 @@ -21,9 +22,12 @@ class RoleView(APIView): numfound, roles = RoleCRUD.search(q, app_id, page, page_size) + id2parents = RoleRelationCRUD.get_parents([i.id for i in roles]) + return self.jsonify(numfound=numfound, page=page, page_size=page_size, + id2parents=id2parents, roles=[i.to_dict() for i in roles]) @args_required('name') @@ -46,3 +50,22 @@ class RoleView(APIView): RoleCRUD.delete_role(rid) return self.jsonify(rid=rid) + + +class RoleRelationView(APIView): + url_prefix = "/roles//parents" + + @args_required('parent_id') + def post(self, child_id): + parent_id = request.values.get('parent_id') + res = RoleRelationCRUD.add(parent_id, child_id) + + return self.jsonify(res.to_dict()) + + @args_required('parent_id') + def delete(self, child_id): + parent_id = request.values.get('parent_id') + + RoleRelationCRUD.delete2(parent_id, child_id) + + return self.jsonify(parent_id=parent_id, child_id=child_id) diff --git a/docker-compose.yml b/docker-compose.yml index cf47042..61519ca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -72,4 +72,4 @@ volumes: db-data: networks: - new: + new: \ No newline at end of file