|
|
|
@@ -20,6 +20,8 @@ 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)',
|
|
|
|
|
'CREATE TABLE admin_session (id INTEGER PRIMARY KEY,admin_id INTEGER,terminal INTEGER,token TEXT,expire_time INTEGER,update_time INTEGER)',
|
|
|
|
|
'CREATE TABLE dept (id INTEGER PRIMARY KEY,name TEXT,delete_time INTEGER)',
|
|
|
|
|
'CREATE TABLE admin_dept (admin_id INTEGER,dept_id INTEGER)',
|
|
|
|
|
'CREATE TABLE admin_role (admin_id INTEGER,role_id INTEGER)',
|
|
|
|
|
'CREATE TABLE system_role (id INTEGER PRIMARY KEY,name TEXT,delete_time INTEGER)',
|
|
|
|
|
'CREATE TABLE system_role_menu (role_id INTEGER,menu_id INTEGER)',
|
|
|
|
@@ -63,11 +65,29 @@ try {
|
|
|
|
|
$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');
|
|
|
|
|
Db::name('dept')->insertAll([['id'=>10,'name'=>'Synthetic A','delete_time'=>null],['id'=>20,'name'=>'Synthetic B','delete_time'=>null],['id'=>30,'name'=>'Deleted Department','delete_time'=>time()]]);
|
|
|
|
|
Db::name('admin_dept')->insertAll([['admin_id'=>1,'dept_id'=>10],['admin_id'=>1,'dept_id'=>20],['admin_id'=>1,'dept_id'=>30],['admin_id'=>2,'dept_id'=>20]]);
|
|
|
|
|
$checkedAt=time()-5;Db::name('iam_v2_binding')->where('admin_id',2)->update(['checked_at'=>$checkedAt]);
|
|
|
|
|
$listing=$service->accounts(['q'=>'employee'],1);ok(count($listing['items'])===1 && $listing['items'][0]['dept_names']===['Synthetic B'],'department labels for scoped account');
|
|
|
|
|
ok($listing['items'][0]['iam']['checked_at']===$checkedAt,'cached successful check time is returned unchanged');
|
|
|
|
|
$all=$service->accounts([],1);$byId=[];foreach($all['items']as$item)$byId[$item['id']]=$item;
|
|
|
|
|
ok($byId[1]['dept_names']===['Synthetic A','Synthetic B'],'soft-deleted department is not displayed');
|
|
|
|
|
$project=$app->config->get('project');$scoped=$project;$scoped['data_scope']['enabled']=true;$app->config->set($scoped,'project');
|
|
|
|
|
$self=$service->accounts([],2);ok(count($self['items'])===1 && (int)$self['items'][0]['id']===2,'metadata does not widen existing data scope');$app->config->set($project,'project');
|
|
|
|
|
$lastSuccess=time()-100;Db::name('iam_v2_binding')->where('admin_id',2)->update(['checked_at'=>$lastSuccess]);
|
|
|
|
|
$offline=new IamV2Manager(new IamV2Client($cfg,fn()=>['status'=>503,'body'=>json_encode(['error'=>['code'=>'SERVICE_UNAVAILABLE']])]));
|
|
|
|
|
$unknown=$offline->accounts(['q'=>'employee'],1)['items'][0]['iam'];
|
|
|
|
|
ok($unknown['state']==='unknown' && $unknown['access']['allowed']===null && $unknown['checked_at']===$lastSuccess,'failed refresh never fabricates allowed access or successful timestamp');
|
|
|
|
|
ok((int)Db::name('iam_v2_binding')->where('admin_id',2)->value('checked_at')===$lastSuccess,'failure preserves actual persisted check time');
|
|
|
|
|
$invokeCallback=function($action,$body,$bearer=null,$method='POST')use($app){
|
|
|
|
|
$request=(new think\Request())->withHeader(['authorization'=>$bearer??('Bearer '.str_repeat('c',32))])->withPost($body);
|
|
|
|
|
$request->setMethod($method);$request->setAction($action);$request->setController('IamV2');$app->instance('request',$request);
|
|
|
|
|
return (new app\adminapi\controller\IamV2Controller($app))->{$action}();
|
|
|
|
|
};
|
|
|
|
|
$request=(new think\Request())->withPost([]);$request->setMethod('POST');$request->setAction('launchStart');$app->instance('request',$request);
|
|
|
|
|
$controller=new app\adminapi\controller\IamV2Controller($app);$method=new ReflectionMethod($controller,'invokeV2');$method->setAccessible(true);
|
|
|
|
|
$response=$method->invoke($controller,static function(){throw new RuntimeException('NOT_LINKED');});$envelope=$response->getData();
|
|
|
|
|
ok($envelope['code']===0 && $envelope['show']===0 && $envelope['data']['error']['code']==='NOT_LINKED','IAM structured failure silences only the global toast');
|
|
|
|
|
$live=$invokeCallback('checkSession',['sessionId'=>$s2['session_id'],'sessionVersion'=>1]);$liveBody=$live->getData();
|
|
|
|
|
ok($live->getCode()===200 && $liveBody['active'] && $liveBody['sessionId']===$s2['session_id'] && $liveBody['sessionVersion']===1,'actual source callback raw HTTP shape/SID/version');
|
|
|
|
|
ok(!isset($liveBody['code']) && !isset($liveBody['data']),'callback is not business response envelope');
|
|
|
|
@@ -180,4 +200,4 @@ try {
|
|
|
|
|
$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, maintenance gates retain revocation core and live self-link protection\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, scoped departments/real cached check time/unknown access, single-toast envelope\n";
|
|
|
|
|