Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/server/node/operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,9 @@ export function createOperatorApp(session?: OperatorSession, attemptsPerMinute =
if (session.source() != 'none') {
return json(409, { error: { code: serverErrorCode.operatorAlreadyConfigured, message: 'Operator authentication is already configured.' }, version: 1 })
}
if (!admitted()) return limited()
if (!(await session.setupAuthorized(context.req.raw))) {
return json(401, { error: { code: serverErrorCode.authenticationInvalid, message: 'Setup authorization or operator token is invalid.' }, version: 1 })
}
const token = await tokenRequest(context.req.raw, 'token')
if (token == null) return json(400, { error: { code: serverErrorCode.operatorInvalid, message: 'Setup request is invalid.' }, version: 1 })
const result = await session.claim(context, token)
Expand Down
51 changes: 51 additions & 0 deletions apps/server/test/host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,57 @@ it('claims an unconfigured deployment with a one-time setup session and restores
}
})

it.each(['valid', 'missing', 'tampered', 'expired'])('checks a %s setup session independently of anonymous attempt limits', async (kind) => {
const directory = await mkdtemp(path.join(tmpdir(), 'open-flow-setup-limit-'))
const file = path.join(directory, 'open-flow.sqlite')
const service = await openService(file)
const code = 'open-flow-setup-code-000000000000000001'
let now = Date.now()
const session = operator(file, undefined, false, code, () => now)
const app = createServerApp(service, { operator: session, operatorLoginAttemptsPerMinute: 1 })
try {
for (let attempt = 0; attempt < 3; attempt += 1) {
const unauthorized = await app.request('/auth/setup', {
body: JSON.stringify({ token, version: 1 }),
method: 'POST',
})
expect(unauthorized.status).toBe(401)
expect(unauthorized.headers.get('set-cookie')).toBeNull()
}
const authorized = await app.request('/auth/setup/session', {
body: JSON.stringify({ code, version: 1 }),
method: 'POST',
})
expect(authorized.status).toBe(200)
const cookies = authorized.headers.get('set-cookie') ?? ''
expect(cookies).toContain('open_flow_operator_setup=')
const cookie = cookies.split(';', 1)[0] ?? ''
const limited = await app.request('/auth/setup/session', {
body: JSON.stringify({ code, version: 1 }),
method: 'POST',
})
expect(limited.status).toBe(429)
expect(Number(limited.headers.get('retry-after'))).toBeGreaterThan(0)
if (kind == 'expired') now += 10 * 60_000
const claim = await app.request('/auth/setup', {
body: JSON.stringify({ token, version: 1 }),
headers: kind == 'missing' ? {} : { cookie: kind == 'tampered' ? `${cookie}tampered` : cookie },
method: 'POST',
})
expect(claim.status).toBe(kind == 'valid' ? 201 : 401)
expect(session.source()).toBe(kind == 'valid' ? 'settings' : 'none')
if (kind == 'valid') {
expect(claim.headers.get('set-cookie')).toContain('open_flow_operator_session=')
expect(await session.matches(token)).toBe(true)
} else {
expect(claim.headers.get('set-cookie')).toBeNull()
}
} finally {
await closeService(service)
await rm(directory, { force: true, recursive: true })
}
})

it('fixes one OOMOL Team when each Flow is created', async () => {
const directory = await mkdtemp(path.join(tmpdir(), 'open-flow-team-'))
const file = path.join(directory, 'open-flow.sqlite')
Expand Down
2 changes: 2 additions & 0 deletions docs/server/container-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ credential;不要让 reverse proxy access log、消息预览或分析工具采

没有 env-managed 或持久化 operator credential 时,health、callback 和已持久化的 runtime 工作仍可运行,但 Control API fail closed,Workbench 进入
setup。Operator 登录与 setup authorization 共享部署实例级限速,超过 `OPEN_FLOW_OPERATOR_LOGIN_ATTEMPTS_PER_MINUTE` 后返回 429 和 `Retry-After`。
`POST /auth/setup` 必须持有有效的 setup session;缺失、篡改或过期的 session 返回 401,不占用上述额度。已授权的认领操作不受该额度限制,
即使 setup authorization 刚好耗尽窗口额度,也能继续完成认领。

达到 `OPEN_FLOW_MAX_PENDING_RUNS` 后,新 Run admission 返回 429;已接受请求的幂等重放仍返回原 Run。Cron 与 Poll 保留当前调度位置并短暂重试。
Cron 所属 Flow 已有未终结 Run 时同样保留当前调度位置;前一个 Run 结束后只补入最早未处理 occurrence,并把下一次计划推进到当前时间之后。
Expand Down