diff --git a/admin/src/components/iam-v2/launch.vue b/admin/src/components/iam-v2/launch.vue index 4fc0b1262..fca9ffa33 100644 --- a/admin/src/components/iam-v2/launch.vue +++ b/admin/src/components/iam-v2/launch.vue @@ -9,7 +9,8 @@ const enabled = ref(false), busy = ref(false) onMounted(async () => { try { - enabled.value = !!(await iamGet('/status')).enabled + const status = await iamGet('/status') + enabled.value = !!status.enabled && status.launch_enabled === true } catch { enabled.value = false } diff --git a/admin/src/components/iam-v2/personal.vue b/admin/src/components/iam-v2/personal.vue index 495fab025..1f4cbb0c9 100644 --- a/admin/src/components/iam-v2/personal.vue +++ b/admin/src/components/iam-v2/personal.vue @@ -27,20 +27,20 @@ 验证当前身份 企业微信重新登录 关联已有 IAM 账号 设置 IAM 登录密码 - 进入甄美 ERP ↗ + 进入甄美 ERP ↗ = { } const enabled = ref(false), reviewAllowed = ref(false), + managementEnabled = ref(false), caps = ref([]), rows = ref([]), total = ref(0), @@ -446,7 +447,7 @@ const batchVisible = ref(false), selectedIndexes = ref([]), batchConfirmed = ref(false), job = ref(null) -const can = (c: string) => caps.value.includes(c) +const can = (c: string) => caps.value.includes(c) && (c === 'read' || managementEnabled.value) const validIdentity = computed( () => operation.kind === 'unbind' || @@ -618,6 +619,7 @@ onMounted(async () => { enabled.value = s.enabled caps.value = s.management_capabilities || [] reviewAllowed.value = s.protected_review_operator === true + managementEnabled.value = s.management_enabled === true if (enabled.value && can('read')) await load() } catch (e) { error.value = identityError(e) diff --git a/docs/iam-v2.md b/docs/iam-v2.md index 91ad30932..fd8c1999a 100644 --- a/docs/iam-v2.md +++ b/docs/iam-v2.md @@ -2,7 +2,7 @@ ## 默认状态与部署次序 -所有新功能由 `iam.v2_enabled` 控制,默认 **false**。关闭时不写新表、不登记来源会话,原密码、企微、IAM 入站登录保持原路径。本文只描述源码;生产开关、迁移、真实浏览器及三端联调需另行验收。 +首次部署 `iam.v2_enabled`、`iam.v2_management_enabled`、`iam.v2_launch_enabled` 都默认 **false**。全局未启用时不登记新来源会话,原密码、企微、IAM 入站登录保持原路径。产生v2关联后,全局核心必须保持true以持续中央停用检查、签名事件和代际补偿;维护/回退优先关闭management与launch两个新流程开关,不能以关闭全局核心绕过这些检查。本文只描述源码;生产开关、迁移、真实浏览器及三端联调需另行验收。 1. 备份部署版本与配置,审查并执行 `server/database/migrations/20261029_iam_v2.sql`(按实际表前缀调整)。此迁移只加七张表及独立菜单/权限,不给现有角色自动授予权限,不改账号与角色。 2. 中央先部署 v2 绑定墓碑、幂等恢复、固定来源回查、版本检查、源/目标访问授权、密码设置协议与签名事件。旧 v1 token 不得获得新管理权限。 @@ -12,6 +12,8 @@ ```ini [IAM] V2_ENABLED = false +V2_MANAGEMENT_ENABLED = false +V2_LAUNCH_ENABLED = false V2_TOKEN = V2_MANAGEMENT_TOKEN = V2_INTROSPECTION_TOKEN = @@ -77,7 +79,7 @@ SQLite 合成测试不加载生产 `.env`、不发网络、不修改真实用户 ## 回退 -先将 `V2_ENABLED=false`,恢复上一已验证代码/静态构建。保留七张新表、旧开户账本、事件与审计记录、所有真实账号和业务数据。关闭 v2 停止委托登录/新证明,不撤回中央已执行的人事操作;错误关联通过有审计的显式解绑/换绑恢复,而非删表或恢复旧数据库。 +已有v2状态时先将 `V2_MANAGEMENT_ENABLED=false`、`V2_LAUNCH_ENABLED=false`,保持 `V2_ENABLED=true` 和当前安全核心;管理写/个人关联密码写/新的委托事务停止,普通授权核验、事件和代际补偿继续。仅可回到同样保持这些核心能力的兼容版本,不能盲退到无v2检查的旧代码。只有从未激活、七表无v2业务状态且全局仍false的首次部署,才可还原本次原文件与原index。保留七张新表、旧开户账本、事件与审计记录、所有真实账号和业务数据。关闭新流程开关停止新的委托登录/管理证明,不撤回中央已执行的人事操作;错误关联通过有审计的显式解绑/换绑恢复,而非删表或恢复旧数据库。 ## 2026-10-29 只读生产入口核对与发布步骤 @@ -97,7 +99,7 @@ SQLite 合成测试不加载生产 `.env`、不发网络、不修改真实用户 4. 中央先完成v2迁移、凭据/固定URL/回查注册、原生IAM本人入口和中央复核部署;ZYT新代码先保持 `V2_ENABLED=false`。配置完整的API/portal/ERP公开callback URL及两条来源映射,尤其ERP公开 `/basic-api` 前缀。 5. 后端按审核清单更新;静态资源先增量加入旧资源目录,最后原子切换index.html,不删旧哈希文件。按实际OPcache配置处理源码重载,不把CLI `opcache_reset()`当成FPM已刷新,不重启无关服务。 6. 先验证开关关闭时原登录/业务行为,再用授权测试账号开启v2验收:密码/企微→ERP、IAM管理后台本地session无KC cookie→ZYT/ERP、员工工作台→两端、同人/异人多标签、已有账号/角色2新开户、密码owner双阶段、敏感单人中央审批及批量跳过、签名事件/30秒补偿。逐项核对真实页面与后端会话,不以HTTP200代替登录验收。 -7. 回退先关闭v2,恢复发布前的本次后端文件与静态index;保留七表、事件、绑定历史、中央操作与所有业务数据。不要用整库恢复或删除IAM员工来回退代码。普通source logout按约定不是全局登出。 +7. 初次未激活部署(全局false且七表空)可恢复发布前文件/index;一旦已有v2状态,回退优先只关闭管理/新登录两个开关、保持全局安全核心true,不盲退到旧代码;保留七表、事件、绑定历史、中央操作与所有业务数据。不要用整库恢复或删除IAM员工来回退代码。普通source logout按约定不是全局登出。 ### 最终发布基线差异(追加) diff --git a/server/app/adminapi/controller/IamV2Controller.php b/server/app/adminapi/controller/IamV2Controller.php index 7b6c115c4..1feddd653 100644 --- a/server/app/adminapi/controller/IamV2Controller.php +++ b/server/app/adminapi/controller/IamV2Controller.php @@ -17,6 +17,9 @@ final class IamV2Controller extends BaseAdminController { try { if (!IamV2Client::enabled()) { throw new RuntimeException('FEATURE_DISABLED'); } + $action = strtolower((string) $this->request->action()); + if (in_array($action, ['operation','preflight','batch','retry','selfstart','passwordsetup'], true) && !IamV2Client::feature('management')) { throw new RuntimeException('FEATURE_DISABLED'); } + if (in_array($action, ['launchstart','targetprepare','targetexchange'], true) && !IamV2Client::feature('launch')) { throw new RuntimeException('FEATURE_DISABLED'); } $reads = ['status','accounts','identities','operationresult','audit','job','targetconfig']; $read = in_array(strtolower((string) $this->request->action()), $reads, true); if (($read && !$this->request->isGet()) || (!$read && !$this->request->isPost())) { throw new RuntimeException('METHOD_NOT_ALLOWED'); } @@ -48,7 +51,7 @@ final class IamV2Controller extends BaseAdminController } public function status() { - if (!IamV2Client::enabled()) { return $this->data(['enabled' => false]); } + if (!IamV2Client::enabled()) { return $this->data(['enabled' => false, 'management_enabled' => false, 'launch_enabled' => false]); } return $this->invokeV2(function () { $token = (string) $this->request->header('token', ''); $legacy = Db::name('admin_session')->where('token', $token)->where('expire_time', '>', time())->find(); @@ -56,7 +59,7 @@ final class IamV2Controller extends BaseAdminController IamV2Source::admin((int) $legacy['admin_id']); $fresh = false; try { $s = IamV2Source::current($token); $fresh = $s['auth_time'] >= time() - 300; } catch (\Throwable $e) { $s = null; } $b = (new IamV2Manager())->binding((int) $legacy['admin_id']); - return ['enabled' => true, 'session_ready' => $s !== null, 'reauth_required' => !$fresh, + return ['enabled' => true, 'management_enabled' => IamV2Client::feature('management'), 'launch_enabled' => IamV2Client::feature('launch'), 'session_ready' => $s !== null, 'reauth_required' => !$fresh, 'management_capabilities' => IamV2Source::capabilities((int) $legacy['admin_id']), 'protected_review_operator' => IamV2Source::reviewOperator((int) $legacy['admin_id']), 'protected_account' => IamV2Source::protectedAccount((int) $legacy['admin_id'])] + $b; @@ -117,6 +120,7 @@ final class IamV2Controller extends BaseAdminController public function launchContinue() { try { + IamV2Source::requirePurpose('launch'); $client = new IamV2Client(); $s = IamV2Source::check((string) ($_COOKIE[IamV2Source::COOKIE] ?? '')); $intent = (string) $this->request->get('intent', ''); $cookie = (string) ($_COOKIE['ZYT_IAM_V2_INTENT'] ?? ''); if ($intent === '' || !hash_equals($cookie, $intent)) { throw new RuntimeException('ACCOUNT_MISMATCH'); } @@ -147,6 +151,7 @@ final class IamV2Controller extends BaseAdminController public function selfCallback() { try { + IamV2Source::requirePurpose('self_link'); $central = new IamV2Client(); $s = IamV2Source::check((string) ($_COOKIE[IamV2Source::COOKIE] ?? '')); $flow = IamV2Flow::take('self_link', (string) $this->request->get('state', ''), $s['session_id']); $oidc = new IamOidcClient($this->selfConfig()); $oidc->exchange((string) $this->request->get('code', ''), $flow['verifier'], $flow['nonce']); @@ -172,6 +177,7 @@ final class IamV2Controller extends BaseAdminController public function passwordContinue() { try { + IamV2Source::requirePurpose('password_setup'); $client = new IamV2Client(); $s = IamV2Source::check((string) ($_COOKIE[IamV2Source::COOKIE] ?? '')); $f = IamV2Flow::take('password_setup', (string) ($_COOKIE['ZYT_IAM_PASSWORD_INTENT'] ?? ''), $s['session_id']); $id = (string) $this->request->get('request_id', ''); $state = (string) $this->request->get('state', ''); @@ -187,10 +193,10 @@ final class IamV2Controller extends BaseAdminController } public function targetConfig() { - if (!IamV2Client::enabled()) { return $this->data(['enabled' => false]); } + if (!IamV2Client::enabled()) { return $this->data(['enabled' => false, 'management_enabled' => false, 'launch_enabled' => false]); } return $this->invokeV2(function () { $source = (string) $this->request->get('source', 'iam-portal'); - return ['enabled' => true, 'restart_url' => IamV2Client::origin((string) Config::get('iam.portal_url')) . ($source === 'iam-admin' ? '/admin' : '/portal')]; + return ['enabled' => IamV2Client::feature('launch'), 'global_enabled' => true, 'management_enabled' => IamV2Client::feature('management'), 'launch_enabled' => IamV2Client::feature('launch'), 'restart_url' => IamV2Client::origin((string) Config::get('iam.portal_url')) . ($source === 'iam-admin' ? '/admin' : '/portal')]; }); } public function targetPrepare() diff --git a/server/app/adminapi/service/iam/v2/IamV2Client.php b/server/app/adminapi/service/iam/v2/IamV2Client.php index 4c5660090..74335396b 100644 --- a/server/app/adminapi/service/iam/v2/IamV2Client.php +++ b/server/app/adminapi/service/iam/v2/IamV2Client.php @@ -22,6 +22,12 @@ final class IamV2Client $settings ??= (array) Config::get('iam', []); return filter_var($settings['v2_enabled'] ?? false, FILTER_VALIDATE_BOOL); } + public static function feature(string $feature, ?array $settings = null): bool + { + $settings ??= (array) Config::get('iam', []); + if (!in_array($feature, ['management', 'launch'], true)) { return false; } + return self::enabled($settings) && filter_var($settings['v2_' . $feature . '_enabled'] ?? false, FILTER_VALIDATE_BOOL); + } public static function https(string $url): string { $p = parse_url($url); diff --git a/server/app/adminapi/service/iam/v2/IamV2Source.php b/server/app/adminapi/service/iam/v2/IamV2Source.php index 354e81021..9261f4210 100644 --- a/server/app/adminapi/service/iam/v2/IamV2Source.php +++ b/server/app/adminapi/service/iam/v2/IamV2Source.php @@ -13,6 +13,11 @@ use app\common\cache\AdminAccountSafeCache; final class IamV2Source { public const COOKIE = 'ZYT_IAM_V2_SESSION'; + public static function requirePurpose(string $purpose): void + { + $feature = in_array($purpose, ['management', 'self_link', 'password_setup'], true) ? 'management' : ($purpose === 'launch' ? 'launch' : null); + if ($feature !== null && !IamV2Client::feature($feature)) { throw new RuntimeException('FEATURE_DISABLED'); } + } public static function enroll(string $token, string $method, bool $cookie = true, ?int $authTime = null): array { if (!in_array($method, ['password', 'wecom', 'oidc', 'iam_launch'], true)) { throw new RuntimeException('REAUTH_REQUIRED'); } @@ -70,10 +75,11 @@ final class IamV2Source /** Service callback: local-only live authorization, safe while central holds its state lock. */ public static function liveStatus(string $id, int $version, string $purpose = '', array $targets = []): array { + self::requirePurpose($purpose); $s = self::check($id, $version); $out = ['active' => true, 'actorLocalId' => (string) $s['admin_id'], 'sessionId' => $s['session_id'], 'sessionVersion' => (int) $s['version'], 'authMethod' => $s['auth_method'], 'authTime' => (int) $s['auth_time'], 'expiresAt' => (int) $s['expires_at']]; - if ($purpose !== 'management') { return $out; } + if ($purpose !== 'management') { return $out + ['protectedAccount' => self::protectedAccount((int) $s['admin_id'])]; } if ($s['auth_time'] < time() - 300) { throw new RuntimeException('REAUTH_REQUIRED'); } if (!$targets || count($targets) > 100) { throw new RuntimeException('INVALID_INPUT'); } foreach ($targets as $target) { @@ -147,6 +153,7 @@ final class IamV2Source } public static function proof(array $session, string $purpose, array $targets = [], array $capabilities = [], ?string $transaction = null, array $extra = []): string { + self::requirePurpose($purpose); self::check($session['session_id'], (int) $session['version']); $secret = bin2hex(random_bytes(32)); $payload = ['applicationId' => (string) Config::get('iam.application_id', 'zyt'), 'actorLocalId' => (string) $session['admin_id'], @@ -160,6 +167,7 @@ final class IamV2Source } public static function introspect(string $proof, string $purpose, ?string $transaction = null): array { + self::requirePurpose($purpose); // The central Go attestor serializes an absent transaction as an empty string. $transaction = $transaction === '' ? null : $transaction; return Db::transaction(function () use ($proof, $purpose, $transaction) { diff --git a/server/app/adminapi/service/iam/v2/IamV2Target.php b/server/app/adminapi/service/iam/v2/IamV2Target.php index c927a378c..c7c94dddb 100644 --- a/server/app/adminapi/service/iam/v2/IamV2Target.php +++ b/server/app/adminapi/service/iam/v2/IamV2Target.php @@ -19,6 +19,7 @@ final class IamV2Target public function __construct(private ?IamV2Client $client = null, private $resolve = null, private $login = null) { $this->settings = (array) Config::get('iam'); + if (!IamV2Client::feature('launch', $this->settings)) { throw new RuntimeException('FEATURE_DISABLED'); } $this->client ??= new IamV2Client(); $this->resolve ??= fn(array $result) => $this->resolveAdmin($result); $this->login ??= function (int $id, array $authentication): array { diff --git a/server/config/iam.php b/server/config/iam.php index edb386578..e45513d08 100644 --- a/server/config/iam.php +++ b/server/config/iam.php @@ -3,6 +3,8 @@ // Optional integration: disabled installations never touch IAM or its transaction store. return [ 'v2_sources' => json_decode((string) env('iam.v2_sources_json', '{}'), true) ?: [], + 'v2_management_enabled' => env('iam.v2_management_enabled', false), + 'v2_launch_enabled' => env('iam.v2_launch_enabled', false), 'v2_enabled' => env('iam.v2_enabled', false), 'v2_management_token' => env('iam.v2_management_token', ''), 'v2_token' => env('iam.v2_token', ''), diff --git a/server/tests/IamV2RouteContractTest.php b/server/tests/IamV2RouteContractTest.php index 09295c5ae..8ef8aaa18 100644 --- a/server/tests/IamV2RouteContractTest.php +++ b/server/tests/IamV2RouteContractTest.php @@ -17,7 +17,7 @@ requireContract(str_contains($source,"self::check(\$p['sessionId']"), 'proof che requireContract(str_contains($source,"self::authorize(\$session, substr(\$cap, 4)"), 'proof rechecks live permission'); requireContract(str_contains($source,"self::scope((int) \$session['admin_id'], \$targets)"), 'target data scope'); requireContract(str_contains($client,'CURLOPT_FOLLOWLOCATION => false') && str_contains($client,'CURLOPT_SSL_VERIFYPEER => true'), 'TLS fixed host no redirect'); -requireContract(str_contains($config,"'v2_enabled' => env('iam.v2_enabled', false)"), 'default OFF'); +requireContract(str_contains($config,"'v2_enabled' => env('iam.v2_enabled', false)") && str_contains($config,"'v2_management_enabled' => env('iam.v2_management_enabled', false)") && str_contains($config,"'v2_launch_enabled' => env('iam.v2_launch_enabled', false)"), 'global and new-flow gates default OFF'); requireContract(!str_contains($controller,"'token' =>"), 'no business token callback'); requireContract(str_contains($controller,"verifiedIdToken()") && str_contains($controller,"'oidcNonce' =>"), 'self-link independently verifiable OIDC token and nonce'); requireContract(!str_contains($controller,"request->post('employeeId'"), 'no browser chosen personal employee'); diff --git a/server/tests/IamV2SourceTest.php b/server/tests/IamV2SourceTest.php index 66ad631e6..d751767d6 100644 --- a/server/tests/IamV2SourceTest.php +++ b/server/tests/IamV2SourceTest.php @@ -15,7 +15,7 @@ $app->config->set(['default'=>'file','stores'=>['file'=>['type'=>'File','path'=> $app->config->set(['default'=>'fixture','connections'=>['fixture'=>['type'=>'sqlite','database'=>$file,'prefix'=>'','fields_strict'=>true]]], 'database'); $manager = new think\DbManager(); $manager->setConfig($app->config->get('database')); $app->instance('think\Db', $manager); $app->config->set(['data_scope'=>['enabled'=>false], 'unique_identification'=>'fixture'], 'project'); -$app->config->set(['application_id'=>'zyt','portal_url'=>'https://iam.example.test','v2_enabled'=>true,'v2_introspection_token'=>str_repeat('c',32)], 'iam'); +$app->config->set(['application_id'=>'zyt','portal_url'=>'https://iam.example.test','v2_enabled'=>true,'v2_management_enabled'=>true,'v2_launch_enabled'=>true,'v2_introspection_token'=>str_repeat('c',32)], 'iam'); try { foreach ([ 'CREATE TABLE admin (id INTEGER PRIMARY KEY,account TEXT,name TEXT,password TEXT,disable INTEGER,root INTEGER,multipoint_login INTEGER DEFAULT 1,delete_time INTEGER)', @@ -59,7 +59,7 @@ try { Db::name('admin')->where('id',1)->update(['disable'=>1]); denies(fn()=>IamV2Source::check($s1['session_id']),'ACCOUNT_DISABLED'); Db::name('admin')->where('id',1)->update(['disable'=>0]); Db::name('admin_session')->where('id',1)->update(['token'=>'rotated']); denies(fn()=>IamV2Source::check($s1['session_id']),'REAUTH_REQUIRED'); Db::name('admin_session')->where('id',1)->update(['token'=>'fixture-token-1']); IamV2Source::revokeSession($s1['session_id']); denies(fn()=>IamV2Source::check($s1['session_id']),'REAUTH_REQUIRED'); ok(IamV2Source::check($s2['session_id'])['revoked']===0,'revoking one browser preserves other independent session'); - $calls=[];$cfg=['v2_enabled'=>true,'api_url'=>'https://iam.example.test/api','public_url'=>'https://zyt.example.test','erp_url'=>'https://erp.example.test','v2_token'=>str_repeat('v',32),'v2_management_token'=>str_repeat('m',32),'application_id'=>'zyt']; + $calls=[];$cfg=['v2_enabled'=>true,'v2_management_enabled'=>true,'v2_launch_enabled'=>true,'api_url'=>'https://iam.example.test/api','public_url'=>'https://zyt.example.test','erp_url'=>'https://erp.example.test','v2_token'=>str_repeat('v',32),'v2_management_token'=>str_repeat('m',32),'application_id'=>'zyt']; $client=new IamV2Client($cfg,function($method,$url,$headers,$body)use(&$calls){$calls[]=compact('method','url','headers','body');return ['status'=>200,'body'=>json_encode(['binding'=>null,'employee'=>null,'access'=>['allowed'=>false]])];}); $service=new IamV2Manager($client);$service->binding(2);$service->binding(2);ok(count($calls)===1,'30 second read cache'); Db::name('iam_v2_binding')->where('admin_id',2)->update(['checked_at'=>time()-31]);$service->binding(2);ok(count($calls)===2,'missed revocation compensation after 30 seconds'); @@ -145,6 +145,7 @@ try { Db::name('system_role_menu')->insert(['role_id'=>7,'menu_id'=>7]);Db::name('admin_role')->insert(['admin_id'=>2,'role_id'=>7]); ok(IamV2Source::introspect($raceProof,'management','')['protectedAccount']===true,'protection recomputed at callback after target privilege change'); denies(fn()=>IamV2Source::authorize($normalSession,'bind',[1],true,true),'PROTECTED_ACCOUNT_REVIEW_REQUIRED'); + ok(IamV2Source::liveStatus($normalSession['session_id'],1,'self_link')['protectedAccount']===true,'self-link callback reports live protection without requiring management capabilities'); denies(fn()=>IamV2Source::liveStatus($normalSession['session_id'],1,'management',['1']),'PROTECTED_ACCOUNT_REVIEW_REQUIRED'); Db::name('system_role_menu')->where('role_id',7)->delete(); denies(fn()=>IamV2Source::liveStatus($normalSession['session_id'],1,'management',['2']),'FORBIDDEN'); @@ -167,7 +168,16 @@ try { $oldGeneration=IamV2Source::enroll('old-generation-token','password',false); IamV2Source::revokeGeneration(4,1,'old-employee'); ok((int) Db::name('admin_session')->where('id',4)->value('expire_time')<=time(),'old generation without newer holder expires actual business session row'); + $gateSession=IamV2Source::enroll('fixture-token-1','password',false); + $pendingProof=IamV2Source::proof($gateSession,'management',[2],['iam:bind']); + $gateConfig=$app->config->get('iam');$gateConfig['v2_management_enabled']=false;$gateConfig['v2_launch_enabled']=false;$app->config->set($gateConfig,'iam'); + ok(IamV2Source::liveStatus($gateSession['session_id'],1)['active'],'closing new-flow gates preserves live-session validation'); + denies(fn()=>IamV2Source::proof($gateSession,'launch',[1]),'FEATURE_DISABLED'); + denies(fn()=>IamV2Source::introspect($pendingProof,'management',''),'FEATURE_DISABLED'); + denies(fn()=>IamV2Source::liveStatus($gateSession['session_id'],1,'management',['2']),'FEATURE_DISABLED'); + Db::name('admin')->where('id',1)->update(['disable'=>1]);denies(fn()=>IamV2Source::liveStatus($gateSession['session_id'],1),'ACCOUNT_DISABLED'); + ok(!IamV2Client::feature('management',['v2_enabled'=>true]) && !IamV2Client::feature('launch',['v2_enabled'=>true]),'new-flow gates default closed independently of global core'); $legacy=$cfg; unset($legacy['v2_token']); $legacy['application_token']=str_repeat('l',32); denies(fn()=>new IamV2Client($legacy),'SERVICE_UNAVAILABLE'); denies(fn()=>IamV2Client::https('https://iam.example.test/path?override=1'),'INVALID_FIXED_URL'); } finally { @unlink($file); if (is_dir($cachePath)) { foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($cachePath, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $path) { $path->isDir() ? rmdir($path->getPathname()) : unlink($path->getPathname()); } rmdir($cachePath); } } -echo "PASS IamV2SourceTest: independent browser SID, legacy reauth, live DB, scope, protected accounts, single-use proof/intent, expiry, immutable authTime, disabled/rotated/revoked sessions, 30s compensation, signed event replay/digest/future/version, revoked tombstone, batch protected filtering/index mapping, selected-only execution, no local account mutation, credential-change revocation, central empty-tx/reconcile contract, first-link/session preservation, access restoration, protected single-review/ordinary denial/live target protection, separate credentials, durable events, historical mapping, generation-specific business expiry\n"; +echo "PASS IamV2SourceTest: independent browser SID, legacy reauth, live DB, scope, protected accounts, single-use proof/intent, expiry, immutable authTime, disabled/rotated/revoked sessions, 30s compensation, signed event replay/digest/future/version, revoked tombstone, batch protected filtering/index mapping, selected-only execution, no local account mutation, credential-change revocation, central empty-tx/reconcile contract, first-link/session preservation, access restoration, protected single-review/ordinary denial/live target protection, separate credentials, durable events, historical mapping, generation-specific business expiry, maintenance gates retain revocation core and live self-link protection\n"; diff --git a/server/tests/IamV2TargetTest.php b/server/tests/IamV2TargetTest.php index 7072e3636..48ce4395a 100644 --- a/server/tests/IamV2TargetTest.php +++ b/server/tests/IamV2TargetTest.php @@ -9,7 +9,7 @@ function targetReject(callable $fn,string $code):void { try{$fn();}catch(Runtime $app=new think\App(dirname(__DIR__).'/');think\Container::setInstance($app);$file=tempnam(sys_get_temp_dir(),'zyt-target-'); $app->config->set(['default'=>'fixture','connections'=>['fixture'=>['type'=>'sqlite','database'=>$file,'prefix'=>'','fields_strict'=>true]]],'database'); $db=new think\DbManager();$db->setConfig($app->config->get('database'));$app->instance('think\Db',$db); -$config=['v2_enabled'=>true,'api_url'=>'https://iam.example.test/api','public_url'=>'https://zyt.example.test','erp_url'=>'https://erp.example.test','application_id'=>'zyt','v2_token'=>str_repeat('v',32), +$config=['v2_enabled'=>true,'v2_management_enabled'=>true,'v2_launch_enabled'=>true,'api_url'=>'https://iam.example.test/api','public_url'=>'https://zyt.example.test','erp_url'=>'https://erp.example.test','application_id'=>'zyt','v2_token'=>str_repeat('v',32), 'v2_sources'=>['iam-admin'=>'https://iam.example.test/admin/iam/launch/continue','iam-portal'=>'https://iam.example.test/portal/launch/continue']];$app->config->set($config,'iam'); try { $ini=parse_ini_string('[IAM]'.PHP_EOL.'V2_SOURCES_JSON = '.json_encode($config['v2_sources']),true,INI_SCANNER_RAW); diff --git a/server/tests/IamV2UiTest.mjs b/server/tests/IamV2UiTest.mjs index 1cc4f1f24..2972a8821 100644 --- a/server/tests/IamV2UiTest.mjs +++ b/server/tests/IamV2UiTest.mjs @@ -15,13 +15,14 @@ for(const file of files){ } const source=fs.readFileSync(new URL(files[0],root),'utf8'), script=source.match(/