// +---------------------------------------------------------------------- namespace addon\mqttbroker\command; use think\console\Command; use think\console\Input; use think\console\Output; use think\facade\Db; use ywxapp\service\AddonService; use addon\mqttbroker\service\Auth; /** * ACL 实网决策验证:用真实 DB 规则校验 Auth::checkAcl 的放行/拒绝逻辑。 * php think mqttbroker:acltest * 验证项:deny 发布拒绝、默认放行、用户定向拒绝、超管绕过、$SYS 只读。 * 不依赖运行中的 Broker,直接复用 Broker 实际调用的 Auth 逻辑。 */ class TestAcl extends Command { protected function configure() { $this->setName('mqttbroker:acltest') ->setDescription('验证 ACL 发布/订阅权限决策(含 deny 拒绝)'); } protected function execute(Input $input, Output $output) { // 配置:开启 ACL,默认放行 $saved = []; try { $saved = AddonService::config('mqttbroker') ?: []; } catch (\Throwable $e) {} $config = array_merge($saved, ['acl_enabled' => '1', 'acl_default' => 'allow']); $auth = new Auth($config); $auth->ensureTables(); // 插入临时规则(测试后清理) $rows = [ ['target_type' => 'member', 'target' => 'alice', 'topic' => 'alice/secret', 'access' => 2, 'allow' => 0, 'sort' => 5, 'remark' => 'tmp'], ['target_type' => 'all', 'target' => '', 'topic' => 'secret/#', 'access' => 2, 'allow' => 0, 'sort' => 10, 'remark' => 'tmp'], ['target_type' => 'all', 'target' => '', 'topic' => 'public/#', 'access' => 3, 'allow' => 1, 'sort' => 20, 'remark' => 'tmp'], ]; $ids = []; foreach ($rows as $r) { $ids[] = Db::name('mqttbroker_acl')->insertGetId(array_merge($r, ['create_at' => time()])); } $results = []; $chk = function (string $case, bool $got, bool $expect) use (&$results) { $results[$case] = ['got' => $got, 'expect' => $expect, 'ok' => $got === $expect]; }; // 1) deny 发布 secret/# 被拒 $chk('deny publish secret/#', $auth->checkAcl(null, 'c1', 'secret/data', Auth::ACT_PUB), false); // 2) 同一主题仅 deny 发布,订阅走默认放行 $chk('subscribe secret/# default-allow', $auth->checkAcl(null, 'c1', 'secret/data', Auth::ACT_SUB), true); // 3) allow 规则 public/# 发布放行 $chk('allow publish public/info', $auth->checkAcl(null, 'c1', 'public/info', Auth::ACT_PUB), true); // 4) 未命中规则走默认放行 $chk('default allow other/topic', $auth->checkAcl(null, 'c1', 'other/topic', Auth::ACT_PUB), true); // 5) 用户定向 deny:alice 发布 alice/secret 被拒 $chk('user alice deny publish', $auth->checkAcl('alice', 'cA', 'alice/secret', Auth::ACT_PUB), false); // 6) alice 订阅 alice/secret(user 规则仅限发布)走默认放行 $chk('user alice subscribe (rule is pub-only)', $auth->checkAcl('alice', 'cA', 'alice/secret', Auth::ACT_SUB), true); // 7) 超管绕过 deny $chk('superuser bypass deny', $auth->checkAcl('root', 'cR', 'secret/data', Auth::ACT_PUB, true), true); // 8) $SYS 只读:订阅放行、发布拒绝 $chk('$SYS subscribe allowed', $auth->checkAcl(null, 'c2', '$SYS/broker/uptime', Auth::ACT_SUB), true); $chk('$SYS publish denied', $auth->checkAcl(null, 'c2', '$SYS/broker/uptime', Auth::ACT_PUB), false); // 清理临时规则 if ($ids) { Db::name('mqttbroker_acl')->whereIn('id', $ids)->delete(); } $pass = 0; $fail = 0; foreach ($results as $case => $r) { $flag = $r['ok'] ? 'PASS' : 'FAIL'; if ($r['ok']) { $pass++; } else { $fail++; } $output->writeln("[{$flag}] {$case} (got=" . var_export($r['got'], true) . " expect=" . var_export($r['expect'], true) . ")"); } $output->writeln(""); $output->writeln("ACL 验证结果:PASS={$pass} FAIL={$fail}"); return $fail === 0 ? 0 : 1; } }