-
Notifications
You must be signed in to change notification settings - Fork 104
Description
RFC: cnpmcore 私有包访问控制
- Status: Draft
- Date: 2026-03-10
1. 背景与动机
cnpmcore 当前缺乏包级别的访问控制。现有 isPrivate 字段仅标记包来源(用户发布 vs 上游同步),所有 GET 端点无鉴权,任何人都可读取所有包。
企业场景中,需要将部分包限制为仅特定团队可访问。例如:
@mycompany/open-sdk— 公开包,所有人可读@mycompany/internal-sdk— 仅 core 团队可访问@mycompany/frontend-utils— 仅 frontend 团队可访问
这要求访问控制的粒度在 Team × Package 级别,而非简单的 Org 级别。
目标
- 每个包独立设置
access: public | restricted - 引入 Organization → Team → Package 三层权限模型(对齐 npm 官方)
- restricted 包仅被授权的 Team 成员 / Admin / Maintainer 可读
- 未登录返回 401,已登录无权限返回 403
- public 包行为完全不变,零额外开销
非目标(本期不做)
- 搜索结果过滤 restricted 包(后续 PR)
- 写权限通过 Team 控制(现有
ensurePublishAccess通过 Maintainer 机制已够用) - Team 级别的
read-writevsread-only区分(本期 Team 只控制读权限,写权限仍由 Maintainer 控制)
业务约束
-
仅 registry 私有包支持发包
- 只有
registry_id = 'self'(本地发布)的包才能由普通用户发布和管理 access - 从上游(如 npmjs.org)同步的包
registry_id不是self,这些包不可被普通用户发布或覆盖
- 只有
-
公网同步包仅 Admin 可应急发布
- 来自上游注册表的包(
registry_id !== 'self')只有 Admin 可以应急发布新版本 - 普通用户无法通过
npm publish覆盖公网同步包
- 来自上游注册表的包(
-
同 scope 下私有包与公网包共存
- 例如
@cnpm/public-utils已从 npmjs.org 同步(registry_id !== 'self'),同时@cnpm也是私有 Org 的 scope - 此时
@cnpm/public-utils不受私有 access 控制影响,因为它的registry_id不是self - 用户不能以私有包身份重新发布
@cnpm/public-utils(registry_id不匹配,拒绝覆盖) - 但用户可以发布新的私有包如
@cnpm/internal-sdk(registry_id = 'self'),并设为 restricted
- 例如
2. 整体设计
2.1 数据模型
参照 npm 官方权限模型,采用 Org → Team → Package 三层结构:
┌─────────────────────────────────────────────────────────────┐
│ Org ("mycompany") │
│ ⟺ scope "@mycompany" │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Team: developers │ │ Team: core │ ... │
│ │ (默认团队) │ │ │ │
│ │ │ │ │ │
│ │ Members: │ │ Members: │ │
│ │ - alice (owner) │ │ - alice │ │
│ │ - bob │ │ - charlie │ │
│ │ - charlie │ │ │ │
│ │ │ │ Packages: │ │
│ │ Packages: │ │ - internal-sdk │ │
│ │ - open-sdk │ │ - core-lib │ │
│ │ - frontend-utils│ │ │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Package: @mycompany/open-sdk access=public → 所有人可读
Package: @mycompany/frontend-utils access=restricted → developers team 成员可读
Package: @mycompany/internal-sdk access=restricted → core team 成员可读
Package: @mycompany/core-lib access=restricted → core team 成员可读
ER 关系:
Org ──1:N──▶ OrgMember ──▶ User (谁属于这个组织)
Org ──1:N──▶ Team (组织下的团队)
Team ──1:N──▶ TeamMember ──▶ User (谁属于这个团队,必须先是 OrgMember)
Team ──N:N──▶ TeamPackage ──▶ Package (团队可以访问哪些包)
2.2 developers 默认团队
参照 npm 行为:
- 创建 Org 时自动创建
developersTeam - 新成员加入 Org 时自动加入
developersTeam developersTeam 不可删除- 在 Org scope 下新发布 restricted 包时,自动授权给
developersTeam
这样的默认行为保证了:
- 简单场景(不需要细粒度控制)只需创建 Org,所有成员通过 developers team 自动获得访问权限
- 需要细粒度控制时,创建额外的 Team,将特定包只授权给特定 Team
2.3 读取权限判断流程
+---------------------------+
| ensurePackageReadAccess() |
+---------------------------+
|
v
+------------------------+ +--------+
| pkg.access == "public"?|--Y->| PASS | <-- public 包,零开销
+------------------------+ +--------+
| N
v
+------------------------+ +-----------+
| Authorization header |--N->| 401 |
| 有用户? | | 未登录 |
+------------------------+ +-----------+
| Y
v
+------------------------+ +--------+
| user 是 Admin? |--Y->| PASS |
+------------------------+ +--------+
| N
v
+------------------------+ +--------+
| user 是 Maintainer? |--Y->| PASS | <-- 查 maintainers 表
+------------------------+ +--------+
| N
v
+------------------------+
| pkg 有 scope? |--N-+
+------------------------+ |
| Y |
v |
+------------------------+ |
| 查 team_packages 表 | |
| WHERE package_id = ? | |
| 得到 teamId 列表 | |
+------------------------+ |
| |
v |
+---------------------------+ |
| 查 team_members 表 | |
| WHERE team_id IN (...) | |
| AND user_id = ? | |
+---------------------------+ |
| |
v |
+------------------------+ | +--------+
| 找到匹配记录? |--Y--->| PASS |
+------------------------+ | +--------+
| N |
v |
+----------+ |
| 403 |<----------+
| 无权限 |
+----------+
注意:Team 权限查询不使用 JOIN,而是分两步单表查询:
- 先查
team_packages拿到 teamId 列表- 再查
team_members判断用户是否属于其中任一 team
2.4 读取链路鉴权位置
权限检查在缓存之前。ShowPackageController 先查 DB 获取 Package 实体做权限判断,再走缓存逻辑。
public 包: 直接跳过(零开销)
|
+----------+ +--------------+-+ +----+ +----------+
| Request |-->| findPackage() |-->|Auth|-->| Cache/DB |--> Response
+----------+ +----------------+ +----+ +----------+
|
| restricted 包:
| 401 未登录
| 403 无权限
v
Error Response
3. PR 拆分
| PR | 内容 | 依赖 |
|---|---|---|
| PR1 | Org + Team 数据层 + 管理 API | 无 |
| PR2 | Package.access + Team 授权 + 读取链路鉴权 | PR1 |
| PR3 | 搜索结果过滤 restricted 包 | PR2 |
4. PR1: Org + Team 数据层 + 管理 API
4.1 实体设计
Org
| 字段 | 类型 | 说明 |
|---|---|---|
| orgId | string | 唯一标识 |
| name | string | 组织名,对应 scope 去掉 @(如 mycompany) |
| description | string | 描述 |
OrgMember(组织成员,控制"谁属于组织"及组织管理权限)
| 字段 | 类型 | 说明 |
|---|---|---|
| orgMemberId | string | 唯一标识 |
| orgId | string | 所属组织 |
| userId | string | 用户 ID |
| role | 'owner' | 'member' |
owner 可管理组织和团队 |
Team(团队,权限分配的核心单元)
| 字段 | 类型 | 说明 |
|---|---|---|
| teamId | string | 唯一标识 |
| orgId | string | 所属组织 |
| name | string | 团队名(如 developers, core) |
| description | string | 描述 |
TeamMember(团队成员,必须先是 OrgMember)
| 字段 | 类型 | 说明 |
|---|---|---|
| teamMemberId | string | 唯一标识 |
| teamId | string | 所属团队 |
| userId | string | 用户 ID |
TeamPackage(团队 ↔ 包授权关系)
| 字段 | 类型 | 说明 |
|---|---|---|
| teamPackageId | string | 唯一标识 |
| teamId | string | 团队 ID |
| packageId | string | 包 ID |
关于 read-only / read-write:npm 官方区分
read-only和read-write。本期简化为只控制读权限(TeamPackage 记录存在即表示该 Team 可读该 Package)。写权限仍通过已有的 Maintainer 机制控制。后续如需 Team 级别写权限控制,在 TeamPackage 上加permissions字段即可。
4.2 DDL
CREATE TABLE IF NOT EXISTS orgs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
gmt_create DATETIME(3) NOT NULL,
gmt_modified DATETIME(3) NOT NULL,
org_id VARCHAR(24) NOT NULL,
name VARCHAR(214) NOT NULL,
description VARCHAR(10240) DEFAULT NULL,
UNIQUE KEY uk_org_id (org_id),
UNIQUE KEY uk_name (name)
);
CREATE TABLE IF NOT EXISTS org_members (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
gmt_create DATETIME(3) NOT NULL,
gmt_modified DATETIME(3) NOT NULL,
org_member_id VARCHAR(24) NOT NULL,
org_id VARCHAR(24) NOT NULL,
user_id VARCHAR(24) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'member',
UNIQUE KEY uk_org_member_id (org_member_id),
UNIQUE KEY uk_org_id_user_id (org_id, user_id),
KEY idx_user_id (user_id)
);
CREATE TABLE IF NOT EXISTS teams (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
gmt_create DATETIME(3) NOT NULL,
gmt_modified DATETIME(3) NOT NULL,
team_id VARCHAR(24) NOT NULL,
org_id VARCHAR(24) NOT NULL,
name VARCHAR(214) NOT NULL,
description VARCHAR(10240) DEFAULT NULL,
UNIQUE KEY uk_team_id (team_id),
UNIQUE KEY uk_org_id_name (org_id, name),
KEY idx_org_id (org_id)
);
CREATE TABLE IF NOT EXISTS team_members (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
gmt_create DATETIME(3) NOT NULL,
gmt_modified DATETIME(3) NOT NULL,
team_member_id VARCHAR(24) NOT NULL,
team_id VARCHAR(24) NOT NULL,
user_id VARCHAR(24) NOT NULL,
UNIQUE KEY uk_team_member_id (team_member_id),
UNIQUE KEY uk_team_id_user_id (team_id, user_id),
KEY idx_user_id (user_id)
);
CREATE TABLE IF NOT EXISTS team_packages (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
gmt_create DATETIME(3) NOT NULL,
gmt_modified DATETIME(3) NOT NULL,
team_package_id VARCHAR(24) NOT NULL,
team_id VARCHAR(24) NOT NULL,
package_id VARCHAR(24) NOT NULL,
UNIQUE KEY uk_team_package_id (team_package_id),
UNIQUE KEY uk_team_id_package_id (team_id, package_id),
KEY idx_package_id (package_id)
);
-- Package 表新增列
ALTER TABLE packages ADD COLUMN access VARCHAR(20) DEFAULT NULL
COMMENT 'package access level: public or restricted';4.3 Repository 层
OrgRepository
| 方法 | 说明 |
|---|---|
findOrgByName(name) |
scope → org 查找 |
saveOrg(org) / removeOrg(orgId) |
CRUD |
saveMember(member) |
Upsert(已存在则更新 role) |
removeMember(orgId, userId) |
删除成员(同时清理其所有 TeamMember) |
listMembers(orgId) |
JOIN User 表 |
TeamRepository
| 方法 | 说明 |
|---|---|
findTeam(orgId, teamName) |
查找团队 |
listTeamsByOrgId(orgId) |
列出 org 下所有团队 |
saveTeam(team) / removeTeam(teamId) |
CRUD(删除时级联清理 members + packages) |
addMember(teamId, userId) / removeMember(teamId, userId) |
管理团队成员 |
listMembers(teamId) |
JOIN User 表 |
addPackage(teamId, packageId) / removePackage(teamId, packageId) |
管理团队可访问的包 |
listPackages(teamId) |
列出团队可访问的包 |
hasPackageAccess(packageId, userId) |
读取链路核心查询 |
hasPackageAccess 实现思路(分步单表查询,不使用 JOIN):
async hasPackageAccess(packageId: string, userId: string): Promise<boolean> {
// Step 1: 查包被授权给了哪些 team
const teamPackages = await TeamPackage.find({ packageId });
if (teamPackages.length === 0) return false;
// Step 2: 查用户是否在其中任一 team
const teamIds = teamPackages.map(tp => tp.teamId);
const member = await TeamMember.findOne({
teamId: teamIds, // WHERE team_id IN (...)
userId,
});
return !!member;
}4.4 Service 层
OrgService
| 方法 | 说明 |
|---|---|
createOrg(cmd) |
创建 org + 自动创建 developers Team + 创建者设为 owner 并加入 developers |
removeOrg(orgId) |
删除 org + 级联删除所有 team / member / team_package |
addMember(orgId, userId, role) |
加入 org + 自动加入 developers Team |
removeMember(orgId, userId) |
退出 org + 自动从所有 Team 移除 |
TeamService
| 方法 | 说明 |
|---|---|
createTeam(orgId, name) |
创建团队(校验 org 存在) |
removeTeam(teamId) |
删除团队(developers 不可删除) |
addMember(teamId, userId) |
加入团队(校验用户是 OrgMember) |
removeMember(teamId, userId) |
从团队移除 |
grantPackageAccess(teamId, packageId) |
授权团队访问包 |
revokePackageAccess(teamId, packageId) |
撤销授权 |
4.5 管理 API
标注
🔌 npm CLI的端点与 npm CLI 命令兼容,可直接通过 npm 客户端调用。
其余为 cnpmcore 扩展 API,需通过 HTTP 客户端(如 curl)调用。
Org 管理 — 🔌 npm CLI 兼容(npm org)
| 端点 | Method | 权限 | npm CLI | 说明 |
|---|---|---|---|---|
/-/org |
PUT | Admin | — | 创建 org(自动创建 developers team) |
/-/org/:orgName |
GET | 登录用户 | — | 查看 org 信息 |
/-/org/:orgName |
DELETE | Admin | — | 删除 org |
/-/org/:orgName/member |
GET | 登录用户 | npm org ls <orgName> |
列出 org 成员 |
/-/org/:orgName/member |
PUT | Admin 或 org owner | npm org set <orgName> <user> [owner] |
添加成员(自动加入 developers) |
/-/org/:orgName/member/:username |
DELETE | Admin 或 org owner | npm org rm <orgName> <user> |
移除成员(自动从所有 team 移除) |
Team 管理 — 🔌 npm CLI 兼容(npm team)
| 端点 | Method | 权限 | npm CLI | 说明 |
|---|---|---|---|---|
/-/org/:orgName/team |
PUT | Admin 或 org owner | npm team create <scope:team> |
创建团队 |
/-/org/:orgName/team |
GET | 登录用户 | npm team ls <scope> |
列出 org 下所有团队 |
/-/org/:orgName/team/:teamName |
GET | 登录用户 | — | 查看团队详情 |
/-/org/:orgName/team/:teamName |
DELETE | Admin 或 org owner | npm team destroy <scope:team> |
删除团队(developers 不可删) |
/-/org/:orgName/team/:teamName/member |
GET | 登录用户 | npm team ls <scope:team> |
列出团队成员 |
/-/org/:orgName/team/:teamName/member |
PUT | Admin 或 org owner | — | 添加团队成员 |
/-/org/:orgName/team/:teamName/member/:username |
DELETE | Admin 或 org owner | — | 移除团队成员 |
Team ↔ Package 授权 — 🔌 npm CLI 部分兼容(npm access)
| 端点 | Method | 权限 | npm CLI | 说明 |
|---|---|---|---|---|
/-/org/:orgName/team/:teamName/package |
GET | 登录用户 | npm access ls-packages <scope:team> |
列出团队可访问的包 |
/-/org/:orgName/team/:teamName/package |
PUT | Admin 或 org owner | npm access grant read-only <scope:team> <pkg> |
授权团队访问包 |
/-/org/:orgName/team/:teamName/package/:fullname |
DELETE | Admin 或 org owner | npm access revoke <scope:team> <pkg> |
撤销授权 |
5. PR2: Package.access + 读取链路鉴权
5.1 Package.access 字段
// app/common/constants.ts
export enum PackageAccess {
public = 'public',
restricted = 'restricted',
}Package 实体新增 access 字段,默认 'public'。已有包 access 为 NULL,实体层默认为 public,无需数据迁移。
5.2 PackageAccessService
核心方法 ensureReadAccess,判断流程见 2.3 流程图。
class PackageAccessService {
// 读权限检查(详细流程见 2.3)
async ensureReadAccess(pkg: Package, user?: User): Promise<void>;
// 设置包的 access 级别(仅 registry_id=self 的包可操作)
async setPackageAccess(pkg: Package, access: PackageAccess): Promise<void>;
}Team 权限查询不使用 JOIN:hasPackageAccess 分两步单表查询(见 4.3),restricted 包访问量不高,单包查询性能可接受。
5.3 发布约束与自动授权
发布流程:
npm publish --access restricted
|
v
+---------------------------+
| 解析 scope & name |
+---------------------------+
|
v
+---------------------------+ +----------------------------+
| 包已存在? |--Y->| registry_id == "self"? |
+---------------------------+ +----------------------------+
| N | Y | N
v v v
+---------------------------+ +-----------+ +----------------+
| 新包: registry_id = "self" | | 正常发布 | | user 是 Admin? |
| access = cmd.access | +-----------+ +----------------+
+---------------------------+ | Y | N
| v v
v +-----------+ +--------+
+---------------------------+ | 应急发布 | | 403 |
| access == "restricted" | +-----------+ | 拒绝 |
| && scope 有对应 Org? | +--------+
+---------------------------+
| Y
v
+---------------------------+
| 自动授权给 developers Team |
| TeamPackage.create( |
| developersTeam, pkg) |
+---------------------------+
发布约束:
- 只有
registry_id = 'self'的包可由普通用户发布(npm publish) - 公网同步包(
registry_id !== 'self')仅 Admin 可应急发布新版本 - 如果一个包名已从上游同步(
registry_id !== 'self'),普通用户不能以私有包身份覆盖发布
自动授权: 当在 Org scope 下发布 restricted 包时,自动将包授权给 developers Team。这保证了简单场景下(只有一个 developers team)的开箱即用。
5.4 加鉴权的端点(8 个 Controller)
| Controller | 方法 | 鉴权时机 |
|---|---|---|
| ShowPackageController | show() |
缓存之前 findPackage + 检查 |
| ShowPackageVersionController | show() |
showPackageVersionManifest 返回 pkg 后 |
| DownloadPackageVersionTar | download() |
SyncMode.all NFS 快速路径之前 + 正常流程中 |
| PackageVersionFileController | #getPackageVersion() |
覆盖 listFiles() 和 raw() |
| PackageTagController | showTags() |
getPackageEntityByFullname 后 |
| AccessController | listCollaborators() |
findPackage 后 |
| DownloadController | showPackageDownloads() |
findPackage 后 |
| PackageBlockController | listPackageBlocks() |
getPackageEntityByFullname 后 |
5.5 新增 API — 🔌 npm CLI 兼容(npm access)
| 端点 | Method | 权限 | Body | npm CLI | 说明 |
|---|---|---|---|---|---|
/-/package/:fullname/access |
PUT | Maintainer 或 Admin | { access: "public" | "restricted" } |
npm access public/restricted <pkg> |
设置包访问级别 |
/-/package/:fullname/collaborators |
GET | 见 5.4 | — | npm access ls-collaborators <pkg> |
列出包协作者 |
注意: 只有
registry_id = 'self'的包才能修改 access。公网同步包不允许设置 access。
6. 典型使用场景
场景 1:简单模式(只用 developers team)
# 1. Admin 创建 org(HTTP API)
curl -X PUT https://registry.example.com/-/org \
-H "Authorization: Bearer <admin-token>" \
-d '{ "name": "mycompany" }'
# → 自动创建 developers team
# 2. 添加成员(npm CLI 兼容)
npm org set mycompany alice --registry=https://registry.example.com
npm org set mycompany bob --registry=https://registry.example.com
# → alice, bob 自动加入 developers team
# 3. 发布 restricted 包(npm CLI 兼容)
npm publish --access restricted --registry=https://registry.example.com
# → @mycompany/sdk (registry_id=self) 自动授权给 developers team
# 4. alice 和 bob 可以 install,其他人 403
npm install @mycompany/sdk # ✅ alice/bob (在 developers team 中)
npm install @mycompany/sdk # ❌ charlie → 403场景 2:细粒度控制(多个 team)
# 1. 创建额外 team(npm CLI 兼容)
npm team create @mycompany:core --registry=https://registry.example.com
npm team create @mycompany:frontend --registry=https://registry.example.com
# 2. 分配成员到不同 team(HTTP API)
curl -X PUT https://registry.example.com/-/org/mycompany/team/core/member \
-d '{ "user": "alice" }'
curl -X PUT https://registry.example.com/-/org/mycompany/team/frontend/member \
-d '{ "user": "bob" }'
# 3. 设置包为 restricted 并调整 team 授权
npm access restricted @mycompany/internal-sdk --registry=https://registry.example.com
# 将 developers team 的默认授权移除,只给 core team
npm access revoke @mycompany:developers @mycompany/internal-sdk --registry=https://registry.example.com
npm access grant read-only @mycompany:core @mycompany/internal-sdk --registry=https://registry.example.com
# 结果:
# @mycompany/internal-sdk → 只有 core team (alice) 可读
# @mycompany/public-utils → developers team (alice + bob) 可读场景 3:同 scope 下私有包与公网同步包共存
# 背景:@cnpm/public-utils 已从 npmjs.org 同步到私有 registry
# registry_id = "https://registry.npmjs.org"(非 self)
# 同时 @cnpm 也是私有 Org 的 scope
# ❌ 尝试以私有包发布 @cnpm/public-utils — 被拒绝
npm publish # 当前目录的 package.json: { "name": "@cnpm/public-utils" }
# → 403: 该包已从上游同步,registry_id 不是 self,不允许覆盖
# ✅ 发布全新的私有包 @cnpm/internal-sdk — 成功
npm publish --access restricted # package.json: { "name": "@cnpm/internal-sdk" }
# → registry_id = self,正常发布为 restricted 包
# 两个包在同一 scope 下共存:
# @cnpm/public-utils → registry_id 非 self,public,任何人可读
# @cnpm/internal-sdk → registry_id = self,restricted,仅授权 team 可读7. 测试方案
7.1 Org + Team Service 测试
| # | 场景 | 预期 |
|---|---|---|
| 1 | 创建 org 自动创建 developers team | developers team 存在 |
| 2 | 添加 org member 自动加入 developers team | 新成员在 developers 中 |
| 3 | 移除 org member 自动从所有 team 移除 | team 中不再有该用户 |
| 4 | developers team 不可删除 | 抛出 ForbiddenError |
| 5 | 创建/删除自定义 team | 正常 CRUD |
| 6 | team member 必须先是 org member | 非 org 成员加入 team 抛错 |
7.2 PackageAccessService 测试
| # | 场景 | 预期 |
|---|---|---|
| 1 | public 包,无用户 | 通过 |
| 2 | public 包,任意用户 | 通过 |
| 3 | restricted 包,无用户 | 401 |
| 4 | restricted 包,admin | 通过 |
| 5 | restricted 包,maintainer | 通过 |
| 6 | restricted 包,属于有权 team 的成员 | 通过 |
| 7 | restricted 包,属于无权 team 的成员 | 403 |
| 8 | restricted 包,org member 但不在有权 team 中 | 403 |
| 9 | restricted 包,非 org 成员 | 403 |
注意 case 8:Org member 不等于有权限。必须属于被授权的 Team 才行。
7.3 Controller 集成测试
6 个端点 × 4 场景(public 200 / restricted 401 / team member 200 / stranger 403)
7.4 自动授权测试
| # | 场景 | 预期 |
|---|---|---|
| 1 | 在 org scope 下发布 restricted 包 | developers team 自动获得授权 |
| 2 | 发布 public 包 | 不创建 team_package 记录 |
8. 关键设计决策
8.1 三层模型 Org → Team → Package(非 Org → Package)
Org 成员不直接获得包访问权限,必须通过 Team 中转。
原因: 同 scope 下的包可能属于不同团队。@mycompany/internal-sdk 只给 core team,@mycompany/frontend-utils 只给 frontend team。Org 级别的成员关系粒度太粗。
简单场景不受影响: developers 默认 team + 自动授权机制保证了不需要细粒度控制时零配置开箱即用。
8.2 Org 与 scope 约定关联,不加外键
org.name = "mycompany" 对应 scope = "@mycompany",通过命名约定关联。
原因: 解耦。已有 Scope 模型只做注册表映射,不适合耦合权限语义。
8.3 access 是包维度
同 scope 不同包可以有不同 access。@mycompany/open-sdk 可以 public,@mycompany/internal-sdk 可以 restricted。
8.4 NULL access = public
已有包 access 为 NULL,实体层默认为 'public'。无需数据迁移,向后完全兼容。
8.5 public 包零开销
ensurePackageReadAccess() 第一行判断 access 字段,public 包不做任何认证查询。
8.6 权限检查在缓存之前
风险: 每次请求多一次 findPackage DB 查询。
缓解: public 包第一行就返回。restricted 包访问量低,多一次查询可接受。
8.7 401 vs 403
| 状态码 | 含义 | 场景 |
|---|---|---|
| 401 | 未认证 | 访问 restricted 包但未提供 Authorization header |
| 403 | 无权限 | 已登录但不在有权限的 team 中 |
8.8 本期不区分 read-only / read-write
npm 官方 TeamPackage 有 read-only 和 read-write 两种权限。本期简化为 TeamPackage 记录存在即可读。写权限仍通过已有的 Maintainer 机制控制。
扩展方式: 后续在 team_packages 表加 permissions VARCHAR(20) 字段,默认 'read-only',即可支持。
8.9 access 控制仅作用于 registry 私有包
只有 registry_id = 'self' 的包才参与 access 控制。上游同步的包(registry_id !== 'self')始终视为 public,不受 restricted 限制。
原因: 同步包来自公网,其可见性不应受私有 Org 权限控制。同时防止用户通过设置 restricted 来"劫持"公网包名。
8.10 公网同步包仅 Admin 可应急发布
已从上游同步的包(registry_id !== 'self'),普通用户不能通过 npm publish 覆盖。仅 Admin 可应急发布。
原因: 防止普通用户意外或恶意覆盖公网包内容。Admin 应急发布用于修复紧急安全问题等场景。
8.11 OrgMember role 设计
| Role | 权限 |
|---|---|
| owner | 管理 org 和 team(增删成员、创建/删除 team、授权/撤销包访问) |
| member | 通过 team 读取 restricted 包 |
Admin 是全局角色,不在 OrgMember 中表达。
9. 向后兼容性
| 项目 | 影响 |
|---|---|
| 已有包 | access = NULL → 视为 public,行为不变 |
| 已有 API | 所有 GET 端点行为不变(public 包无额外开销) |
| 公网同步包 | registry_id !== 'self' 的包不受 access 控制影响,始终可读 |
| 发布行为 | 公网同步包仍不允许普通用户覆盖(已有行为),仅新增 Admin 应急发布能力 |
| 数据库 | 新增 5 张表 + 1 个 ALTER TABLE,无破坏性变更 |
| npm 客户端 | 核心操作兼容 npm org / npm team / npm access 命令 |
10. 文件清单
新增文件
| 文件 | 说明 |
|---|---|
app/core/entity/Org.ts |
Org 实体 |
app/core/entity/OrgMember.ts |
OrgMember 实体 |
app/core/entity/Team.ts |
Team 实体 |
app/core/entity/TeamMember.ts |
TeamMember 实体 |
app/core/entity/TeamPackage.ts |
TeamPackage 实体 |
app/repository/model/Org.ts |
Org ORM Model |
app/repository/model/OrgMember.ts |
OrgMember ORM Model |
app/repository/model/Team.ts |
Team ORM Model |
app/repository/model/TeamMember.ts |
TeamMember ORM Model |
app/repository/model/TeamPackage.ts |
TeamPackage ORM Model |
app/repository/OrgRepository.ts |
Org 数据访问层 |
app/repository/TeamRepository.ts |
Team 数据访问层 |
app/core/service/OrgService.ts |
Org 业务逻辑 |
app/core/service/TeamService.ts |
Team 业务逻辑 |
app/core/service/PackageAccessService.ts |
访问控制核心逻辑 |
app/port/controller/OrgController.ts |
Org 管理 API |
app/port/controller/TeamController.ts |
Team 管理 API |
修改文件
| 文件 | 变更 |
|---|---|
app/common/constants.ts |
新增 PackageAccess 枚举 |
app/core/entity/Package.ts |
加 access 字段 |
app/core/service/PackageManagerService.ts |
publish() 支持 access + 自动授权 developers team |
app/repository/model/Package.ts |
加 access 列 |
app/repository/PackageRepository.ts |
加 isPackageMaintainer() |
app/port/controller/AbstractController.ts |
加 ensurePackageReadAccess() |
app/port/controller/AccessController.ts |
加鉴权 + setAccess API |
app/port/controller/DownloadController.ts |
加鉴权 |
app/port/controller/PackageBlockController.ts |
加鉴权 |
app/port/controller/PackageTagController.ts |
加鉴权 |
app/port/controller/PackageVersionFileController.ts |
加鉴权 |
app/port/controller/package/DownloadPackageVersionTar.ts |
加鉴权 |
app/port/controller/package/SavePackageVersionController.ts |
传 access |
app/port/controller/package/ShowPackageController.ts |
加鉴权(缓存前) |
app/port/controller/package/ShowPackageVersionController.ts |
加鉴权 |
sql/ddl_mysql.sql |
加表 + 列 |
sql/ddl_postgresql.sql |
加表 + 列 |
sql/mysql/3.77.0.sql |
迁移文件 |
11. 已决策事项
| # | 问题 | 决策 |
|---|---|---|
| 1 | Org 创建权限 | 仅 Admin 可创建 |
| 2 | access 默认值 | 默认 public,不支持 Org 级别的 defaultAccess,除非用户发布时手动传 --access restricted |
| 3 | CDN 缓存 | 仅私有部署场景,不考虑 CDN 缓存问题 |
| 4 | npm CLI 兼容 | 核心 API(npm org, npm team, npm access)按 npm CLI 协议设计,cnpmcore 同时支持管理员维度的配置 |
| 5 | 已有包迁移 | 不在本次 RFC 范围内 |
| 6 | 性能 | 不使用 JOIN,改为分步单表查询(见 4.3)。restricted 包访问量不高,单包查询性能可接受 |
| 7 | 同步包 access 保护 | 只在 Service 层校验,不加 DB 约束 |