gitlab是一个代码管理平台,在公司内部多使用它来进行代码托管服务。gitlab提供给我们的一个非常实用的功能就是对项目的访问需要进行权限审查,每个人只能看到自己有权限查看的项目。
在gitlab-ce源码的Gitlab::Access module中定义了项目的所有访问级别如下:
NO_ACCESS = 0
GUEST = 10
REPORTER = 20
DEVELOPER = 30
MAINTAINER = 40
# @deprecated
MASTER = MAINTAINER
OWNER = 50
而数据库的project_authorizations表中维护了用户和项目之间的访问权限关系,它的结构为:
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可以访问的所有项目:
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),获取当前用户可以访问的所有组,包括任何嵌套组和任何共享组。
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如下:
#前面方法引用该方法时传入的参数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