Harbor 本地数据库模式用户自助注册(DB Auth):测试用例 1-01 的完整走查与源码实现剖析
【免费下载链接】harborAn open source trusted cloud native registry project that stores, signs, and scans content.项目地址: https://gitcode.com/GitHub_Trending/ha/harbor
本篇技术文章围绕 Harbor 用户管理测试用例tests/testcases/Group1-user-management/1-01-DB-user-registration.md展开,完整复现“本地数据库认证模式(auth_mode: db_auth)下非管理员用户自助注册(Sign Up)”这一功能场景:从测试环境与前置条件、7 步手工测试流程、非法输入校验清单,到 Harbor 服务端注册鉴权、用户存在性检查、密码校验的源码级实现链路。读完后,你既能按文档独立执行该测试用例,也能理解背后db_auth与self_registration两个开关在源码中如何协同控制注册能力。
测试用例目标与适用场景
用例文件位于 1-01-DB-user-registration.md,其官方目标(Purpose)描述为:
To verify that a non-admin user can register an account (signup) when users are managed locally by Harbor (DB mode).
也就是说,该用例验证的核心命题是:当 Harbor 的用户体系完全由本地数据库托管(而非 LDAP/UAA/OIDC 等外部身份源)时,普通访客可以在 Harbor 首页自行注册账号,并用注册凭据完成 Web UI 登录与 Docker 客户端登录。
这一点在源码中有直接印证。Harbor 定义了四种认证模式常量,其中db_auth即本地数据库模式,见 src/common/const.go:
const ( DBAuth = "db_auth" LDAPAuth = "ldap_auth" UAAAuth = "uaa_auth" HTTPAuth = "http_auth" OIDCAuth = "oidc_auth" )只有db_auth模式下用户数据落在本地 PostgreSQL 数据库,用户自助注册才有意义;其他模式下账号由外部身份提供方管理。
测试环境与前置条件
用例文档的 Environment 一节列出三条硬性前提,测试执行前必须逐项确认:
- Harbor 实例已运行并可访问:需要一个完整部署的 Harbor(包含 portal、core、数据库等组件),且 Web UI 可正常打开;
- 认证模式为本地数据库:
auth_mode必须配置为db_auth,用户数据存储在本地数据库。对应源码中common.DBAuth = "db_auth"常量; - 客户端主机:一台安装了 Docker CLI 的 Linux 主机,用于第 6 步的
docker login验证。
在此之上,还有一个用例文档没有显式列出、但由源码决定测试能否成立的关键前置条件:self_registration(自助注册开关)必须为 true。该配置项在 src/lib/config/metadata/metadatalist.go 中定义:
{Name: common.SelfRegistration, Scope: UserScope, Group: BasicGroup, EnvKey: "SELF_REGISTRATION", DefaultValue: "false", ItemType: &BoolType{}, Editable: false, Description: `Whether the Harbor instance supports self-registration. If it''s set to false, admin need to add user to the instance.`},要点有三:
- 默认值为
false。若实例未开启自助注册,UI 首页不会出现可用的注册入口,本用例第 1 步“点击 Sign Up”即无法进行,因此执行用例前务必先确认该开关已打开; - 环境变量键为
SELF_REGISTRATION,部署编排层可据此注入; - 描述明确说明:关闭自助注册后,只能由管理员代为添加用户(对应同目录下用例 1-09-admin-create-delete-user.md 的场景)。
读取该开关的统一入口在 src/lib/config/userconfig.go:
// SelfRegistration returns the enablement of self registration func SelfRegistration(ctx context.Context) (bool, error) { return DefaultMgr().Get(ctx, common.SelfRegistration).GetBool(), nil }测试步骤完整走查
以下 7 步与用例文档一一对应,每一步都标注了其在服务端对应的位置,便于在测试失败时定位问题。
步骤 1-2:在首页点击 Sign Up 并填写注册信息
在 Harbor 首页点击 “Sign Up”,进入注册表单,填写用户名、邮箱、密码及确认密码。
注册表单提交前,前端会先调用“用户存在性检查”接口,实时校验用户名/邮箱是否已被占用。该检查的后端实现在 src/core/controllers/base.go 的UserExists:
// UserExists checks if user exists when user input value in sign in form. func (cc *CommonController) UserExists() { ctx := cc.Context() flag, err := config.SelfRegistration(ctx) if err != nil { log.Errorf("Failed to get the status of self registration flag, error: %v, disabling user existence check", err) } securityCtx, ok := security.FromContext(ctx) isAdmin := ok && securityCtx.IsSysAdmin() if !flag && !isAdmin { cc.CustomAbort(http.StatusPreconditionFailed, "self registration deactivated, only sysadmin can check user existence") } target := cc.GetString("target") value := cc.GetString("value") var query *q.Query switch target { case "username": query = q.New(q.KeyWords{"Username": value}) case "email": query = q.New(q.KeyWords{"Email": value}) } ... }可以看到:用户名和邮箱两条查询路径都受self_registration开关控制——开关关闭且请求者非系统管理员时,直接返回 412(Precondition Failed)。这正是“注册表单能实时提示用户名是否被占用”背后的机制。
步骤 3-5:用用户名、邮箱分别登录 UI
- 第 3 步:使用新注册用户的用户名登录 UI,验证成功后登出;
- 第 5 步:再次使用同一用户的邮箱登录 UI,确认邮箱同样可用作登录凭据,然后登出。
DB 模式下的认证逻辑在 src/core/auth/db/db.go 中实现:
// Auth implements Authenticator interface to authenticate user against DB. type Auth struct { auth.DefaultAuthenticateHelper userMgr user.Manager } // Authenticate calls dao to authenticate user. func (d *Auth) Authenticate(ctx context.Context, m models.AuthModel) (*models.User, error) { u, err := d.userMgr.MatchLocalPassword(ctx, m.Principal, m.Password) if err != nil { return nil, err } if u == nil { return nil, auth.NewErrAuth("Invalid credentials") } return u, nil }关键点:认证函数接收的是models.AuthModel.Principal(登录主体,即用户名或邮箱)与密码,交由userMgr.MatchLocalPassword在本地用户表中完成匹配;匹配不到则统一返回Invalid credentials,避免泄露“用户名不存在”与“密码错误”的差异。该认证器在包初始化时通过auth.Register(common.DBAuth, &Auth{userMgr: user.New()})注册到认证工厂(见 db.go),当实例auth_mode为db_auth时被选中。从源码结构看,用户名与邮箱两种主体走同一MatchLocalPassword匹配路径,这正是步骤 3 与步骤 5 预期都能登录成功的底层原因。
步骤 6:Docker 客户端登录验证(用户名与邮箱各验一次)
在装有 Docker CLI 的 Linux 主机上执行:
docker login <harbor_host>用例要求username 与 email 两种凭据都要验证通过(原文:verify both)。Docker Registry V2 的登录最终同样落到 Harbor core 的认证中间件,再经由当前认证模式(此处即db_auth)完成校验,因此只要 UI 登录成功,Docker 登录预期也应成功;若此步失败,应优先检查<harbor_host>的 TLS/证书配置与 443 端口可达性,而非用户数据本身。
步骤 7:非法输入校验(六类异常注册)
退出 UI 后注册第二个新用户,依次输入非法值,验证校验是否生效、错误提示是否合理。用例文档列出的六类非法输入为:
| 非法输入类型 | 预期行为 | 服务端对应校验点 |
|---|---|---|
| 用户名与已有用户重复 | 注册被拒绝,提示已占用 | UserExists 按Username查询返回占用提示 |
| 用户名长度过长 | 拒绝并提示长度超限 | 表单/后端字段长度校验 |
| 邮箱格式错误 | 拒绝并提示格式非法 | 邮箱格式校验 |
| 邮箱长度过长 | 拒绝并提示长度超限 | 字段长度校验 |
| 密码不符合密码规则 | 拒绝并提示规则要求 | 见下文“密码与 Secret 规则” |
| 两次密码输入不一致 | 拒绝并提示不匹配 | 确认密码一致性校验 |
预期结果(Expected Outcome)
用例文档给出的预期结果共四条,作为测试判定标准:
- 第 2 步成功创建新用户;
- 新用户可在步骤 3、5 分别用用户名、邮箱通过 UI 登录;
- 新用户可在步骤 6 用邮箱和用户名通过 Docker 客户端登录;
- 第 7 步的非法注册输入全部被拒绝,且界面能展示正确的错误信息。
源码纵深:注册能力的三重闸门
除了上述流程内联的校验点外,Harbor 对“创建用户”这个动作设置了多层闸门,理解它们有助于解释各类报错来源。
闸门一:认证模式必须为 db_auth
API 层创建用户的准入检查在 src/server/v2.0/handler/user.go 的requireCreatable:
func (u *usersAPI) requireCreatable(ctx context.Context) error { a, err := u.getAuth(ctx) ... if a != common.DBAuth { return errors.ForbiddenError(nil).WithMessagef("creating local user is not allowed under auth mode: %s", a) }非db_auth模式(LDAP/OIDC 等)下创建本地用户直接返回 403。这与用例前提“auth_mode 必须为 db_auth”互为印证。
闸门二:self_registration 开关与会话校验
requireCreatable的后半段(user.go)继续检查自助注册开关:
sr, err := config.SelfRegistration(ctx) ... accessErr := u.RequireSystemAccess(ctx, rbac.ActionCreate, rbac.ResourceUser) if !sr { return accessErr } if accessErr != nil && !lib.GetCarrySession(ctx) { return errors.ForbiddenError(nil).WithMessage("self-registration cannot be triggered via API") } return nil其语义分三层:
self_registration=false时,创建用户需要系统级Create权限——即只有管理员能加人,普通用户注册请求被拒;self_registration=true且请求者本身无创建权限时,还要求请求携带会话(来自 UI 的注册表单提交),防止绕过页面直接以匿名 API 方式批量刷用户;- 系统管理员创建用户则不受自助注册开关限制。
闸门三:密码/Secret 复杂度规则
Harbor 服务端对用户密码、随机生成的 CLI secret 等凭据统一执行复杂度规则,见 requireValidSecret:
func requireValidSecret(in string) error { hasLower := regexp.MustCompile(`[a-z]`) hasUpper := regexp.MustCompile(`[A-Z]`) hasNumber := regexp.MustCompile(`[0-9]`) if len(in) >= 8 && len(in) <= 128 && hasLower.MatchString(in) && hasUpper.MatchString(in) && hasNumber.MatchString(in) { return nil } return errors.BadRequestError(nil).WithMessage("the password or secret must be 8-128, inclusively, characters long with at least 1 uppercase letter, 1 lowercase letter and 1 number") }规则为:长度 8–128(含端点),且至少包含 1 个大写字母、1 个小写字母、1 个数字。用例第 7 步中“password input does not compliant to password rule”一类非法输入,触发的正是这类复杂度校验;编写测试数据时,可用“纯数字密码”“纯小写短密码”等样本快速命中该规则。
辅助观测点:systeminfo 与 exporter
- 前端需要知道“是否显示 Sign Up 入口”,答案来自系统信息接口:src/controller/systeminfo/controller.go 在响应模型中输出
SelfRegistration字段(controller.go 处从配置读取并安全转换),UI 据此决定是否展示注册按钮; - Prometheus exporter 同样暴露了这两个动态开关,见 src/pkg/exporter/system_collector.go 中
harbor_self_registration指标(以及harbor_auth_mode),排障时可直接curlmetrics 端点确认实例当前的auth_mode与self_registration值,无需登录 UI。
可能的干扰因素与排查建议
用例文档的 “Possible Problems” 标注为 None,即不存在已知的固有失败模式。但结合源码闸门,实际执行时最常见的“看似 bug 实为配置”的场景有两个:
- 首页看不到 Sign Up 或点击无效果:大概率是
self_registration为默认的false(见 metadatalist.go 的默认值定义)。先用 systeminfo 接口或 exporter 指标确认开关状态,再决定是否开启; - UI 登录成功但
docker login失败:认证链路相同(均为 db.go 的Authenticate),因此问题通常出在客户端到 Harbor 的网络/TLS 层面,而非账号数据。
小结与相关用例
本用例完整覆盖了 Harbor 本地数据库认证模式下“注册 → UI 登录(用户名/邮箱)→ Docker 登录(用户名/邮箱)→ 非法输入拒绝”的闭环,是验证db_auth与self_registration两个配置协同工作的基准用例。同组还有若干可衔接执行的用例,共同构成用户管理测试组:
- 1-03-DB-user-update-password.md:DB 模式下修改密码;
- 1-04-DB-user-update-account-settings.md:更新账户设置;
- 1-09-admin-create-delete-user.md:管理员创建/删除用户(对应
self_registration=false时的唯一加人途径); - 1-07-LDAP-mode-general.md:作为对照,了解非 DB 模式下认证行为的差异。
【免费下载链接】harborAn open source trusted cloud native registry project that stores, signs, and scans content.项目地址: https://gitcode.com/GitHub_Trending/ha/harbor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考