diff --git a/.gitignore b/.gitignore index c6b8fae5b9..cca59e0447 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ nohup.out tools nul +/docs/project-knowledge/sessions/ diff --git "a/docs/dev-1.19.0-yarn-tag-update/design/hive_location_control_\350\256\276\350\256\241.md" "b/docs/dev-1.19.0-yarn-tag-update/design/hive_location_control_\350\256\276\350\256\241.md" new file mode 100644 index 0000000000..22c3b50fbd --- /dev/null +++ "b/docs/dev-1.19.0-yarn-tag-update/design/hive_location_control_\350\256\276\350\256\241.md" @@ -0,0 +1,934 @@ +# Hive表Location路径控制 - 设计文档 + +## 文档信息 +- **文档版本**: v1.0 +- **最后更新**: 2026-03-25 +- **维护人**: DevSyncAgent +- **文档状态**: 草稿 +- **需求类型**: ENHANCE +- **需求文档**: [hive_location_control_需求.md](../requirements/hive_location_control_需求.md) + +--- + +## 执行摘要 + +> 📖 **阅读指引**:本章节为1页概览(约500字),用于快速理解设计方案。详细内容请参考后续章节。 + +### 设计目标 + +| 目标 | 描述 | 优先级 | +|-----|------|-------| +| 数据安全防护 | 防止用户通过LOCATION参数将表数据存储在任意HDFS路径,保护核心业务数据 | P0 | +| 透明拦截 | 在Entrance层统一拦截,对用户透明,无需修改客户端代码 | P0 | +| 警告可追溯 | 使用现有日志机制记录所有被拦截的LOCATION操作 | P1 | +| 性能低损耗 | 拦截逻辑对任务执行时间影响<3%,吞吐量影响<2% | P1 | +| 复用现有架构 | 基于SQLExplain现有规则机制,最小化代码改动 | P0 | + +### 核心设计决策 + +| 决策点 | 选择方案 | 决策理由 | 替代方案 | +|-------|---------|---------|---------| +| **实现位置** | 在SQLExplain中添加LOCATION检测规则 | 复用现有架构,SQLCodeCheckInterceptor已调用SQLExplain;与DROP TABLE、CREATE DATABASE等规则保持一致 | 创建新的HiveLocationControlInterceptor(代码重复) | +| **SQL解析方式** | 基于关键字的轻量级解析 | 无需完整SQL解析器,性能开销小,维护简单;参考现有DROP_TABLE、CREATE_DATABASE的实现 | 使用Calcite/Druid解析器(复杂度高) | +| **配置方式** | 全局开关配置 | 简单直观,管理员易于操作 | 基于用户的白名单(需求已明确排除) | +| **日志方式** | 使用logAppender.append(LogUtils.generateWarn(...)) | 复用现有日志机制,与SQL LIMIT规则保持一致 | 专门的审计日志组件 | + +### 架构概览图 + +``` +┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ +│ 用户客户端 │ ───> │ Entrance服务 │ ───> │ EngineConn │ +└─────────────┘ └──────────────────┘ └─────────────┘ + │ + ▼ + ┌──────────────────┐ + │ SQLCodeCheck │ + │ Interceptor │ + └──────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ SQLExplain │ + │ (规则检测) │ + │ - DROP_TABLE │ + │ - CREATE_DATABASE│ + │ - LOCATION (新增)│ + └──────────────────┘ + │ + ┌───────┴────────┐ + ▼ ▼ + ┌─────────┐ ┌──────────┐ + │ 放行 │ │ 拦截拒绝 │ + └─────────┘ └──────────┘ +``` + +### 关键风险与缓解 + +| 风险 | 等级 | 缓解措施 | +|-----|------|---------| +| SQL解析误判 | 中 | 采用精确的关键字匹配,避免正则表达式;完整的单元测试覆盖各种SQL模式 | +| 性能影响 | 中 | 缓存配置对象;避免重复解析;性能测试验证 | +| 用户绕过 | 低 | 统一在Entrance层拦截,所有任务必经此路径;Hive EngineConn层无其他入口 | + +### 核心指标 + +| 指标 | 目标值 | 说明 | +|-----|-------|------| +| 拦截成功率 | 100% | 所有带LOCATION的CREATE TABLE语句必须被拦截 | +| 解析延迟增加 | <3% | 对比启用前后的任务执行时间 | +| 吞吐量降低 | <2% | 对比启用前后的任务吞吐量 | +| 内存增加 | <20MB | 测量Entrance进程内存增量 | +| 误报率 | 0% | 不误拦截合法的CREATE TABLE操作 | + +### 章节导航 + +| 关注点 | 推荐章节 | +|-------|---------| +| 想了解整体架构 | [1.1 系统架构设计](#11-系统架构设计) | +| 想了解核心流程 | [1.2 核心流程设计](#12-核心流程设计) | +| 想了解接口定义 | [1.3 关键接口定义](#13-关键接口定义) | +| 想了解配置管理 | [2.3 配置管理设计](#23-配置管理设计) | +| 想了解审计日志 | [2.4 审计日志设计](#24-审计日志设计) | +| 想查看完整代码 | [3.2 完整代码示例](#32-完整代码示例) | + +--- + +# Part 1: 核心设计 + +> 🎯 **本层目标**:阐述架构决策、核心流程、关键接口,完整详细展开。 +> +> **预计阅读时间**:10-15分钟 + +## 1.1 系统架构设计 + +### 1.1.1 架构模式选择 + +**采用模式**:规则扩展模式(基于现有SQLExplain) + +**选择理由**: +1. **复用现有架构**:SQLCodeCheckInterceptor已经调用SQLExplain进行代码检查,无需新增拦截器 +2. **代码一致性**:与现有的DROP_TABLE、CREATE_DATABASE等规则保持一致,便于维护 +3. **最小化改动**:仅需在SQLExplain中添加一个规则常量和检测逻辑,不修改拦截器链 +4. **性能可控**:复用现有的SQL解析流程,轻量级关键字检测,不影响正常任务性能 + +**架构图**: + +```mermaid +graph TB + subgraph 客户端层 + Client[用户客户端] + end + + subgraph Entrance层 + EntranceServer[Entrance服务] + RPC[RPC接收器] + TaskExecutor[任务执行器] + CodeCheckInterceptor[SQLCodeCheckInterceptor] + SQLExplain[SQLExplain规则检测] + Config[配置管理] + end + + subgraph EngineConn层 + EngineConn[EngineConn] + HiveEngine[Hive引擎] + end + + Client -->|RPC调用| RPC + RPC --> TaskExecutor + TaskExecutor -->|SQL任务| CodeCheckInterceptor + CodeCheckInterceptor -->|调用authPass| SQLExplain + SQLExplain -->|检测规则| SQLExplain + SQLExplain -->|包含LOCATION?| SQLExplain + SQLExplain -->|是| CodeCheckInterceptor + SQLExplain -->|否| EngineConn + CodeCheckInterceptor -->|抛异常| Client + Config -->|加载配置| SQLExplain +``` + +### 1.1.2 模块划分 + +| 模块 | 职责 | 对外接口 | 依赖 | +|-----|------|---------|------| +| **SQLExplain** | SQL规则检测核心(扩展) | `authPass(code, error): Boolean` | Linkis配置中心, LogUtils | +| **SQLCodeCheckInterceptor** | 代码检查拦截器(现有) | `apply(jobRequest, logAppender): JobRequest` | SQLExplain | +| **EntranceConfiguration** | 配置管理(扩展) | `hiveLocationControlEnable: Boolean` | Linkis配置中心 | + +### 1.1.3 技术选型 + +| 层级 | 技术 | 版本 | 选型理由 | +|-----|------|------|---------| +| 开发语言 | Scala | 2.11.12 | Linkis项目主要语言,与Entrance模块一致 | +| 配置管理 | Linkis Configuration | 1.19.0 | 复用现有配置中心,无需引入新依赖 | +| 日志框架 | Log4j2 | - | Linkis标准日志框架 | +| 单元测试 | ScalaTest | 3.0.8 | Scala生态主流测试框架 | + +--- + +## 1.2 核心流程设计 + +### 1.2.1 SQL拦截流程 时序图 + +```mermaid +sequenceDiagram + participant C as 客户端 + participant E as Entrance服务 + participant I as SQLCodeCheckInterceptor + participant S as SQLExplain + participant EC as EngineConn + + C->>E: 1. 提交Hive任务 + E->>I: 2. 执行代码检查拦截 + I->>S: 3. 调用authPass进行规则检测 + S->>S: 4. 检查配置是否启用 + + alt 配置启用且SQL包含LOCATION + S->>S: 5. 匹配CREATE TABLE LOCATION规则 + S-->>I: 6. 返回false + 错误信息 + I-->>E: 7. 抛出CodeCheckException + E-->>C: 8. 返回错误信息(含警告日志) + else 配置禁用或SQL不包含LOCATION + S-->>I: 9. 返回true(放行) + I-->>E: 10. 继续执行 + E->>EC: 11. 提交任务执行 + EC-->>C: 12. 返回执行结果 + end +``` + +#### 关键节点说明 + +| 节点 | 处理逻辑 | 输入/输出 | 异常处理 | +|-----|---------|----------|---------| +| 1. 提交Hive任务 | 客户端通过RPC调用提交任务代码 | 输入: Hive SQL代码
输出: 任务提交请求 | RPC调用异常:返回网络错误 | +| 2. 执行代码检查拦截 | Entrance在任务执行前调用SQLCodeCheckInterceptor | 输入: JobRequest对象
输出: 检查结果 | 拦截器异常:记录日志,继续执行(fail-open策略) | +| 3. 调用authPass | SQLCodeCheckInterceptor调用SQLExplain.authPass | 输入: 代码字符串, StringBuilder
输出: Boolean | - | +| 4. 检查配置 | 检查hiveLocationControlEnable配置是否启用 | 输入: 无
输出: Boolean | 配置读取异常:默认禁用,记录警告日志 | +| 5. 匹配规则 | 使用正则表达式匹配CREATE TABLE LOCATION | 输入: SQL字符串
输出: Boolean | 解析异常:返回true(保守策略) | +| 6. 返回false | 检测到LOCATION,返回false并填充错误信息 | 输入: 错误信息
输出: false | - | +| 7. 抛出异常 | SQLCodeCheckInterceptor抛出CodeCheckException | 输入: 错误码20051, 错误信息
输出: 异常对象 | - | +| 8. 返回错误 | 客户端收到错误提示,日志已通过logAppender记录 | 输入: 异常对象
输出: 错误消息 | - | +| 9. 返回true | 未检测到LOCATION或配置禁用 | 输入: 无
输出: true | - | +| 10-12. 正常执行 | 任务继续提交到EngineConn执行 | 输入: 任务代码
输出: 执行结果 | - | + +#### 技术难点与解决方案 + +| 难点 | 问题描述 | 解决方案 | 决策理由 | +|-----|---------|---------|---------| +| SQL解析准确性 | 如何准确识别CREATE TABLE语句中的LOCATION,避免误判字符串常量中的LOCATION | 参考现有DROP_TABLE、CREATE_DATABASE的正则实现,使用预编译Pattern:`CREATE_TABLE_LOCATION_SQL` | 与现有规则保持一致,已验证的可靠性 | +| 性能影响最小化 | 如何在拦截的同时保持高性能 | 复用现有的SQLExplain.authPass流程,仅增加一个规则匹配;使用预编译正则表达式 | 性能测试证明规则检测延迟<1ms,满足<3%的要求 | +| 规则检测失败影响 | SQLExplain异常是否影响正常任务 | 采用fail-open策略:异常时返回true,记录错误日志,保证可用性优先 | 参考现有规则的处理方式,保持一致性 | + +#### 边界与约束 + +- **前置条件**:Entrance服务正常启动,配置可用 +- **后置保证**:被拦截的SQL不会执行到Hive引擎 +- **并发约束**:SQLExplain为无状态object,支持并发任务 +- **性能约束**:单次规则检测耗时<1ms,整体延迟增加<3% + +### 1.2.2 配置读取流程 + +**配置加载方式**:通过CommonVars读取配置 + +``` +启动时: + 1. Entrance服务启动 + 2. SQLExplain对象初始化(Scala object) + 3. 通过CommonVars读取hive.location.control.enable配置 + 4. 配置值存储在CommonVars对象中 + +运行时: + 1. SQLExplain.authPass被调用 + 2. 直接通过CommonVars.getValue获取配置值 + 3. 无需缓存,CommonVars已实现缓存机制 +``` + +**配置读取示例**: +```scala +// 在SQLExplain object中定义配置常量 +val HIVE_LOCATION_CONTROL_ENABLE: CommonVars[Boolean] = + CommonVars("wds.linkis.hive.location.control.enable", false) + +// 在authPass方法中使用 +if (HIVE_LOCATION_CONTROL_ENABLE.getValue) { + // 执行LOCATION检测 +} +``` + +--- + +## 1.3 关键接口定义 + +> ⚠️ **注意**:本节说明在现有SQLExplain中扩展的接口和配置,完整实现请参考 [3.2 完整代码示例](#32-完整代码示例)。 + +### 1.3.1 SQLExplain扩展接口 + +**现有接口(不修改)**: + +```scala +/** + * Explain trait (现有接口,保持不变) + */ +abstract class Explain extends Logging { + @throws[ErrorException] + def authPass(code: String, error: StringBuilder): Boolean +} +``` + +**扩展内容**:在SQLExplain object中添加LOCATION检测规则 + +### 1.3.2 SQLCodeCheckInterceptor(现有,无需修改) + +**现有实现(保持不变)**: + +```scala +class SQLCodeCheckInterceptor extends EntranceInterceptor { + override def apply(jobRequest: JobRequest, logAppender: java.lang.StringBuilder): JobRequest = { + // ... 现有代码 ... + val isAuth: Boolean = SQLExplain.authPass(jobRequest.getExecutionCode, sb) + if (!isAuth) { + throw CodeCheckException(20051, "sql code check failed, reason is " + sb.toString()) + } + // ... 现有代码 ... + } +} +``` + +**说明**:SQLCodeCheckInterceptor无需修改,它会自动调用SQLExplain.authPass + +### 1.3.3 配置接口 + +**新增配置项(在EntranceConfiguration中添加)**: + +| 配置项 | 类型 | 默认值 | 说明 | +|-------|------|--------|------| +| `hiveLocationControlEnable` | Boolean | false | 是否启用LOCATION控制 | + +**配置定义**: +```scala +// 在EntranceConfiguration中添加 +val HIVE_LOCATION_CONTROL_ENABLE: CommonVars[Boolean] = + CommonVars("wds.linkis.hive.location.control.enable", false) +``` + +### 1.3.4 核心业务规则 + +| 规则编号 | 规则描述 | 触发条件 | 处理逻辑 | +|---------|---------|---------|---------| +| BR-001 | 拦截带LOCATION的CREATE TABLE | 配置启用 AND SQL匹配CREATE_TABLE_LOCATION_SQL规则 | 返回false,填充错误信息到StringBuilder | +| BR-002 | 放行不带LOCATION的CREATE TABLE | 配置禁用 OR SQL不匹配规则 | 返回true | +| BR-003 | 不拦截ALTER TABLE SET LOCATION | SQL包含ALTER TABLE | 不匹配规则,返回true | +| BR-004 | 忽略注释中的LOCATION | LOCATION在注释中 | 通过SQLCommentHelper.dealComment处理后再检测 | + +**规则模式(参考现有DROP_TABLE、CREATE_DATABASE实现)**: +```scala +// 在SQLExplain中添加规则常量 +val CREATE_TABLE_LOCATION_SQL = "\\s*create\\s+table\\s+\\w+\\s.*?location\\s+'.*'\\s*" +``` + +--- + +## 1.4 设计决策记录 (ADR) + +### ADR-001: 拦截位置选择 + +- **状态**:已采纳 +- **背景**:需要在Linkis中拦截Hive CREATE TABLE语句的LOCATION参数,有多个可能的拦截位置 +- **决策**:选择在Entrance层的SQL解析阶段进行拦截 +- **选项对比**: + +| 选项 | 优点 | 缺点 | 适用场景 | +|-----|------|------|---------| +| **Entrance层** | 统一入口,所有任务必经;易于维护;不影响引擎 | 对所有Hive任务有轻微性能开销 | 当前方案(已采纳) | +| EngineConn层 | 更接近Hive引擎;拦截更精确 | 需要修改EngineConn代码;用户可能绕过 | 不采用 | +| Hive Server层 | 完全在Hive侧实现 | 需要修改Hive源码;升级困难 | 不采用 | + +- **结论**:选择Entrance层,因为它是所有任务的统一入口,无绕过风险,且易于维护 +- **影响**:Entrance模块需要新增拦截器逻辑,但不影响EngineConn和其他模块 + +### ADR-002: SQL解析方式 + +- **状态**:已采纳 +- **背景**:需要检测SQL中是否包含LOCATION关键字,有多种实现方式 +- **决策**:采用基于正则表达式的轻量级解析 +- **选项对比**: + +| 选项 | 优点 | 缺点 | 适用场景 | +|-----|------|------|---------| +| **正则表达式** | 实现简单;性能好;易于维护 | 可能存在边界情况 | 当前方案(已采纳) | +| Calcite解析器 | 解析准确;支持复杂SQL | 依赖重;性能开销大;学习成本高 | 不采用 | +| 字符串包含 | 最简单 | 误判率高(如字符串常量) | 不采用 | + +- **结论**:使用正则表达式 `(?i)\bCREATE\s+TABLE\b.*?\bLOCATION\b`,配合注释过滤 +- **影响**:需要充分的单元测试覆盖各种SQL模式 + +### ADR-003: 故障处理策略 + +- **状态**:已采纳 +- **背景**:拦截器本身可能发生异常(如配置读取失败),需要决定如何处理 +- **决策**:采用fail-open策略,拦截器异常时放行 +- **选项对比**: + +| 选项 | 优点 | 缺点 | 适用场景 | +|-----|------|------|---------| +| **fail-open(放行)** | 保证可用性;不影响业务 | 安全性降低 | 当前方案(已采纳) | +| fail-close(拒绝) | 安全性最高 | 可能影响所有任务 | 不采用 | + +- **结论**:fail-open,记录错误日志,告警通知运维 +- **影响**:拦截器异常时LOCATION控制失效,需要监控告警 + +--- + +# Part 2: 支撑设计 + +> 📐 **本层目标**:数据模型、配置策略、测试策略的结构化摘要。 +> +> **预计阅读时间**:5-10分钟 + +## 2.1 数据模型设计 + +### 2.1.1 配置数据模型 + +**说明**:Location控制配置项(通过CommonVars管理) + +| 配置键 | 类型 | 默认值 | 说明 | 约束 | +|-------|------|--------|------|------| +| `wds.linkis.hive.location.control.enable` | Boolean | false | 是否启用LOCATION控制 | 必须是true或false | + +### 2.1.2 规则模式数据模型 + +**说明**:SQL检测规则模式(预编译正则表达式) + +| 规则名称 | 正则模式 | 说明 | 示例匹配 | +|---------|---------|------|---------| +| CREATE_TABLE_LOCATION_SQL | `(?i)\s*create\s+(?:external\s+)?table\s+\S+\s.*?location\s+['"`]` | 匹配CREATE TABLE语句中的LOCATION子句 | `CREATE TABLE test LOCATION '/path'` | + +--- + +## 2.2 配置管理设计 + +### 2.2.1 配置项定义 + +| 配置项 | 类型 | 默认值 | 说明 | 修改生效方式 | +|-------|------|--------|------|-------------| +| `wds.linkis.hive.location.control.enable` | Boolean | false | 是否启用LOCATION控制 | 重启后生效(CommonVars热加载) | + +### 2.2.2 配置加载方式 + +``` +启动时: + 1. Entrance服务启动 + 2. SQLExplain对象初始化(Scala object) + 3. CommonVars读取配置文件 + 4. 配置值存储在CommonVars对象中(已实现缓存) + +运行时: + 1. SQLExplain.authPass被调用 + 2. 通过HIVE_LOCATION_CONTROL_ENABLE.getValue获取配置 + 3. CommonVars已实现缓存机制,无需额外处理 +``` + +### 2.2.3 配置验证 + +| 验证项 | 规则 | 错误处理 | +|-------|------|---------| +| enable类型 | 必须是Boolean | CommonVars自动处理,非法值使用默认值false | +| enable范围 | true或false | CommonVars自动处理,非法值视为false | + +--- + +## 2.3 日志记录设计 + +### 2.3.1 日志方式 + +**日志级别**:WARN(被拦截时) + +**日志格式**:使用LogUtils.generateWarn + +**实现方式**: +- 错误信息通过StringBuilder传递 +- SQLCodeCheckInterceptor收到false后,将StringBuilder内容包装到CodeCheckException中 +- 异常消息会被自动记录到日志 + +**日志输出示例**: +``` +2026-03-25 10:30:15.123 WARN SQLCodeCheckInterceptor - sql code check failed, reason is CREATE TABLE with LOCATION clause is not allowed. Please remove the LOCATION clause and retry. SQL: CREATE TABLE test (id INT) LOCATION '/user/data' +``` + +### 2.3.2 日志存储 + +| 存储方式 | 路径 | 保留策略 | +|---------|------|---------| +| 文件日志 | `{LINKIS_HOME}/logs/linkis-entrance.log` | 遵循Linkis日志轮转策略 | + +### 2.3.3 日志查询 + +**查询命令示例**: + +```bash +# 查询所有LOCATION相关的拦截记录 +grep "LOCATION clause is not allowed" linkis-entrance.log + +# 查询特定时间段的拦截记录 +grep "2026-03-25" linkis-entrance.log | grep "LOCATION clause is not allowed" + +# 统计拦截次数 +grep "LOCATION clause is not allowed" linkis-entrance.log | wc -l +``` + +--- + +## 2.4 性能优化设计 + +### 2.4.1 性能优化措施 + +| 优化项 | 优化方法 | 预期效果 | +|-------|---------|---------| +| 正则表达式预编译 | 启动时编译正则,缓存Pattern对象 | 避免每次解析重新编译,减少CPU开销 | +| 配置缓存 | 内存缓存配置对象,定时刷新 | 减少配置读取开销 | +| 快速返回 | 检测到非CREATE TABLE语句直接返回 | 减少不必要的解析 | +| 异步日志 | 审计日志异步写入 | 避免阻塞主流程 | + +### 2.4.2 性能指标 + +| 指标 | 目标值 | 测量方法 | +|-----|-------|---------| +| 单次拦截延迟 | <1ms | 微基准测试 | +| 整体任务延迟增加 | <3% | 对比启用前后的任务执行时间 | +| 吞吐量影响 | <2% | 压力测试对比 | +| 内存增加 | <20MB | JMX内存监控 | + +--- + +## 2.5 测试策略 + +### 2.5.1 单元测试 + +**测试类**:`SQLExplainSpec`(扩展现有测试类) + +| 测试用例 | 描述 | 预期结果 | +|---------|------|---------| +| testAuthPass_CreateTableWithLocation_ShouldBlock | 带LOCATION的CREATE TABLE应被拦截 | 返回false,error包含错误信息 | +| testAuthPass_CreateTableWithoutLocation_ShouldAllow | 不带LOCATION的CREATE TABLE应放行 | 返回true | +| testAuthPass_AlterTableSetLocation_ShouldAllow | ALTER TABLE SET LOCATION应放行 | 返回true | +| testAuthPass_ConfigDisabled_ShouldAllow | 配置禁用时应放行 | 返回true | +| testAuthPass_CTASWithLocation_ShouldBlock | CTAS带LOCATION应被拦截 | 返回false | +| testAuthPass_LocationInComment_ShouldAllow | 注释中的LOCATION应被忽略 | 返回true | +| testAuthPass_LocationInString_ShouldAllow | 字符串常量中的LOCATION应被忽略 | 返回true | +| testAuthPass_ExternalTableWithLocation_ShouldBlock | EXTERNAL TABLE带LOCATION应被拦截 | 返回false | + +**覆盖率目标**:>85% + +### 2.5.2 集成测试 + +**测试场景**: + +| 场景 | 步骤 | 预期结果 | +|-----|------|---------| +| 端到端拦截测试 | 提交带LOCATION的CREATE TABLE任务 | 任务被拒绝,返回CodeCheckException | +| 日志验证 | 检查日志文件 | 记录完整的拦截信息 | +| 配置修改测试 | 修改配置并重启Entrance | 新配置生效 | +| 性能测试 | 并发提交100个任务 | 吞吐量降低<2% | + +### 2.5.3 回归测试 + +**回归范围**: + +- SQLExplain现有规则测试(DROP TABLE、CREATE DATABASE等) +- SQLCodeCheckInterceptor功能测试 +- Hive引擎正常执行测试 +- 多用户并发任务测试 +- 不同Hive版本兼容性测试(1.x, 2.x, 3.x) + +--- + +# Part 3: 参考资料 + +> 📎 **本层目标**:完整代码示例、配置文件,供开发参考。 +> +> **预计阅读时间**:15-20分钟 + +## 3.1 类图 + +```mermaid +classDiagram + class SQLCodeCheckInterceptor { + +apply(jobRequest: JobRequest, logAppender: StringBuilder): JobRequest + } + + class SQLExplain { + -DROP_TABLE_SQL: String + -CREATE_DATABASE_SQL: String + -CREATE_TABLE_LOCATION_SQL: String + -HIVE_LOCATION_CONTROL_ENABLE: CommonVars + -LOCATION_PATTERN: Pattern + +authPass(code: String, error: StringBuilder): Boolean + +dealSQLLimit(): Unit + } + + class EntranceConfiguration { + +HIVE_LOCATION_CONTROL_ENABLE: CommonVars + } + + class CommonVars { + +getValue(): T + } + + SQLCodeCheckInterceptor --> SQLExplain : 调用 + SQLExplain --> EntranceConfiguration : 读取配置 + SQLExplain --> CommonVars : 使用 +``` + +--- + +## 3.2 完整代码示例 + +### 3.2.1 SQLExplain扩展实现 + +**文件路径**:`linkis-computation-governance/linkis-entrance/src/main/scala/org/apache/linkis/entrance/interceptor/impl/Explain.scala` + +**修改方式**:在现有的SQLExplain object中添加LOCATION检测规则 + +```scala +object SQLExplain extends Explain { + // ... 现有代码保持不变 ... + + // ========== 新增代码开始 ========== + // Hive LOCATION控制规则常量(参考现有DROP_TABLE_SQL、CREATE_DATABASE_SQL) + val CREATE_TABLE_LOCATION_SQL = "(?i)\\s*create\\s+(?:external\\s+)?table\\s+\\S+\\s.*?location\\s+['\"`](?:[^'\"`]|['\"`](?:[^'\"`]|['\"`](?:[^'\"`]|['\"`](?:[^'\"`]|['\"`](?:[^'\"`]|['\"`](?:[^'\"`]|['\"`](?:[^'\"`])*)*)*)*)*)*)*['\"`]\\s*" + + // LOCATION控制配置 + val HIVE_LOCATION_CONTROL_ENABLE: CommonVars[Boolean] = + CommonVars("wds.linkis.hive.location.control.enable", false) + + // 预编译正则表达式(性能优化) + private val LOCATION_PATTERN: Pattern = Pattern.compile(CREATE_TABLE_LOCATION_SQL) + // ========== 新增代码结束 ========== + + override def authPass(code: String, error: StringBuilder): Boolean = { + // 快速路径:配置未启用,直接放行 + if (!HIVE_LOCATION_CONTROL_ENABLE.getValue) { + return true + } + + Utils.tryCatch { + // 检查是否匹配CREATE TABLE LOCATION规则 + if (LOCATION_PATTERN.matcher(code).find()) { + error.append("CREATE TABLE with LOCATION clause is not allowed. " + + "Please remove the LOCATION clause and retry. " + + s"SQL: ${code.take(100)}...") + return false + } + + true + } { + case e: Exception => + logger.warn(s"Failed to check LOCATION in SQL: ${code.take(50)}", e) + // fail-open策略:异常时放行 + true + } + } + + // ... 现有代码(dealSQLLimit等方法)保持不变 ... +} +``` + +**说明**: +- 在SQLExplain object中添加规则常量CREATE_TABLE_LOCATION_SQL +- 添加配置项HIVE_LOCATION_CONTROL_ENABLE +- 在authPass方法中添加LOCATION检测逻辑 +- 使用现有的StringBuilder参数传递错误信息 +- 异常时采用fail-open策略(返回true) +- 完全复用现有的SQLCodeCheckInterceptor流程 + +### 3.2.2 EntranceConfiguration扩展 + +**文件路径**:`linkis-computation-governance/linkis-entrance/src/main/scala/org/apache/linkis/entrance/conf/EntranceConfiguration.scala` + +```scala +object EntranceConfiguration { + // ... 现有配置保持不变 ... + + // ========== 新增配置开始 ========== + /** + * 是否启用Hive表LOCATION路径控制 + * 默认值:false(禁用) + * 说明:启用后,将拦截CREATE TABLE语句中的LOCATION参数 + */ + val HIVE_LOCATION_CONTROL_ENABLE: CommonVars[Boolean] = + CommonVars("wds.linkis.hive.location.control.enable", false) + // ========== 新增配置结束 ========== +} +``` + +### 3.2.3 SQLCodeCheckInterceptor(无需修改) + +**说明**:SQLCodeCheckInterceptor无需修改,它会自动调用SQLExplain.authPass + +**现有实现(保持不变)**: +```scala +class SQLCodeCheckInterceptor extends EntranceInterceptor { + override def apply(jobRequest: JobRequest, logAppender: java.lang.StringBuilder): JobRequest = { + // ... 现有代码 ... + val isAuth: Boolean = SQLExplain.authPass(jobRequest.getExecutionCode, sb) + if (!isAuth) { + throw CodeCheckException(20051, "sql code check failed, reason is " + sb.toString()) + } + // ... 现有代码 ... + } +} +``` + +**说明**: +- SQLCodeCheckInterceptor会调用SQLExplain.authPass +- authPass返回false时,会抛出CodeCheckException +- 错误信息通过StringBuilder传递,最终包含在异常消息中 + +--- + +## 3.3 配置文件示例 + +### 3.3.1 linkis.properties + +**文件路径**:`{LINKIS_HOME}/conf/linkis.properties` + +```properties +# ============================================ +# Hive Location Control Configuration +# ============================================ + +# 是否启用location控制(禁止CREATE TABLE指定LOCATION) +# 默认值:false(禁用) +# 启用后,将拦截所有包含LOCATION子句的CREATE TABLE语句 +wds.linkis.hive.location.control.enable=false +``` + +### 3.3.2 配置说明 + +**配置项**:`wds.linkis.hive.location.control.enable` + +**可选值**: +- `true`:启用LOCATION控制,拦截CREATE TABLE语句中的LOCATION子句 +- `false`:禁用LOCATION控制(默认值) + +**生效方式**:重启Entrance服务后生效 + +**使用场景**: +- 生产环境:建议启用(设置为true),防止用户指定LOCATION +- 开发/测试环境:可以禁用(设置为false),方便开发调试 + +--- + +## 3.4 单元测试示例 + +**测试文件路径**:`linkis-computation-governance/linkis-entrance/src/test/scala/org/apache/linkis/entrance/interceptor/impl/SQLExplainSpec.scala` + +```scala +package org.apache.linkis.entrance.interceptor.impl + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class SQLExplainSpec extends AnyFlatSpec with Matchers { + + "SQLExplain" should "block CREATE TABLE with LOCATION" in { + val error = new StringBuilder() + val sql = "CREATE TABLE test (id INT) LOCATION '/user/data'" + + // 先启用配置 + SQLExplain.HIVE_LOCATION_CONTROL_ENABLE.setValue(true) + + val result = SQLExplain.authPass(sql, error) + + result shouldBe false + error.toString should include ("LOCATION clause is not allowed") + } + + it should "allow CREATE TABLE without LOCATION" in { + val error = new StringBuilder() + val sql = "CREATE TABLE test (id INT)" + + SQLExplain.HIVE_LOCATION_CONTROL_ENABLE.setValue(true) + + val result = SQLExplain.authPass(sql, error) + + result shouldBe true + error.toString shouldBe empty + } + + it should "allow ALTER TABLE SET LOCATION" in { + val error = new StringBuilder() + val sql = "ALTER TABLE test SET LOCATION '/user/data'" + + SQLExplain.HIVE_LOCATION_CONTROL_ENABLE.setValue(true) + + val result = SQLExplain.authPass(sql, error) + + result shouldBe true + } + + it should "allow when config is disabled" in { + val error = new StringBuilder() + val sql = "CREATE TABLE test (id INT) LOCATION '/user/data'" + + SQLExplain.HIVE_LOCATION_CONTROL_ENABLE.setValue(false) + + val result = SQLExplain.authPass(sql, error) + + result shouldBe true + } + + it should "block CTAS with LOCATION" in { + val error = new StringBuilder() + val sql = "CREATE TABLE test AS SELECT * FROM other LOCATION '/user/data'" + + SQLExplain.HIVE_LOCATION_CONTROL_ENABLE.setValue(true) + + val result = SQLExplain.authPass(sql, error) + + result shouldBe false + } + + it should "ignore LOCATION in comments" in { + val error = new StringBuilder() + val sql = "-- CREATE TABLE test LOCATION '/path'\nCREATE TABLE test (id INT)" + + SQLExplain.HIVE_LOCATION_CONTROL_ENABLE.setValue(true) + + val result = SQLExplain.authPass(sql, error) + + result shouldBe true + } + + it should "block EXTERNAL TABLE with LOCATION" in { + val error = new StringBuilder() + val sql = "CREATE EXTERNAL TABLE test (id INT) LOCATION '/user/data'" + + SQLExplain.HIVE_LOCATION_CONTROL_ENABLE.setValue(true) + + val result = SQLExplain.authPass(sql, error) + + result shouldBe false + } + + it should "allow LOCATION in string constants" in { + val error = new StringBuilder() + val sql = "SELECT * FROM test WHERE comment = 'this location is ok'" + + SQLExplain.HIVE_LOCATION_CONTROL_ENABLE.setValue(true) + + val result = SQLExplain.authPass(sql, error) + + result shouldBe true + } +} +``` + +--- + +## 附录A:部署指南 + +### A.1 编译打包 + +```bash +# 1. 编译Entrance模块 +cd linkis-computation-governance/linkis-entrance +mvn clean package -DskipTests + +# 输出:linkis-entrance-1.19.0.jar +``` + +### A.2 配置部署 + +```bash +# 1. 备份现有配置 +cp $LINKIS_HOME/conf/linkis.properties $LINKIS_HOME/conf/linkis.properties.bak + +# 2. 添加配置项(可选,默认为false禁用) +echo "" >> $LINKIS_HOME/conf/linkis.properties +echo "# Hive Location Control" >> $LINKIS_HOME/conf/linkis.properties +echo "wds.linkis.hive.location.control.enable=false" >> $LINKIS_HOME/conf/linkis.properties + +# 3. 验证配置 +grep "wds.linkis.hive.location.control.enable" $LINKIS_HOME/conf/linkis.properties +``` + +### A.3 启动验证 + +```bash +# 1. 重启Entrance服务 +sh $LINKIS_HOME/sbin/linkis-daemon.sh restart entrance + +# 2. 检查日志 +tail -f $LINKIS_HOME/logs/linkis-entrance.log + +# 3. 提交测试任务(启用配置后) +# 使用beeline或linkis-client提交带LOCATION的CREATE TABLE语句 +# 预期结果:被拒绝并返回错误信息 +``` + +--- + +## 附录B:监控指标 + +### B.1 Prometheus指标 + +```scala +// 拦截次数 +location_intercept_total{user="zhangsan", sql_type="CREATE TABLE"} 100 + +// 拦截成功率 +location_intercept_success_ratio 1.0 + +// 拦截延迟(毫秒) +location_intercept_latency_ms{quantile="p50"} 0.5 +location_intercept_latency_ms{quantile="p99"} 2.0 +``` + +### B.2 Grafana面板 + +**面板1:拦截统计** +- 拦截总次数(折线图) +- 拦截用户分布(饼图) +- 拦截SQL类型分布(柱状图) + +**面板2:性能监控** +- 拦截延迟P50/P99(折线图) +- 任务吞吐量对比(启用前后) + +--- + +## 附录C:常见问题 + +### Q1: 如何启用location控制? + +A: 修改配置文件 `$LINKIS_HOME/conf/linkis.properties`,设置: +```properties +wds.linkis.hive.location.control.enable=true +``` +然后重启Entrance服务。 + +### Q2: 如何查询拦截日志? + +A: 使用grep命令: +```bash +grep "LOCATION clause is not allowed" $LINKIS_HOME/logs/linkis-entrance.log +``` + +### Q3: SQLExplain异常会影响正常任务吗? + +A: 不会。SQLExplain采用fail-open策略,异常时返回true放行任务,保证可用性优先。 + +### Q4: 支持哪些Hive版本? + +A: 支持Hive 1.x、2.x、3.x,因为仅基于SQL关键字检测,不依赖Hive API。 + +### Q5: 与现有规则(DROP TABLE、CREATE DATABASE)有什么区别? + +A: 实现方式完全一致,都是在SQLExplain中添加规则常量和检测逻辑,复用SQLCodeCheckInterceptor的调用流程。 + +--- + +**文档变更记录** + +| 版本 | 日期 | 变更内容 | 作者 | +|------|------|---------|------| +| v1.0 | 2026-03-25 | 初始版本 | AI设计生成 | diff --git a/docs/dev-1.19.0-yarn-tag-update/features/hive_location_control.feature b/docs/dev-1.19.0-yarn-tag-update/features/hive_location_control.feature new file mode 100644 index 0000000000..3133aa2898 --- /dev/null +++ b/docs/dev-1.19.0-yarn-tag-update/features/hive_location_control.feature @@ -0,0 +1,181 @@ +# language: zh-CN +功能: Hive表Location路径控制 + + 作为 数据平台管理员 + 我希望能够禁止用户在CREATE TABLE语句中指定LOCATION参数 + 以防止用户通过指定LOCATION路径创建表,保护数据安全 + + 背景: + Given Entrance服务已启动 + And location控制功能已启用 + + # ===== P0功能:拦截带LOCATION的CREATE TABLE ===== + + 场景: 不带LOCATION的CREATE TABLE(成功) + When 用户执行SQL: + """ + CREATE TABLE test_table ( + id INT, + name STRING + ) + """ + Then 表创建成功 + And 不记录拦截日志 + + 场景: 带LOCATION的CREATE TABLE(被拦截) + When 用户执行SQL: + """ + CREATE TABLE test_table ( + id INT, + name STRING + ) + LOCATION '/user/hive/warehouse/test_table' + """ + Then 表创建失败 + And 错误信息包含: "Location parameter is not allowed in CREATE TABLE statement" + And 审计日志记录: "sql_type=CREATE_TABLE, location=/user/hive/warehouse/test_table, is_blocked=true" + + # ===== P0功能:功能开关 ===== + + 场景: 禁用location控制后允许带LOCATION的CREATE TABLE + Given location控制功能已禁用 + When 用户执行SQL: + """ + CREATE TABLE test_table ( + id INT, + name STRING + ) + LOCATION '/any/path/test_table' + """ + Then 表创建成功 + And 不执行location拦截 + + # ===== P1功能:CTAS语句 ===== + + 场景: CTAS未指定location(成功) + When 用户执行SQL: + """ + CREATE TABLE test_table AS + SELECT * FROM source_table + """ + Then 表创建成功 + And 不记录拦截日志 + + 场景: CTAS指定location(被拦截) + When 用户执行SQL: + """ + CREATE TABLE test_table + LOCATION '/user/hive/warehouse/test_table' + AS + SELECT * FROM source_table + """ + Then 表创建失败 + And 错误信息包含: "Location parameter is not allowed in CREATE TABLE statement" + And 审计日志记录: "sql_type=CTAS, location=/user/hive/warehouse/test_table, is_blocked=true" + + # ===== 不在范围:ALTER TABLE ===== + + 场景: ALTER TABLE SET LOCATION(不拦截) + When 用户执行SQL: + """ + ALTER TABLE test_table SET LOCATION '/user/hive/warehouse/new_table' + """ + Then 操作不被拦截 + And 执行结果由Hive引擎决定 + + # ===== 边界场景 ===== + + 场景: CREATE TEMPORARY TABLE with LOCATION(被拦截) + When 用户执行SQL: + """ + CREATE TEMPORARY TABLE temp_table ( + id INT + ) + LOCATION '/tmp/hive/temp_table' + """ + Then 表创建失败 + And 错误信息包含: "Location parameter is not allowed in CREATE TABLE statement" + + 场景: CREATE EXTERNAL TABLE with LOCATION(被拦截) + When 用户执行SQL: + """ + CREATE EXTERNAL TABLE external_table ( + id INT, + name STRING + ) + LOCATION '/user/hive/warehouse/external_table' + """ + Then 表创建失败 + And 错误信息包含: "Location parameter is not allowed in CREATE TABLE statement" + + 场景: 多行SQL格式带LOCATION(被拦截) + When 用户执行SQL: + """ + CREATE TABLE test_table + ( + id INT COMMENT 'ID', + name STRING COMMENT 'Name' + ) + COMMENT 'Test table' + LOCATION '/user/hive/warehouse/test_table' + """ + Then 表创建失败 + And 错误信息包含: "Location parameter is not allowed in CREATE TABLE statement" + + # ===== 性能测试场景 ===== + + 场景: 大量并发建表操作(不带LOCATION) + When 100个用户并发执行: + """ + CREATE TABLE test_table (id INT) + """ + Then 所有操作成功 + And 性能影响<3% + + 场景: 大量并发建表操作(带LOCATION) + When 100个用户并发执行: + """ + CREATE TABLE test_table (id INT) LOCATION '/any/path' + """ + Then 所有操作都被拦截 + And 性能影响<3% + + # ===== 错误处理场景 ===== + + 场景: SQL语法错误 + When 用户执行SQL: + """ + CREATE TABLE test_table ( + id INT + ) LOCATIO '/invalid/path' + """ + Then SQL解析失败 + And 返回语法错误信息 + + 场景: 空SQL语句 + When 用户执行空SQL + Then 不执行location检查 + And 返回SQL为空的错误 + + # ===== 审计日志完整性 ===== + + 场景: 验证所有被拦截的操作都有审计日志 + Given 用户执行以下操作: + | SQL类型 | Location路径 | + | CREATE_TABLE | /user/hive/warehouse/table1 | + | CREATE_TABLE | /invalid/path | + | CTAS | /user/data/table2 | + When 检查审计日志 + Then 所有被拦截的操作都有日志记录 + And 日志包含: timestamp, user, sql_type, location_path, is_blocked, reason + + # ===== 错误信息清晰度测试 ===== + + 场景: 验证错误信息包含原始SQL + When 用户执行SQL: + """ + CREATE TABLE test_table (id INT) LOCATION '/user/critical/data' + """ + Then 表创建失败 + And 错误信息包含: "Please remove the LOCATION clause and retry" + And 错误信息包含原始SQL片段 diff --git "a/docs/dev-1.19.0-yarn-tag-update/requirements/hive_location_control_\351\234\200\346\261\202.md" "b/docs/dev-1.19.0-yarn-tag-update/requirements/hive_location_control_\351\234\200\346\261\202.md" new file mode 100644 index 0000000000..7c39d9aa5c --- /dev/null +++ "b/docs/dev-1.19.0-yarn-tag-update/requirements/hive_location_control_\351\234\200\346\261\202.md" @@ -0,0 +1,313 @@ +# Hive表Location路径控制 - 需求文档 + +## 文档信息 + +| 项目 | 内容 | +|------|------| +| 需求ID | LINKIS-ENHANCE-HIVE-LOCATION-001 | +| 需求名称 | Hive表Location路径控制 | +| 需求类型 | 功能增强(ENHANCE) | +| 优先级 | P1(高优先级) | +| 涉及模块 | linkis-computation-governance/linkis-entrance | +| 文档版本 | v2.0 | +| 创建时间 | 2026-03-25 | +| 最后更新 | 2026-03-25 | + +--- + +## 一、功能概述 + +### 1.1 功能名称 + +Hive表Location路径控制 + +### 1.2 一句话描述 + +在Entrance层拦截Hive CREATE TABLE语句中的LOCATION参数,防止用户通过指定LOCATION路径创建表,保护数据安全。 + +### 1.3 功能背景 + +**当前痛点**: +- 用户可以通过CREATE TABLE语句指定LOCATION参数,将表数据存储在任意HDFS路径 +- 可能导致关键业务表数据被误删或恶意修改 +- 威胁数据安全性和业务稳定性 +- 缺乏统一的安全控制机制 + +**影响范围**: +- 所有通过Linkis执行的Hive任务(交互式查询和批量任务) +- 生产环境存在数据安全风险 + +### 1.4 期望价值 + +**主要价值**: +- 防止用户恶意或误操作通过LOCATION指定路径创建Hive表 +- 统一在Entrance层进行拦截,避免用户绕过控制 +- 保护核心业务数据安全,提升系统安全性 + +**次要价值**: +- 提供完整的操作审计日志,满足合规要求 +- 简单的配置机制,易于部署和维护 +- 清晰的错误提示,提升用户体验 + +--- + +## 二、功能范围 + +### 2.1 核心功能(P0) + +| 功能点 | 描述 | 验收标准 | +|-------|------|---------| +| **LOCATION参数拦截** | 在Entrance层拦截CREATE TABLE语句中的LOCATION参数 | 所有包含LOCATION的CREATE TABLE语句被拦截 | +| **功能开关** | 提供配置项开关,允许管理员启用/禁用该功能 | 开关控制生效,禁用时不影响现有功能 | +| **错误提示** | 返回明确的错误信息,说明为什么被拦截 | 错误信息清晰,指导用户正确操作 | + +### 2.2 增强功能(P1) + +| 功能点 | 描述 | 验收标准 | +|-------|------|---------| +| **审计日志** | 记录所有被拦截的LOCATION操作 | 所有被拦截的操作都有日志记录 | +| **CTAS语句拦截** | 拦截CREATE TABLE AS SELECT中的LOCATION参数 | CTAS语句中的LOCATION被正确拦截 | + +### 2.3 不在范围内 + +- **不拦截ALTER TABLE语句的SET LOCATION操作**(仅拦截CREATE TABLE) +- **不提供任何白名单或豁免机制**(完全禁止指定LOCATION) +- **不影响非LOCATION相关的Hive操作** +- **不涉及跨引擎的控制**(仅限Hive引擎) +- **不拦截CTAS语句**(CREATE TABLE AS SELECT,不指定LOCATION的情况) + +--- + +## 三、详细功能需求 + +### 3.1 拦截规则 + +#### 3.1.1 需要拦截的SQL语句 + +| SQL类型 | 示例 | 是否拦截 | +|---------|------|---------| +| CREATE TABLE ... LOCATION | `CREATE TABLE t ... LOCATION '/path'` | **拦截** | +| CTAS with LOCATION | `CREATE TABLE t AS SELECT ... LOCATION '/path'` | **拦截** | +| CREATE TABLE without LOCATION | `CREATE TABLE t ...` (不指定LOCATION) | **放行** | +| CTAS without LOCATION | `CREATE TABLE t AS SELECT ...` (不指定LOCATION) | **放行** | +| ALTER TABLE ... SET LOCATION | `ALTER TABLE t SET LOCATION '/path'` | **不拦截** | + +#### 3.1.2 拦截逻辑 + +**拦截条件**(同时满足): +1. 启用了location控制功能(`hive.location.control.enable=true`) +2. SQL语句是CREATE TABLE或CREATE TABLE AS SELECT +3. 语句中包含LOCATION关键字 + +**拦截动作**: +1. 在Entrance层进行SQL解析 +2. 检测到LOCATION关键字时,拒绝执行该SQL +3. 返回明确的错误信息给用户 + +#### 3.1.3 拦截错误信息 + +``` +错误信息模板: +Location parameter is not allowed in CREATE TABLE statement. +Please remove the LOCATION clause and retry. +SQL: [原始SQL语句] +Reason: To protect data security, specifying LOCATION in CREATE TABLE is disabled. +``` + +### 3.2 配置项 + +#### 3.2.1 配置项设计 + +| 配置项名称 | 类型 | 默认值 | 说明 | +|-----------|------|--------|------| +| `hive.location.control.enable` | Boolean | false | 是否启用location控制(禁止CREATE TABLE指定LOCATION) | + +#### 3.2.2 配置示例 + +```properties +# 启用location控制 +hive.location.control.enable=true +``` + +### 3.3 审计日志 + +#### 3.3.1 日志内容 + +| 字段 | 说明 | +|------|------| +| timestamp | 操作时间 | +| user | 执行用户 | +| sql_type | SQL类型(CREATE TABLE / CTAS) | +| location_path | location路径(如果有) | +| is_blocked | 是否被拦截(true) | +| reason | 拦截原因 | + +#### 3.3.2 日志示例 + +``` +2026-03-25 10:30:15 | user=zhangsan | sql_type=CREATE TABLE | location=/user/data/test | is_blocked=true | reason=Location parameter not allowed +2026-03-25 10:31:20 | user=lisi | sql_type=CTAS | location=/user/critical/data | is_blocked=true | reason=Location parameter not allowed +``` + +--- + +## 四、非功能需求 + +### 4.1 性能要求 + +| 指标 | 目标值 | 测量方法 | +|------|--------|---------| +| 解析延迟 | <3% | 对比启用前后的任务执行时间 | +| 吞吐量影响 | <2% | 对比启用前后的任务吞吐量 | +| 内存增加 | <20MB | 测量Entrance进程内存增量 | + +### 4.2 可靠性要求 + +| 指标 | 目标值 | +|------|--------| +| 拦截成功率 | 100% | +| 误报率 | 0%(不误拦截合法操作) | +| 审计日志完整性 | 100% | + +### 4.3 可用性要求 + +| 指标 | 目标值 | +|------|--------| +| 配置生效时间 | 重启后生效 | +| 不影响现有功能 | 100%兼容 | +| 向后兼容性 | 支持Hive 1.x/2.x/3.x | + +### 4.4 安全性要求 + +| 指标 | 目标值 | +|------|--------| +| 绕过拦截 | 0个漏洞 | +| 配置修改权限 | 仅管理员可修改 | +| 审计日志防篡改 | 日志不可修改 | + +--- + +## 五、技术要求 + +### 5.1 技术栈 + +| 技术项 | 版本 | +|--------|------| +| Java | 1.8 | +| Scala | 2.11.12 / 2.12.17 | +| Hive | 2.3.3(兼容1.x和3.x) | +| Spring Boot | 2.7.12 | + +### 5.2 实现方案 + +**实现位置**:linkis-computation-governance/linkis-entrance + +**实现方式**:在Entrance层的SQL解析阶段进行拦截 + +**关键组件**: +1. `HiveLocationControlInterceptor`:拦截器,负责检测CREATE TABLE语句中的LOCATION参数 +2. `LocationControlConfig`:配置管理器,负责加载配置 +3. `LocationAuditLogger`:审计日志记录器 + +**集成点**: +- 与Linkis配置中心集成 +- 与Linkis审计日志集成(统一日志格式) + +### 5.3 代码规范 + +- 遵循Apache Linkis代码规范 +- 遵循Scala/Java编码规范 +- 单元测试覆盖率 >80% +- 关键逻辑必须有集成测试 + +--- + +## 六、验收标准 + +### 6.1 功能验收 + +| 场景 | 操作 | 预期结果 | +|------|------|---------| +| 普通建表(无LOCATION) | CREATE TABLE t (id int) | 成功创建 | +| 带LOCATION建表被拦截 | CREATE TABLE t ... LOCATION '/path' | 拒绝执行,返回错误信息 | +| CTAS无LOCATION | CREATE TABLE t AS SELECT ... | 成功创建 | +| CTAS带LOCATION被拦截 | CREATE TABLE t AS SELECT ... LOCATION '/path' | 拒绝执行,返回错误信息 | +| 功能开关禁用 | 禁用location控制后执行带LOCATION的建表 | 成功执行(不拦截) | +| 功能开关启用 | 启用location控制后执行带LOCATION的建表 | 拒绝执行 | +| 审计日志 | 执行被拦截的操作 | 记录审计日志 | + +### 6.2 性能验收 + +| 测试项 | 测试方法 | 通过标准 | +|--------|---------|---------| +| 解析延迟 | 执行1000次建表操作,对比启用前后 | 延迟增加<3% | +| 吞吐量 | 并发执行100个任务,对比吞吐量 | 吞吐量降低<2% | +| 内存占用 | 测量Entrance进程内存 | 内存增加<20MB | + +### 6.3 安全验收 + +| 测试项 | 测试方法 | 通过标准 | +|--------|---------|---------| +| 拦截测试 | 尝试各种带LOCATION的CREATE TABLE语句 | 100%拦截成功 | +| 审计完整性 | 检查所有被拦截操作的日志 | 100%记录完整 | + +--- + +## 七、风险与依赖 + +### 7.1 技术风险 + +| 风险 | 影响 | 缓解措施 | +|------|------|---------| +| SQL解析复杂度 | 复杂SQL可能解析失败 | 使用成熟的SQL解析器 | +| 性能影响 | 频繁解析可能影响性能 | 优化解析逻辑,避免重复解析 | + +### 7.2 依赖项 + +| 依赖 | 类型 | 说明 | +|------|------|------| +| Linkis配置中心 | 功能依赖 | 用于配置管理 | +| Linkis审计日志 | 功能依赖 | 用于统一日志记录 | + +### 7.3 限制条件 + +- 仅支持Hive引擎,不支持其他引擎 +- 仅拦截CREATE TABLE语句,不拦截ALTER TABLE +- 不支持任何形式的白名单或豁免 + +--- + +## 八、参考文档 + +- Apache Hive官方文档:https://cwiki.apache.org/confluence/display/Hive +- Linkis官方文档:https://linkis.apache.org/ +- Linkis Entrance开发指南:`docs/linkis-entrance-development-guide.md` + +--- + +## 附录 + +### 附录A:术语表 + +| 术语 | 定义 | +|------|------| +| Location | Hive表的存储路径,可以是HDFS或本地路径 | +| Entrance | Linkis的任务入口服务,负责接收和调度任务 | +| CTAS | CREATE TABLE AS SELECT,创建表并填充数据 | + +### 附录B:配置清单 + +完整配置项列表见 3.2.1 配置项设计。 + +### 附录C:测试用例清单 + +详细测试用例见测试文档:`docs/dev-1.19.0-yarn-tag-update/testing/hive_location_control_测试用例.md` + +--- + +**文档变更记录** + +| 版本 | 日期 | 变更内容 | 作者 | +|------|------|---------|------| +| v1.0 | 2026-03-25 | 初始版本(基于白名单方案) | AI需求分析 | +| v2.0 | 2026-03-25 | 移除白名单机制,简化为Entrance层拦截 | AI需求分析 | diff --git a/linkis-computation-governance/linkis-entrance/src/main/scala/org/apache/linkis/entrance/conf/EntranceConfiguration.scala b/linkis-computation-governance/linkis-entrance/src/main/scala/org/apache/linkis/entrance/conf/EntranceConfiguration.scala index cd420a695d..06e4952d50 100644 --- a/linkis-computation-governance/linkis-entrance/src/main/scala/org/apache/linkis/entrance/conf/EntranceConfiguration.scala +++ b/linkis-computation-governance/linkis-entrance/src/main/scala/org/apache/linkis/entrance/conf/EntranceConfiguration.scala @@ -420,7 +420,6 @@ object EntranceConfiguration { var SPARK_DYNAMIC_ALLOCATION_ENABLED = CommonVars.apply("spark.dynamic.allocation.enabled", false).getValue - var SPARK_DYNAMIC_ALLOCATION_ADDITIONAL_CONFS = CommonVars.apply("spark.dynamic.allocation.additional.confs", "").getValue @@ -453,4 +452,11 @@ object EntranceConfiguration { val TASK_DIAGNOSIS_TIMEOUT_SCAN = CommonVars("linkis.task.diagnosis.timeout.scan", "2m").getValue + /** + * Whether to enable Hive table LOCATION path control Default value: false (disabled) Description: + * When enabled, CREATE TABLE statements with LOCATION clause will be blocked + */ + val HIVE_LOCATION_CONTROL_ENABLE: CommonVars[Boolean] = + CommonVars("wds.linkis.hive.location.control.enable", false) + } diff --git a/linkis-computation-governance/linkis-entrance/src/main/scala/org/apache/linkis/entrance/interceptor/impl/Explain.scala b/linkis-computation-governance/linkis-entrance/src/main/scala/org/apache/linkis/entrance/interceptor/impl/Explain.scala index 14e327bdf7..8bdc4192c2 100644 --- a/linkis-computation-governance/linkis-entrance/src/main/scala/org/apache/linkis/entrance/interceptor/impl/Explain.scala +++ b/linkis-computation-governance/linkis-entrance/src/main/scala/org/apache/linkis/entrance/interceptor/impl/Explain.scala @@ -94,6 +94,10 @@ object SQLExplain extends Explain { val DROP_TABLE_SQL = "\\s*drop\\s+table\\s+\\w+\\s*" val CREATE_DATABASE_SQL = "\\s*create\\s+database\\s+\\w+\\s*" + // Hive LOCATION control configuration + val HIVE_LOCATION_CONTROL_ENABLE: CommonVars[Boolean] = + CommonVars("wds.linkis.hive.location.control.enable", false) + private val IDE_ALLOW_NO_LIMIT_REGEX = "--set\\s*ide.engine.no.limit.allow\\s*=\\s*true".r.unanchored @@ -106,7 +110,44 @@ object SQLExplain extends Explain { private val LOG: Logger = LoggerFactory.getLogger(getClass) override def authPass(code: String, error: StringBuilder): Boolean = { - true + Utils.tryCatch { + // Fast path: if location control is disabled, pass through immediately + if (!HIVE_LOCATION_CONTROL_ENABLE.getHotValue) { + return true + } + + // Handle null or empty code + if (code == null || code.trim.isEmpty) { + return true + } + + // Check if the SQL contains CREATE TABLE with LOCATION clause + val cleanedCode = SQLCommentHelper.dealComment(code) + + // Simple regex to match: CREATE TABLE ... LOCATION '...' + // Case-insensitive, supports EXTERNAL TABLE, handles quotes (single, double, backtick) + // Uses DOTALL to match across newlines + val locationPattern = + "(?is)create\\s+(?:external\\s+)?table\\s+\\S+.*?location\\s+['\"`].*?['\"`]".r + + if (locationPattern.findFirstIn(cleanedCode).isDefined) { + error + .append("CREATE TABLE with LOCATION clause is not allowed. ") + .append("Please remove the LOCATION clause and retry. ") + .append(s"SQL: ${if (code.length > 100) code.take(100) + "..." else code}") + return false + } + + true + } { case e: Exception => + logger.warn( + s"Failed to check LOCATION in SQL: ${if (code != null && code.length > 50) code.take(50) + "..." + else code}", + e + ) + // Fail-open strategy: return true on exception to ensure availability + true + } } /** diff --git a/linkis-computation-governance/linkis-entrance/src/test/java/org/apache/linkis/entrance/interceptor/impl/SQLExplainTest.java b/linkis-computation-governance/linkis-entrance/src/test/java/org/apache/linkis/entrance/interceptor/impl/SQLExplainTest.java index c5efb5633e..a55740a6ee 100644 --- a/linkis-computation-governance/linkis-entrance/src/test/java/org/apache/linkis/entrance/interceptor/impl/SQLExplainTest.java +++ b/linkis-computation-governance/linkis-entrance/src/test/java/org/apache/linkis/entrance/interceptor/impl/SQLExplainTest.java @@ -17,11 +17,15 @@ package org.apache.linkis.entrance.interceptor.impl; +import org.apache.linkis.common.conf.BDPConfiguration; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class SQLExplainTest { + private static final String CONFIG_KEY = "wds.linkis.hive.location.control.enable"; + @Test void isSelectCmdNoLimit() { @@ -52,4 +56,242 @@ void isSelectOverLimit() { res = SQLExplain.isSelectOverLimit(code); Assertions.assertEquals(false, res); } + + // ===== Hive Location Control Tests ===== + + @Test + void testBlockCreateTableWithLocation() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "CREATE TABLE test (id INT) LOCATION '/user/data'"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertFalse(result); + Assertions.assertTrue(error.toString().contains("LOCATION clause is not allowed")); + } + + @Test + void testAllowCreateTableWithoutLocation() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "CREATE TABLE test (id INT)"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertTrue(result); + Assertions.assertEquals("", error.toString()); + } + + @Test + void testAllowAlterTableSetLocation() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "ALTER TABLE test SET LOCATION '/user/data'"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertTrue(result); + Assertions.assertEquals("", error.toString()); + } + + @Test + void testAllowWhenConfigDisabled() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "CREATE TABLE test (id INT) LOCATION '/user/data'"; + + BDPConfiguration.set(CONFIG_KEY, "false"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertTrue(result); + Assertions.assertEquals("", error.toString()); + } + + @Test + void testBlockExternalTableWithLocation() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "CREATE EXTERNAL TABLE test (id INT) LOCATION '/user/data'"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertFalse(result); + Assertions.assertTrue(error.toString().contains("LOCATION clause is not allowed")); + } + + @Test + void testIgnoreLocationInComments() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "-- CREATE TABLE test LOCATION '/path'\nCREATE TABLE test (id INT)"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertTrue(result); + Assertions.assertEquals("", error.toString()); + } + + @Test + void testAllowLocationInStringConstants() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "SELECT * FROM test WHERE comment = 'this location is ok'"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertTrue(result); + Assertions.assertEquals("", error.toString()); + } + + @Test + void testHandleEmptySQL() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = ""; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertTrue(result); + } + + @Test + void testHandleNullSQL() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = null; + + BDPConfiguration.set(CONFIG_KEY, "true"); + // Should not throw exception and should return true (fail-open) + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertTrue(result); + } + + @Test + void testCaseInsensitiveForCreateTable() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "create table test (id int) location '/user/data'"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertFalse(result); + Assertions.assertTrue(error.toString().contains("LOCATION clause is not allowed")); + } + + @Test + void testCaseInsensitiveForLocation() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "CREATE TABLE test (id INT) location '/user/data'"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertFalse(result); + Assertions.assertTrue(error.toString().contains("LOCATION clause is not allowed")); + } + + @Test + void testMultiLineCreateTableWithLocation() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = + "CREATE TABLE test (\n" + + " id INT,\n" + + " name STRING\n" + + ")\n" + + "LOCATION '/user/data'"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertFalse(result); + Assertions.assertTrue(error.toString().contains("LOCATION clause is not allowed")); + } + + @Test + void testAllowCreateTableWithOtherClauses() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "CREATE TABLE test (id INT) PARTITIONED BY (dt STRING) STORED AS PARQUET"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertTrue(result); + Assertions.assertEquals("", error.toString()); + } + + @Test + void testHandleLocationWithDoubleQuotes() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "CREATE TABLE test (id INT) LOCATION \"/user/data\""; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertFalse(result); + Assertions.assertTrue(error.toString().contains("LOCATION clause is not allowed")); + } + + @Test + void testHandleLocationWithBackticks() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "CREATE TABLE test (id INT) LOCATION `/user/data`"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertFalse(result); + Assertions.assertTrue(error.toString().contains("LOCATION clause is not allowed")); + } + + @Test + void testTruncateLongSQLErrorMessage() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String longSql = + "CREATE TABLE test (id INT) LOCATION '/user/very/long/path/" + + "that/keeps/going/on/and/on/forever/and/ever/because/it/is/just/so/long/" + + "and/needs/to/be/truncated/in/the/error/message'"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(longSql, error); + + Assertions.assertFalse(result); + Assertions.assertFalse(error.toString().contains(longSql)); + Assertions.assertTrue(error.toString().contains("...")); + } + + @Test + void testNotBlockInsertStatements() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "INSERT INTO TABLE test VALUES (1, 'test')"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertTrue(result); + Assertions.assertEquals("", error.toString()); + } + + @Test + void testNotBlockSelectStatements() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "SELECT * FROM test WHERE id > 100"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertTrue(result); + Assertions.assertEquals("", error.toString()); + } + + @Test + void testNotBlockDropTableStatements() { + scala.collection.mutable.StringBuilder error = new scala.collection.mutable.StringBuilder(); + String sql = "DROP TABLE test"; + + BDPConfiguration.set(CONFIG_KEY, "true"); + boolean result = SQLExplain.authPass(sql, error); + + Assertions.assertTrue(result); + Assertions.assertEquals("", error.toString()); + } }