支持明确指定的钉钉主管理员首次登录 (#132)
This commit is contained in:
parent
230962f6a3
commit
7fa8284255
@ -92,7 +92,9 @@ class DingTalkClosedAccess
|
||||
return null;
|
||||
}
|
||||
|
||||
$role = Role::where('name', SystemAccess::USER)->firstOrFail();
|
||||
$roleName = FinanceRoster::isDesignatedSuperAdmin((string) $profile['dingtalk_user_id'])
|
||||
? SystemAccess::SUPER_ADMIN : SystemAccess::USER;
|
||||
$role = Role::where('name', $roleName)->firstOrFail();
|
||||
$email = $this->normalizeEmail($profile['email'] ?? null);
|
||||
if (! $email || User::withTrashed()->where('email', $email)->exists()) {
|
||||
$email = 'ding-'.hash('sha256', $profile['union_id']).'@dingtalk.invalid';
|
||||
@ -112,7 +114,7 @@ class DingTalkClosedAccess
|
||||
'created_from' => 'dingtalk_roster',
|
||||
]);
|
||||
$account->roles()->attach($role->id);
|
||||
// Shell access only; no financial pages or administrative privileges.
|
||||
// Explicit designation controls the role; ordinary roster users get shell access.
|
||||
$account->permissions()->attach(Permission::where('name', 'read-admin-panel')->firstOrFail()->id);
|
||||
$account->companies()->attach($companies->first()->id);
|
||||
if ($mobile = $this->normalizeMobile($profile['mobile'] ?? null)) {
|
||||
|
||||
@ -7,6 +7,11 @@ use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||||
/** Single-person, current HR authorization lookup; no HR rows or stale grants are cached. */
|
||||
class FinanceRoster
|
||||
{
|
||||
public static function isDesignatedSuperAdmin(string $userId): bool
|
||||
{
|
||||
return $userId !== '' && in_array($userId, config('dingtalk.super_admin_user_ids', []), true);
|
||||
}
|
||||
|
||||
public function __construct(private readonly UpstreamReadOnlyQueryFactory $queries)
|
||||
{
|
||||
}
|
||||
@ -77,7 +82,8 @@ class FinanceRoster
|
||||
return null;
|
||||
}
|
||||
$allowed = array_map('strval', config('dingtalk.finance_department_ids', []));
|
||||
if (! array_intersect($allowed, array_map('strval', array_column($ancestors, 'Id')))) {
|
||||
if (! self::isDesignatedSuperAdmin($userId)
|
||||
&& ! array_intersect($allowed, array_map('strval', array_column($ancestors, 'Id')))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,10 @@ return [
|
||||
// When omitted, automatic provisioning requires exactly one enabled company.
|
||||
'company_id' => env('DINGTALK_COMPANY_ID'),
|
||||
|
||||
// Explicitly approved enterprise user IDs only; never names or departments.
|
||||
// Applies the highest role on first provisioning, not on subsequent logins.
|
||||
'super_admin_user_ids' => array_values(array_filter(array_map('trim', explode(',', env('DINGTALK_SUPER_ADMIN_USER_IDS', ''))))),
|
||||
|
||||
'authorize_url' => env('DINGTALK_AUTHORIZE_URL', 'https://login.dingtalk.com/oauth2/auth'),
|
||||
'token_url' => env('DINGTALK_TOKEN_URL', 'https://api.dingtalk.com/v1.0/oauth2/userAccessToken'),
|
||||
'user_url' => env('DINGTALK_USER_URL', 'https://api.dingtalk.com/v1.0/contact/users/me'),
|
||||
|
||||
@ -13,6 +13,8 @@
|
||||
|
||||
已绑定的启用账号继续使用原账号及原权限。姓名、花名、邮箱和手机号不会用于自动认领其他账号。已拒绝、解绑或停用的身份不会通过再次授权自动恢复。
|
||||
|
||||
负责人明确指定的主管理员可通过受保护的运行时配置 `DINGTALK_SUPER_ADMIN_USER_IDS` 登记企业 userid(逗号分隔,默认空)。仅这些精确身份可以不受财务部门范围限制,首次开户赋予 `super_admin`;仍必须通过唯一花名册、在职、非离职、有效组织链和钉钉企业成员核验。既有账号角色不因该配置在登录时被覆盖。移除指定身份会恢复财务部门准入要求,但不直接改写既有角色。配置不得使用姓名、手机号或整部门,也不得把真实身份值提交到仓库。
|
||||
|
||||
新账号使用钉钉接口返回的姓名、可用邮箱和手机号;邮箱缺失或已被其他账号使用时,生成稳定的 `@dingtalk.invalid` 内部占位地址。系统生成不可预知的随机本地密码,不展示或发送密码,不发送注册邀请。花名册显示名仅作为姓名缺失时的后备。
|
||||
|
||||
新账号赋予现有 `user` 角色及进入应用外壳所需的 `read-admin-panel` 权限,不添加管理权限。按负责人最新要求,尚无页面权限配置时默认显示完整业务导航;主管理员随后可在“侧边栏管理”中设置指定用户的业务范围。配置优先级为指定用户、角色、默认完整导航;显式配置为空时没有业务页面,不回退到完整导航。管理员专属操作(包括店铺密码读取)继续单独鉴权。
|
||||
|
||||
@ -131,6 +131,27 @@ class DingTalkLoginTest extends FeatureTestCase
|
||||
$this->get(route('dashboard', ['company_id' => $this->company->id]))->assertRedirectContains('/workspace/');
|
||||
}
|
||||
|
||||
public function test_explicitly_designated_identity_gets_highest_role_only_on_first_provisioning(): void
|
||||
{
|
||||
config(['dingtalk.super_admin_user_ids' => ['ding-user-1']]);
|
||||
Http::preventStrayRequests();
|
||||
Http::fake($this->dingtalkResponses('union-designated', 'open-designated'));
|
||||
$state = $this->startAuthorization();
|
||||
$this->get(route('login.dingtalk.callback', ['code' => 'fixture', 'state' => $state]));
|
||||
$created = auth()->user();
|
||||
$this->assertNotNull($created);
|
||||
$this->assertTrue(\App\Utilities\SystemAccess::isSuperAdmin($created));
|
||||
$id = $created->id;
|
||||
$created->syncRoles([\App\Models\Auth\Role::where('name', 'user')->firstOrFail()->id]);
|
||||
$created->flushCache();
|
||||
auth()->logout();
|
||||
session()->invalidate();
|
||||
$state = $this->startAuthorization();
|
||||
$this->get(route('login.dingtalk.callback', ['code' => 'fixture-again', 'state' => $state]));
|
||||
$this->assertSame($id, auth()->id());
|
||||
$this->assertFalse(\App\Utilities\SystemAccess::isSuperAdmin(auth()->user()));
|
||||
}
|
||||
|
||||
public function test_shared_mobile_does_not_claim_an_existing_account(): void
|
||||
{
|
||||
Http::preventStrayRequests();
|
||||
|
||||
@ -15,6 +15,7 @@ class FinanceRosterTest extends TestCase
|
||||
public function test_roster_authorization(array $overrides, bool $expected): void
|
||||
{
|
||||
config(['dingtalk.finance_department_ids' => ['22848291', '994080505']]);
|
||||
config(['dingtalk.super_admin_user_ids' => $overrides['designated_ids'] ?? []]);
|
||||
$data = array_replace([
|
||||
'flowers' => [['Id' => 123, 'name' => '测试员工', 'flowerName' => '测试花名', 'deptId' => '55248182', 'status' => '', 'empStatus' => '正式']],
|
||||
'employees' => [['active' => 1, 'dept_id_list' => '[55248182]']],
|
||||
@ -49,6 +50,11 @@ class FinanceRosterTest extends TestCase
|
||||
public static function rosterCases(): array
|
||||
{
|
||||
return [
|
||||
'designated active administrator outside finance' => [['designated_ids' => ['ding-fixture-id'], 'ancestors' => [['Id' => '55248182', 'parent_id' => '1', 'depth' => 0]]], true],
|
||||
'designation does not cover another identity' => [['designated_ids' => ['another-id'], 'ancestors' => [['Id' => '55248182', 'parent_id' => '1', 'depth' => 0]]], false],
|
||||
'designated administrator still requires active employment' => [['designated_ids' => ['ding-fixture-id'], 'employees' => [['active' => 0]]], false],
|
||||
'designated administrator cannot bypass resignation' => [['designated_ids' => ['ding-fixture-id'], 'resignation' => ['userid' => 'ding-fixture-id']], false],
|
||||
'designated administrator needs roster identity' => [['designated_ids' => ['ding-fixture-id'], 'flowers' => []], false],
|
||||
'finance descendant' => [[], true],
|
||||
'second finance root' => [['ancestors' => [['Id' => '994080505', 'parent_id' => '994121400', 'depth' => 1]]], true],
|
||||
'concurrent finance membership' => [['employees' => [['active' => 1, 'dept_id_list' => '[100,55248182]']]], true],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user