安全模型(开源自托管)
VIP / 限额
开源版不启用人数、个人项目数、团队项目数、版本数、AI 次数等 VIP 拦截(VIPRightsAspect 放行)。License 上传接口仍保留,但不作为功能门禁。
种子账号
| 账号 | 用途 | 默认密码 |
|---|---|---|
admin | 运维/手工 | 123456(dev 可用;prod 默认拒绝该口令) |
e2e0..e2e15 | Playwright 并发隔离(≤16 worker) | 123456 |
e2e-serial | chromium-serial 空态用例 | 123456 |
防护:
erd.security.e2e-accounts-enabled:dev=true,prod/默认=false → 生产拒绝e2e\\d+/e2e-serial登录erd.security.allow-demo-admin:dev=true,prod/默认=false → 生产拒绝用户名admin+ 种子口令123456(改密后不受影响);逃生阀ERD_ALLOW_DEMO_ADMIN=trueerd.security.allow-open-register:dev=true,prod/默认=false → 生产拒绝匿名开放注册;逃生阀ERD_ALLOW_OPEN_REGISTER=true- 公网部署应改密
admin、删除e2e*用户;见 deployment.md
SQL 执行
设计器内 JDBC/同步走登录会话与(团队)审批路径;开源版 VIP 权益切面不拦截次数。
| 路径 | 门禁 |
|---|---|
queryInfo/exec、queryInfo/explain | SqlGuard.assertReadOnly:单语句 SELECT / EXPLAIN / SHOW / DESC(jsqlparser + 词首回退);禁多语句、GRANT/OUTFILE 等 |
connector/sqlexec、dbsync | mutate:允许同步所需 DDL/DML;SqlGuard.assertMutateAllowed 拒 GRANT/REVOKE/CREATE USER/INTO OUTFILE/LOAD DATA LOCAL 等;强制 dataSourceId(applyMutate,拒 raw JDBC+账密) |
| `connector/ping | dbReverse* |
FE 热路径(已保存数据源):ping / dbReverse* / sqlexec / dbsync 传 dataSourceId,客户端不附带 url/username/password(preferDataSourceIdPayload)。无 id 的 raw JDBC 仅留给 ping / dbReverse*(试连与新建未保存 UX);sqlexec/dbsync 后端硬拒。
JDBC 驱动来源:仅默认发布镜像内置的四型(MySQL/MariaDB、PostgreSQL、Oracle、SQL Server)——MySQL 是应用自举必需的系统驱动,其余三型是官方连接器包(目标状态与 backend 应用本体的编译依赖解耦,只在镜像构建阶段合并,行为对使用者无差异);不存在任何运行时上传驱动 jar 的接口(管理员角色也没有)——未来新库类型走部署期镜像 tag / 挂载目录扩展,详见 ADR-0029「驱动管理」章节。这是刻意的架构选择,不是待补的功能缺口:拒绝上传入口直接消灭了"任意 jar = RCE"这类攻击面,不需要额外签名/沙箱机制来兜底。
匿名放行(前缀剥离后路径)
- 登录/退出:
/login、/auth/login、/exit - 会话 JWT TTL:默认
JWT_EXPIRES_IN=604800(7 天);HS256,存前端localStorage,无 UI refresh / 服务端吊销会话票。公网可收紧。公开 API 的 OAuth access(默认 1h)+ refresh 是另一平面,见下。 - 注册:仅
/project/group/user/register(前端POST /ncnb/project/group/user/register);受erd.security.allow-open-register门控(dev=true,prod/默认=false);重复入口/user/register已去 HTTP 映射并不再 ignore - 第三方 IdP 联邦(ADR-0021):
/federate/providers、/federate/{github|google|wechat}、/federate/{github|google|wechat}/callback、/federate/session(及/auth/federate/…前缀形态);不含/federate/links/**(须会话 JWT) - 只读分享:仅
GET /share/{token}(及/ncnb/share/{token}前缀剥离前形态),见 ADR-0007;create/revoke/fork不在 ignore-urls(需登录) - Actuator:
/actuator/**放行,但 exposure 仅health,info;health不show-details;info仅 app name/version(无密钥)。禁止扩大到 env/beans/heapdump - OpenAPI / Swagger UI:Security 仍对
/v3/api-docs/**、/swagger-ui/**(及/webjars/**)匿名放行;唯一门控为springdoc.api-docs.enabled/springdoc.swagger-ui.enabled(prod=false)。本地/dev 默认开启。已无martin.swagger/martin.resource-server配置键。
第三方登录 IdP 联邦(ADR-0021)
- 方向:GitHub / Google / 微信 → 本系统会话 JWT(与账密登录同构);不是 ADR-0013 对外 IdP(PAT/
erd_oat_) - GitHub:OAuth App;scope=
read:user user:email;subject=数字id;已验证邮箱可按邮箱绑定 - Google:Authorization Code + OIDC(
openid email profile);须email_verified;subject=sub;可按邮箱绑定已有用户 - 微信:开放平台网站应用扫码(
snsapi_login);subject 优先unionid否则openid;不做公众号网页授权 - 存储:
user_identity_link(仅 subject;无 token);短票 Rediserd:federate:session:*(回调后换票,JWT 不进 URL) - 开关:
GITHUB_*/GOOGLE_*/WECHAT_*三项齐全才启用;缺则/auth/federate/providers对应false,启动不崩溃 - 建号:无链接且无邮箱匹配时走
erd.security.allow-open-register;关闭则提示先账密登录再绑定 - UI:登录页条件按钮;
/account/settings?selectKey=security绑定/解绑;落地/login/federate?ticket=;登出与账密同源(清 JWT) - 明确不做:回潮
/login/success、/account/settings/wechat、/auth/oauth2/**、password-grant
只读分享
- 创建/吊销需登录且为项目创建人(UI:设计器顶栏「分享」弹层)
- 匿名响应按 ADR-0008 清空
profile.dbs(连接只在data_sources);可保留defaultDataSourceId引用
公开 API PAT / OAuth(ADR-0013)
- PAT 铸造:会话 JWT →
POST /auth/personal-access-tokens(前缀剥离后/personal-access-tokens);明文erd_pat_…仅响应一次 - OAuth 切片 A(M2M):会话 JWT →
POST /auth/oauth-clients注册 confidential(默认;client_id=erd_cli_…,client_secret=erd_cs_…仅响应一次)→ 匿名POST /oauth/token(grant_type=client_credentials,body 或 Basic)→access_token=erd_oat_…(默认 TTL 3600s,ERD_PUBLIC_API_OAUTH_TTL)。列表/吊销:GET|DELETE /auth/oauth-clients。吊销 client 使未过期 OAT 与未消费 auth code 失效。 - OAuth 切片 B(浏览器 + PKCE):注册时可选
clientType=public|confidential+redirectUris[](精确匹配;仅 https 或 localhost/127.0.0.1/[::1])。产品同意页/oauth/authorize(AuthBrandShell)。GET /oauth/authorize(须会话 JWT)→ 同意预览 JSON(client 名 / scopes / redirect host;不签发 code)。POST … decision=allow→ 302(或Accept: application/json→{redirect_to})?code=erd_ac_…&state=;decision=deny→error=access_denied。匿名POST /oauth/tokengrant_type=authorization_code+code+redirect_uri+code_verifier(public 无 secret;confidential 须 secret)→erd_oat_…+erd_ort_…(refresh)。auth code 仅 SHA-256、默认 TTL 120s(ERD_PUBLIC_API_OAUTH_CODE_TTL)、单次消费。未注册 redirect 永不 302。 - OAuth refresh(post-MVP):仅
authorization_code换票签发 refresh(client_credentials不发)。POST /oauth/tokengrant_type=refresh_token→ 轮换(旧erd_ort_立即吊销,新 access+refresh 同family_id);提交已吊销 refresh → 整族 OAT/ORT 吊销 +invalid_grant。TTL:ERD_PUBLIC_API_OAUTH_REFRESH_TTL(默认 2592000s / 30 天)。POST /oauth/revoke(token + client 认证;RFC 7009 未知票仍 200)。吊销 client → 未过期 OAT/ORT + 未消费 code 一并失效。 - OIDC(RS256 + JWKS)+ nonce/at_hash:
GET /.well-known/openid-configuration(issuer=ERD_OIDC_ISSUER或ERD_UI_URL);id_tokenRS256(ERD_OIDC_RSA_PRIVATE_KEY/_PATH/ PKCS12;prod fail-fast;与JWT_SECRET分离;已废ERD_OIDC_HMAC);GET /.well-known/jwks.json发布公钥(kid)。授权码/refresh 且 scope 含openid→ 换票附id_token;client_credentials不发。authorize 可选nonce→ 绑定 auth code → code 换票回显;refresh 续期 id_token 不含 nonce(OIDC Core §12.2)。at_hash按同响应 access_token 计算(SHA-256 左半 + base64url)。GET /oauth/userinfo:Bearererd_oat_+openid(拒 PAT/会话 JWT)。 - 存储:表
personal_access_token/oauth_api_client/oauth_access_token/oauth_authorization_code/oauth_refresh_token仅哈希 + hint;禁止明文入库存仓 - 调用:
Authorization: Bearer erd_pat_…或erd_oat_…→/api/v1/**(独立 SecurityFilterChain;不接受会话 JWT / refresh)。client_credentialsOAT 以注册人身份;authorization_code/ refresh 续期的 OAT 以授权用户身份。 - Scope(已解锁):默认
projects:read、versions:read;可显式铸造projects:write、versions:write、openid(OAuth 注册同PatScopes;换票scope⊆ 客户端) - 只读项目:
GET /api/v1/projects、GET /api/v1/projects/{id}需projects:read+project_user成员;详情projectJSON清空profile.dbs(ADR-0008) - 只读版本:
GET /api/v1/projects/{id}/versions、…/versions/{versionId}需versions:read+ 成员;列表不含projectJSON;详情清空profile.dbs - 提交版本:
POST /api/v1/projects/{id}/versions需versions:write+ 成员;bodyprojectJSON/snapshot;写入前清空profile.dbs;仅 insert(忽略客户端 id);会话 JWT 不接受 - 写项目:
PATCH /api/v1/projects/{id}(元数据)与PUT /api/v1/projects/{id}/projectJSON需projects:write+ 成员;PUT 写入前清空profile.dbs - MCP:仓库
mcp/经 PAT(或等价 OAT)调上列 REST;stdio / Streamable HTTP;写 tools:create_version(versions:write)、update_project/put_project_json(projects:write)。见mcp/README.md - 限流:默认 60/min/token(
ERD_PUBLIC_API_RATE_LIMIT);Redisson 集群共享;超限 429;Redis 不可用 fail-closed → 503(读写共用) - 后置:
第三方 IdP 联邦→ 见 ADR-0021;不因换票/吊销/userinfo/discovery 匿名口放宽 CORS - 产品 UI:
/account/settings?selectKey=personalAccessTokens— 列表 / 铸造(scopes + 可选过期)/ 明文一次揭示 / 吊销/account/settings?selectKey=oauthClients— 列表 / 注册(含openid)/ 复制client_id/client_secret创建时一次揭示(不可再查看)/ 吊销/oauth/authorize— OAuth 同意(client / scopes / redirect host + Allow/Deny)