maestro/src/bridge/visibility.ts
oss-sync 29ccaf1e92
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (dfadcd5f)
2026-06-23 06:38:48 +00:00

145 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export interface VisibilityWhere {
clause: string;
params: unknown[];
}
export interface VisibilityOptions {
/**
* 行がスペースに属する場合の space_id 列名(例 `'space_id'`、spaces 自身は `'id'`)。
* 指定かつ非 admin のとき、可視性節に「スペースのメンバーなら閲覧可」の OR ブランチを足す。
* **コード制御のリテラルのみ**tableAlias と同じ信頼レベル)。ユーザー入力を渡してはならない。
*/
spaceColumn?: string;
}
export function buildVisibilityWhere(
user: Express.User,
tableAlias: string,
opts?: VisibilityOptions,
): VisibilityWhere {
if (user.role === 'admin') {
return { clause: '1=1', params: [] };
}
// Synthetic users (no-auth 'local' user, internal callers) may omit orgIds —
// treat as no org membership rather than throwing on `.length`.
const orgIds = user.orgIds ?? [];
const orgPlaceholders = orgIds.length > 0
? orgIds.map(() => '?').join(',')
: 'NULL';
// 既存の params 順は [user.id, ...user.orgIds]。membership の user.id は必ず末尾に足す(順序が重要)。
const params: unknown[] = [user.id, ...orgIds];
let membershipBranch = '';
if (opts?.spaceColumn) {
// スペース行の閲覧資格 = 明示メンバーspace_membersOR スペースの根オーナー
// spaces.owner_id。根オーナーは通常 space_members 行を持たないため、
// owner サブクエリを足さないと自分のスペースの「他メンバーの行」が見えないgap-1
// user.id を clause 出現順members → spacesに 2 回 push する。
membershipBranch =
` OR (${tableAlias}.${opts.spaceColumn} IN (SELECT space_id FROM space_members WHERE user_id = ?)` +
` OR ${tableAlias}.${opts.spaceColumn} IN (SELECT id FROM spaces WHERE owner_id = ?))`;
params.push(user.id, user.id);
}
return {
clause: `(
${tableAlias}.owner_id = ?
OR ${tableAlias}.visibility = 'public'
OR (${tableAlias}.visibility = 'org' AND ${tableAlias}.visibility_scope_org_id IN (${orgPlaceholders}))${membershipBranch}
)`.replace(/\s+/g, ' ').trim(),
params,
};
}
/**
* `spaces` テーブル専用の可視性 WHERE。通常の行可視性と決定的に違うのは
* **個人スペースkind='personal'は所有者本人にしか見えないadmin も例外でない)**
* という点。これがないと admin の listSpaces が他ユーザーの個人スペースを拾い、
* UI が「最初に見つかった personal」を自分のワークスペースと誤認する誤った
* members-owner / AGENTS.md / MCP / 設定で動く root cause
*
* - `kind='personal'` 行: `s.owner_id = ?`viewer.idのみ。role を問わない。
* - `kind='case'` 行: 従来の `buildVisibilityWhere`admin=1=1 / owner / org /
* public / space メンバーシップ OR ブランチ)をそのまま適用。
*
* param 順は SQL の出現順に合わせる: 先頭に personal-owner の viewer.id、続いて
* `buildVisibilityWhere` が返す paramsadmin のときは空)。`tableAlias` は呼び出し
* 側のコード制御リテラルのみbuildVisibilityWhere と同じ信頼レベル)。
*/
export function buildSpaceVisibilityWhere(
user: Express.User,
tableAlias: string,
): VisibilityWhere {
const base = buildVisibilityWhere(user, tableAlias, { spaceColumn: 'id' });
const clause =
`(
(${tableAlias}.kind = 'personal' AND ${tableAlias}.owner_id = ?)
OR (${tableAlias}.kind != 'personal' AND ${base.clause})
)`.replace(/\s+/g, ' ').trim();
// 出現順: personal-owner の viewer.id → base の params。
return { clause, params: [user.id, ...base.params] };
}
export function canEditEntity(user: Express.User, entity: { ownerId: string | null }): boolean {
return user.role === 'admin' || entity.ownerId === user.id;
}
export type SpaceMemberRole = 'owner' | 'editor' | 'viewer';
/**
* スペースの管理操作メンバー管理・スペース設定変更・archiveを許可するか。
* admin / 根オーナーspace.ownerId/ member.role==='owner' なら true。
* `memberRole` は Repository.getSpaceMemberRole で事前取得したロールDB アクセスは Repository に置く)。
*/
export function canManageSpace(
user: Express.User,
space: { ownerId: string | null },
memberRole?: SpaceMemberRole | null,
): boolean {
return (
user.role === 'admin' ||
(space.ownerId !== null && space.ownerId === user.id) ||
memberRole === 'owner'
);
}
/**
* スペース配下の行編集(タスク継続/削除・カレンダー CRUD・ファイル upload 等)を
* スペースのメンバーシップ起点で許可するか。canManageSpace に加え editor も許可。
* viewer は read のみbuildVisibilityWhere が扱う)で書き込み不可。
*/
export function canEditInSpace(
user: Express.User,
space: { ownerId: string | null },
memberRole?: SpaceMemberRole | null,
): boolean {
return canManageSpace(user, space, memberRole) || memberRole === 'editor';
}
/**
* 指定 user が指定タスクを閲覧できるか判定する。SQL ではなくロード済みオブジェクト
* に対して使う。`buildVisibilityWhere` と同じセマンティクスをコード上で再現する。
*
* - admin: 常に true
* - owner: 常に true
* - public: 常に true
* - org: user.orgIds に visibilityScopeOrgId が含まれているかで判定
* - スペースメンバー/根オーナー: `spaceAccessible === true` のとき true
* (呼び出し側が `repo.userCanViewSpace(task.spaceId, user)` で算出して渡す。
* この関数自体は DB を触らず純粋に保つ)
* - private: 上記いずれにも該当しなければ false
*/
export function canUserSeeTask(
user: Express.User,
task: { ownerId: string | null; visibility: 'private' | 'org' | 'public'; visibilityScopeOrgId: string | null },
spaceAccessible?: boolean,
): boolean {
if (user.role === 'admin') return true;
if (task.ownerId !== null && task.ownerId === user.id) return true;
if (task.visibility === 'public') return true;
if (task.visibility === 'org' && task.visibilityScopeOrgId !== null) {
if (user.orgIds.includes(task.visibilityScopeOrgId)) return true;
}
// スペースメンバーシップ起点の閲覧許可buildVisibilityWhere の OR ブランチに対応)。
if (spaceAccessible === true) return true;
return false;
}