gitlab是一个代码管理平台,在公司内部多使用它来进行代码托管服务。gitlab提供给咱们的一个很是实用的功能就是对项目的访问须要进行权限审查,每一个人只能看到本身有权限查看的项目。
在gitlab-ce源码的Gitlab::Access module中定义了项目的全部访问级别以下:git
NO_ACCESS = 0 GUEST = 10 REPORTER = 20 DEVELOPER = 30 MAINTAINER = 40 # @deprecated MASTER = MAINTAINER OWNER = 50
而数据库的project_authorizations表中维护了用户和项目之间的访问权限关系,它的结构为:sql
Column | Type | Collation | Nullable | Default --------------+---------+-----------+----------+--------- user_id | integer | | not null | project_id | integer | | not null | access_level | integer | | not null |
表结构比较简单,从表中能够很直接的看到用户对项目的访问权限级别。
gitlab用户查看项目时,显示的是该用户能够有权限看到项目,该业务逻辑被定义在gitlab/lib/gitlab/project_authorizations.rb类中,几个类间的部分关键关系如图:
类间关系以下:~~~~数据库
project_authorizations是调用主体,这个类的初始化方法为数组
# user - 用于计算受权的用户对象 def initialize(user) @user = user end
这个类的关键方法为calculate,返回的是user能够访问的全部项目:gitlab
def calculate # postgresql的cte技术能够理解为本身建立的一个临时视图, #你能够对它继续进行操做 # recursive_cte是该类的一个私有方法,下面会进行分析 cte = recursive_cte cte_alias = cte.table.alias(Group.table_name) #Arel是一个SQL AST管理器,AST全称Abstract Syntax Tree(抽象语法树),使用Arel能够更方便进行复制查询 #构建projects表的Arel projects = Project.arel_table links = ProjectGroupLink.arel_table relations = [ # 用户能够直接访问的项目 # 返回project_id和access_level user.projects.select_for_project_authorization, # 用户的我的项目 # 返回project_id和access_level user.personal_projects.select_as_maintainer_for_project_authorization, # 直接属于用户能够访问的任何组的项目 # namespaces存储用户及项目组的路径 Namespace .unscoped # unscoped方法将会过滤掉调用对象的where语句块 .select([alias_as_column(projects[:id], 'project_id'), cte_alias[:access_level]]) .from(cte_alias) .joins(:projects), # 与用户有权访问的任何名称空间共享的项目 Namespace .unscoped .select([ links[:project_id], least(cte_alias[:access_level], links[:group_access], 'access_level') ]) .from(cte_alias) .joins('INNER JOIN project_group_links ON project_group_links.group_id = namespaces.id') .joins('INNER JOIN projects ON projects.id = project_group_links.project_id') .joins('INNER JOIN namespaces p_ns ON p_ns.id = projects.namespace_id') .where('p_ns.share_with_group_lock IS FALSE') ] #ProjectAuthorization是对应于project_authorization表的model,下面会进行分析 ProjectAuthorization .unscoped .with .recursive(cte.to_arel) .select_from_union(relations) end
其中,recursive_cte是该类的一个私有方法,它的目的是构建一个递归CTE(Common Table Expressions),获取当前用户能够访问的全部组,包括任何嵌套组和任何共享组。post
def recursive_cte #RecursiveCTE是用于轻松构建递归CTE语句的类, #它的<<方法能够让咱们把查询的添加到里面的数组上, #最后执行to_arel方法合并全部添加的数组中的查询构造对应的CTE语句 cte = Gitlab::SQL::RecursiveCTE.new(:namespaces_cte) #构建members和namespaces的Arel members = Member.arel_table namespaces = Namespace.arel_table # 用户所属的命名空间 cte << user.groups .select([namespaces[:id], members[:access_level]]) .except(:order) if Feature.enabled?(:share_group_with_group) # 与任何组共享的命名空间 cte << Group.select([namespaces[:id], 'group_group_links.group_access AS access_level']) .joins(join_group_group_links) .joins(join_members_on_group_group_links) end # 用户所属的全部组的子组 cte << Group.select([ namespaces[:id], greatest(members[:access_level], cte.table[:access_level], 'access_level') ]) .joins(join_cte(cte)) .joins(join_members_on_namespaces) .except(:order) #返回构造好的CTE语句 cte end
calculate方法中的project_authorization是对应于前面显示的project_authorization表的model类,里面定义的类方法select_from_union以下:spa
#前面方法引用该方法时传入的参数relations #是多个对project_id和access_level查询语句 #里面包装的from_union能够对relations数组中的多个查询进行UNION def self.select_from_union(relations) from_union(relations) .select(['project_id', 'MAX(access_level) AS access_level']) .group(:project_id) end
# 生成一个查询,该查询使用FROM来使用UNION选择数据 # # 在UNION中使用FROM在过去能够产生更好的查询计划。所以,咱们一般建议使用这种模式,而不是使用WHERE IN。 # #例如: # users = User.from_union([User.where(id: 1), User.where(id: 2)]) # # 这将产生如下SQL查询: # # SELECT * # FROM ( # SELECT * # FROM users # WHERE id = 1 # # UNION # # SELECT * # FROM users # WHERE id = 2 # ) users; # # members -要在联合中使用的ActiveRecord::Relation对象的数组 # # remove_duplicates - 是否去重,默认为true # # alias_as - 别名,默认为当前表名 def from_union(members, remove_duplicates: true, alias_as: table_name) union = Gitlab::SQL::Union .new(members, remove_duplicates: remove_duplicates) .to_sql from(Arel.sql("(#{union}) #{alias_as}")) end