前言
之前发了一篇GitLab被恶意注册,所以关闭注册功能,被恶意创建了很多用户和项目,当时未找到批量删除的方法,后续操作用户和项目实在是不方便,得找半天,通过搜索,找到了可以通过GitLab API批量创建删除用户和项目。
这里直接介绍解决教程,有关GitLab api的讲解以后单独写一篇。
一、原理说明
GitLab官方提供了RESTful API,可以通过接口的方式管理GitLab。
官方文档提供了非常详细的说明文档,API部分有具体的参数说明,使用curl访问示例,json返回结果示例。
REST API:https://docs.gitlab.com/api/rest/
Projects API:https://docs.gitlab.com/api/projects/
Users API:https://docs.gitlab.com/api/users/
项目可见性分为私有、内部、公共,用户权限分为普通和管理员,除了访问公共项目,其他的访问都需要一个用户的令牌,普通用户可以查询,管理员用户可以增删改查。
所以流程就很简单了,登录一个管理员账号,创建个人token令牌,使用任意一种语言或工具,带着令牌访问REST API,查询要删除的用户列表,再遍历删除,用户创建的项目会同步删除。
二、创建访问令牌
登录管理员账号,点击右上角个人中心,点击设置
点击左侧的Access Tokens设置个人令牌,name随便写一个,Expires at是过期时间,可以不写,再下面的Scopes是权限范围,必须选一个,由于我们需要删除用户,勾选api和read_user权限,最后点击创建按钮
创建完成后,会提示创建成功,以及Token值,注意下面的提示:
Make sure you save it - you won’t be able to access it again.
意思是,确保你保存了它——你将无法再次访问它,点击右边的复制按钮,发送到微信或者是保存到txt文件,以防忘记,不过忘了再创建一个就好了
往下可以看到激活的个人令牌,点击Revoke可以撤销制定令牌
一开始以为是sudo权限,在ApiPost种测试了一下不行,提示需要read_user和api权限
说明一下其他几个选项,感觉read_user和sudo描述不准确
权限名称 | 权限范围 |
---|---|
api | 对所有的Groups(组)和Projects(项目)的读/写权限 |
read_user | 对所有用户信息的只读权限 |
sudo | 管理员权限,包括所有操作 |
read_repository | 通过Git-over-HTTP对私有项目的只读权限 |
三、批量删除用户和项目
实现方式可以选择在命令行中使用curl,或者使用python、Java、js等语言,我比较推荐使用python,方便编写和处理查询列表,也便于保存,如果没有的话使用命令行就行,毕竟是一次性的操作。
打开PyCharm,创建新项目
创建后如下图,右键项目名称,创建一个子项目目录
就叫批量删除用户吧,然后回车
右键子目录,创建一个python文件
也叫批量删除用户,然后回车
由于没有什么复杂的操作,所以只需要安装一个requests库,点击下面的Termianl,输入pip install requests
,之前将下载源换成了阿里的,这里就不讲怎么换了
处理代码如下,注释很详细,下面代码有问题,后续会说,不要复制这个
import requests
# 配置GitLab地址
GITLAB_URL = "http://10.10.10.10"
# 配置Token令牌
PRIVATE_TOKEN = "_dmeNrhyNymzbvSyNj6j"
# 这里用删除2024年1月1日以后创建的用户示范,还有多种查询参数,有特殊要求可以在查询结果中再筛选
def main():
# Token令牌要放在header中,在这里统一配置,参数名必须大写
headers = {"PRIVATE-TOKEN": PRIVATE_TOKEN}
# 第一步:查询2024-01-01之后创建的所用用户,分页大小为100
response_total = requests.get(
f"{GITLAB_URL}/api/v4/users",
# 查询时Token要放在header中
headers = headers,
params = {
"created_after": "2024-01-01",
"per_page": 100
}
)
# 判断所用用户查询是否成功
if response_total.status_code != 200:
print(f"所有用户查询失败: {response_total.status_code}:{response_total.text}")
return
# 获取需处理总用户数和总页数
total_users = int(response_total.headers.get('X-Total', 1))
total_pages = int(response_total.headers.get('X-Total-Pages', 1))
print(f"总用户数: {total_users},总页数: {total_pages}")
# 第二步:遍历分页
for page in range(1, total_pages + 1):
print(f"正在处理第 {page}/{total_pages} 页")
# 在全部查询中加上分页号的参数
response_users = requests.get(
f"{GITLAB_URL}/api/v4/users",
headers = headers,
params = {
"created_after": "2024-01-01",
"per_page": 100,
"page": page
}
)
# 判断分页查询是否成功
if response_users.status_code != 200:
print(f"获取第页{page}用户失败: {response_users.status_code}:{response_users.text}")
continue
# 第三步:批量删除当前页用户,遍历当前分页的每一个用户,这里可以再额外筛选
for user in response_users.json():
response_delete = requests.delete(
f"{GITLAB_URL}/api/v4/users/{user['id']}",
headers = headers
# 删除用户时Token也可以放在参数里,但是参数名必须小写,在ApiPost中只能在参数小写,header中大小写都不行,奇怪
# params = {"private_token": PRIVATE_TOKEN}
)
# 删除用户成功则返回204且无内容
if response_delete.status_code == 204:
# 如果是删除几万条用户就不用打印正常日志了
print(f"已删除用户: {user['username']}")
else:
print(f"删除失败 id:{user['id']} {user['username']},[{response_delete.status_code}]:{response_delete.text}")
if __name__ == "__main__":
main()
点击这个绿色的小三角运行程序
日志如下
第一个问题,很低级的错误,删除数据经典错误,又不小心写出来了,留着鞭策自己
删除数据要从最后一页或一行开始删除,因为删除了第一页,之前的序号相当于都往前一个,此时再删除第二页,删除的其实是原来的第三页,原来的第二页就被跳过了,理论上每次都会跳过一页,所以只会删除一半,这里能到第18页是因为第二个问题。
解决方案有三种,一种是使用while,循环查第一页,直到没有数据,第二种是从最后一页开始删除,再往前遍历,还有一种省心的,提前创建一个数组,把每次查询到的user收集起来,最后再遍历数组删除,会多花费一点内存
数据量不大选第三种最好,这里我会选择第一种,更加通用一些
第二个问题,GitLab在删除用户时,如果用户有关联信息,则会执行soft deleted,即软删除,会把该用户的相关信息,移动到一个叫Ghost User的用户下
在用户管理处,正常应该在2FA Disabled里面,搜索ghost,就可以看到这个用户
解决方案是加一个hard_delete参数,值设为true,可以看到默认为soft deleted,
hard_delete的描述翻译:如果为真,则通常会被移动到 Ghost 用户的贡献将被删除,同时也会删除该用户拥有的所有组。
第三个问题,可以看到虽然用户删除到了770,但是组也增加到了804,可能是GitLab的暂存机制吧,这里增加的组是暂时的,过一会儿就好了
重来一遍,加上hard_delete参数,修改第三步删除用户的请求,删除正常日志
# 第三步:批量删除当前页用户,遍历当前分页的每一个用户,这里可以再额外筛选
for user in response_users.json():
response_delete = requests.delete(
f"{GITLAB_URL}/api/v4/users/{user['id']}",
headers = headers,
# 删除用户时Token也可以放在参数里,但是参数名必须小写,在ApiPost中只能在参数小写,header中大小写都不行,奇怪
# params = {"private_token": PRIVATE_TOKEN}
params = {"hard_delete": "true"}
)
# 删除用户成功则返回204且无内容
if response_delete.status_code != 204:
print(f"删除失败 id:{user['id']} {user['username']},[{response_delete.status_code}]:{response_delete.text}")
由于没有打印正常日志,看不出来,我看着控制台可以看出来1234页是慢慢删除的,5678是一秒出来的,应该还剩300多个还可以再测试一次
删除的数量差是380,groups增加了435,奇怪
几分钟的功夫,还退回来20个用户,groups少了64,先不管了
在ApiPost中测试一个超过4的页数,可以看到返回结果是一个"[]"
在分页查询结果处加个判断,打印日志并跳过
# 判断分页查询是否成功
if response_users.status_code != 200:
print(f"获取第页{page}用户失败: {response_users.status_code}:{response_users.text}")
continue
elif response_users.text == "[]":
print(f"获取第页{page}用户失败: 无数据")
continue
不错,这次终于解决删除问题了
按理说删除200个,user少了241,groups少了200,怀疑是GitLab的统计接口的问题
ApiPost中查询还剩下112个,正确无误
四、最终python代码
改写查询逻辑,不通过分页页数查询,采用while循环查询,全部代码都贴上来,第一步可以选择不要
import requests
# 配置GitLab地址
GITLAB_URL = "http://10.10.10.10"
# 配置Token令牌
PRIVATE_TOKEN = "_dmeNrhyNymzbvSyNj6j"
# 这里用删除2024年1月1日以后创建的用户示范,还有多种查询参数,有特殊要求可以在查询结果中再筛选
def main():
# Token令牌要放在header中,在这里统一配置,参数名必须大写
headers = {"PRIVATE-TOKEN": PRIVATE_TOKEN}
# 第一步:查询2024-01-01之后创建的所用用户,分页大小为100
response_total = requests.get(
f"{GITLAB_URL}/api/v4/users",
# 查询时Token要放在header中
headers = headers,
params = {
"created_after": "2024-01-01",
"per_page": 100
}
)
# 判断所用用户查询是否成功
if response_total.status_code != 200:
print(f"所有用户查询失败: {response_total.status_code}:{response_total.text}")
return
# 获取需处理总用户数和总页数
total_users = int(response_total.headers.get('X-Total', 1))
total_pages = int(response_total.headers.get('X-Total-Pages', 1))
print(f"总用户数: {total_users},总页数: {total_pages}")
# 第二步:循环查询第一页
page = 1
while True:
print(f"正在处理第 {page}/{total_pages} 页")
# 在全部查询中加上分页号的参数
response_users = requests.get(
f"{GITLAB_URL}/api/v4/users",
headers = headers,
params = {
"created_after": "2024-01-01",
"per_page": 100,
}
)
page += 1
# 判断分页查询是否成功
if response_users.status_code != 200:
print(f"获取第页{page}用户失败: {response_users.status_code}:{response_users.text}")
continue
elif response_users.text == "[]":
print(f"获取第页{page}用户失败: 无数据,查询结束")
break
# 第三步:批量删除当前页用户,遍历当前分页的每一个用户,这里可以再额外筛选
for user in response_users.json():
response_delete = requests.delete(
f"{GITLAB_URL}/api/v4/users/{user['id']}",
headers = headers,
# 删除用户时Token也可以放在参数里,但是参数名必须小写,在ApiPost中只能在参数小写,header中大小写都不行,奇怪
# params = {"private_token": PRIVATE_TOKEN}
params = {"hard_delete": "true"}
)
# 删除用户成功则返回204且无内容
if response_delete.status_code != 204:
print(f"删除失败 id:{user['id']} {user['username']},[{response_delete.status_code}]:{response_delete.text}")
if __name__ == "__main__":
main()
控制台日志如下,因为有找不到删除失败的,所以处理了4次,最终ApiPost查确认是没有了,可能是访问太快导致GitLab删除失败
GitLab里也正常,groups还在恢复中
过了一会儿都恢复正常了,到这里处理就结束了
如果想用shell脚本来处理可以自己转化一下,或者直接复制给ai,但肯定是没有python方便调试和运行
希望大家在解决问题之外也能有所收获,都看到这里了,点个关注吧~~~