feat: 新增资产拦截规则(全局黑名单)增删改查
新增独立于命令拦截(intercept_rules)的资产拦截功能,仅提供规则的 存储与管理;具体匹配/拦截逻辑另行实现。 - db: 新增 asset_intercept_rules 表(schema.sql,幂等)与 CRUD 层 (db/asset_intercept.go);kind 支持全等/模糊的 域名·IP·URL 及 CIDR - db: 首启动 seed 内置模糊拦截政府(.gov/.gov.cn)与教育(.edu/.edu.cn) 网站,settings 标记位门控,可禁用/删除且重启不复活 - server: /api/asset-intercept/rules 的 list/create/update/delete/toggle; cidr、exact_ip 做格式校验 - web: 新增「资产拦截」页(/system/intercept/assets)与侧边栏入口、 api 与类型定义
This commit is contained in:
1 parent
a590b415ba
commit
dfc9d2fa2e
9 files changed
+687
No files matched your search
@@ -0,0 +1,77 @@
|
||||
package db
|
||||
|
||||
import "time"
|
||||
|
||||
// AssetInterceptRule is one row of asset_intercept_rules — a global asset
|
||||
// blocklist entry. Unlike intercept_rules (which matches tool name / input),
|
||||
// these match the *target asset*: an exact/fuzzy domain·ip·url, or a CIDR range.
|
||||
// This layer only stores rules; the matching/enforcement logic lives elsewhere.
|
||||
type AssetInterceptRule struct {
|
||||
ID int64 `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Kind string `json:"kind"` // exact_domain|exact_ip|exact_url|fuzzy_domain|fuzzy_ip|fuzzy_url|cidr
|
||||
Pattern string `json:"pattern"`
|
||||
Note string `json:"note"`
|
||||
Builtin bool `json:"builtin"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
const assetInterceptRuleCols = `id, enabled, kind, pattern, note, builtin, created_at, updated_at`
|
||||
|
||||
func scanAssetInterceptRule(row interface{ Scan(...any) error }) (AssetInterceptRule, error) {
|
||||
var r AssetInterceptRule
|
||||
err := row.Scan(&r.ID, &r.Enabled, &r.Kind, &r.Pattern, &r.Note, &r.Builtin, &r.CreatedAt, &r.UpdatedAt)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// ListAssetInterceptRules returns all rules, built-ins first then newest first.
|
||||
func (d *DB) ListAssetInterceptRules() ([]AssetInterceptRule, error) {
|
||||
rows, err := d.Query(`SELECT ` + assetInterceptRuleCols + ` FROM asset_intercept_rules ORDER BY builtin DESC, id DESC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []AssetInterceptRule
|
||||
for rows.Next() {
|
||||
r, err := scanAssetInterceptRule(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// CreateAssetInterceptRule inserts a new user rule (builtin is always false here).
|
||||
func (d *DB) CreateAssetInterceptRule(kind, pattern, note string, enabled bool) (AssetInterceptRule, error) {
|
||||
row := d.QueryRow(`
|
||||
INSERT INTO asset_intercept_rules(enabled, kind, pattern, note, builtin)
|
||||
VALUES ($1, $2, $3, $4, false)
|
||||
RETURNING `+assetInterceptRuleCols,
|
||||
enabled, kind, pattern, note)
|
||||
return scanAssetInterceptRule(row)
|
||||
}
|
||||
|
||||
// UpdateAssetInterceptRule replaces the editable fields of an existing rule.
|
||||
func (d *DB) UpdateAssetInterceptRule(id int64, kind, pattern, note string, enabled bool) (AssetInterceptRule, error) {
|
||||
row := d.QueryRow(`
|
||||
UPDATE asset_intercept_rules
|
||||
SET enabled=$2, kind=$3, pattern=$4, note=$5
|
||||
WHERE id=$1
|
||||
RETURNING `+assetInterceptRuleCols,
|
||||
id, enabled, kind, pattern, note)
|
||||
return scanAssetInterceptRule(row)
|
||||
}
|
||||
|
||||
// DeleteAssetInterceptRule removes a rule (built-in rules are deletable too).
|
||||
func (d *DB) DeleteAssetInterceptRule(id int64) error {
|
||||
_, err := d.Exec(`DELETE FROM asset_intercept_rules WHERE id=$1`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// ToggleAssetInterceptRule flips the enabled state of a rule.
|
||||
func (d *DB) ToggleAssetInterceptRule(id int64, enabled bool) error {
|
||||
_, err := d.Exec(`UPDATE asset_intercept_rules SET enabled=$2 WHERE id=$1`, id, enabled)
|
||||
return err
|
||||
}
|
||||
@@ -261,9 +261,41 @@ ON CONFLICT (name) DO NOTHING`,
|
||||
if err := d.seedDefaultInterceptRulesV2(); err != nil {
|
||||
return fmt.Errorf("seed intercept rules v2: %w", err)
|
||||
}
|
||||
if err := d.seedDefaultAssetInterceptRules(); err != nil {
|
||||
return fmt.Errorf("seed asset intercept rules: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedDefaultAssetInterceptRules inserts the built-in asset blocklist (fuzzy
|
||||
// domain matches for government / education sites) once on first startup. Gated
|
||||
// by a settings flag so a user's later disable/delete is never resurrected on
|
||||
// restart — same policy as the intercept-rule seed.
|
||||
func (d *DB) seedDefaultAssetInterceptRules() error {
|
||||
if v, _, _ := d.GetSetting("asset_intercept_default_rules_v1"); v == "done" {
|
||||
return nil
|
||||
}
|
||||
rules := []struct {
|
||||
kind string
|
||||
pattern string
|
||||
note string
|
||||
}{
|
||||
{"fuzzy_domain", ".gov", "[内置] 政府网站 (.gov)"},
|
||||
{"fuzzy_domain", ".gov.cn", "[内置] 政府网站 (.gov.cn)"},
|
||||
{"fuzzy_domain", ".edu", "[内置] 教育网站 (.edu)"},
|
||||
{"fuzzy_domain", ".edu.cn", "[内置] 教育网站 (.edu.cn)"},
|
||||
}
|
||||
for _, r := range rules {
|
||||
if _, err := d.Exec(`
|
||||
INSERT INTO asset_intercept_rules(enabled, kind, pattern, note, builtin)
|
||||
VALUES (true, $1, $2, $3, true)
|
||||
ON CONFLICT DO NOTHING`, r.kind, r.pattern, r.note); err != nil {
|
||||
return fmt.Errorf("asset rule %q: %w", r.pattern, err)
|
||||
}
|
||||
}
|
||||
return d.SetSetting("asset_intercept_default_rules_v1", "done")
|
||||
}
|
||||
|
||||
// builtinSkillVisibility maps a shipped skill's directory name → the built-in
|
||||
// agent keys that should see it by default. The skill FILES themselves live on the
|
||||
// filesystem (SkillDir, loaded by norma at runtime); DB only carries this visibility
|
||||
|
||||
@@ -1220,3 +1220,31 @@ CREATE INDEX IF NOT EXISTS idx_side_requests_history ON side_question_requests(s
|
||||
-- Additive v3 archive fields; old archives restore these as empty objects.
|
||||
ALTER TABLE side_question_sessions ADD COLUMN IF NOT EXISTS memory JSONB NOT NULL DEFAULT '{}';
|
||||
ALTER TABLE side_question_requests ADD COLUMN IF NOT EXISTS context_info JSONB NOT NULL DEFAULT '{}';
|
||||
|
||||
-- =====================================================================
|
||||
-- 资产拦截规则(全局黑名单)
|
||||
-- 独立于 §K 命令拦截(intercept_rules):intercept_rules 匹配工具名/入参文本,
|
||||
-- 这张表匹配「目标资产」——全等/模糊的域名·IP·URL 以及 CIDR 网段。
|
||||
-- 仅存规则;具体的匹配/拦截逻辑在别处实现。
|
||||
-- kind 七种:
|
||||
-- exact_domain / exact_ip / exact_url —— 全等匹配
|
||||
-- fuzzy_domain / fuzzy_ip / fuzzy_url —— 模糊匹配
|
||||
-- cidr —— CIDR 网段
|
||||
-- =====================================================================
|
||||
CREATE TABLE IF NOT EXISTS asset_intercept_rules (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
kind TEXT NOT NULL CHECK (kind IN (
|
||||
'exact_domain', 'exact_ip', 'exact_url',
|
||||
'fuzzy_domain', 'fuzzy_ip', 'fuzzy_url',
|
||||
'cidr')),
|
||||
pattern TEXT NOT NULL,
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
builtin BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_intercept_enabled ON asset_intercept_rules(enabled);
|
||||
DROP TRIGGER IF EXISTS trg_asset_intercept_rules_upd ON asset_intercept_rules;
|
||||
CREATE TRIGGER trg_asset_intercept_rules_upd BEFORE UPDATE ON asset_intercept_rules
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
Reference in new issue
Block a user