Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
271e675
[Feature-17923][ApiServer][UI] Introduce project-scoped authorization
ruanwenjun Oct 16, 2026
276a590
[Test-17923][E2E] Use project resource authorizations in fixtures
ruanwenjun Oct 16, 2026
2772432
[Test-17923][E2E] Wait for enabled resource selections
ruanwenjun Oct 16, 2026
3cba37b
[Fix-17923][ApiServer] Address project authorization review findings
ruanwenjun Oct 23, 2026
bf95574
[Docs-17923][Authorization] Add detailed project authorization diagram
ruanwenjun Oct 23, 2026
2fe81a3
[Docs-17923][Authorization] Compare legacy and project authorization …
ruanwenjun Oct 23, 2026
f05a11d
[Docs-17923][Authorization] Highlight grant ownership changes with re…
ruanwenjun Oct 23, 2026
4cdd094
Merge upstream dev into Feature-17923 and resolve workflow copy conflict
ruanwenjun Oct 29, 2026
ca111e3
[Fix-17923][Docs] Reference authorization diagrams in upgrade guide
ruanwenjun Oct 29, 2026
35fb65c
[Docs-17923] Remove legacy authorization draw.io source
ruanwenjun Oct 29, 2026
539c695
[Docs-17923] Keep only rendered authorization diagrams
ruanwenjun Oct 29, 2026
9c887a9
[Fix-17923][Authorization] Address project permission review findings
ruanwenjun Oct 29, 2026
2eca6ac
[Fix-17923][Authorization] Support explicit default tenant grants and…
ruanwenjun Oct 30, 2026
34cee24
[Fix-17923][Test] Complete project resource grants in workflow tests
ruanwenjun Oct 30, 2026
8130a4d
[Fix-17923][CI] Share current Maven artifacts with API test jobs
ruanwenjun Oct 30, 2026
e86016e
[Fix-17923][ApiServer] Align authorization checks and project API ass…
ruanwenjun Oct 30, 2026
0332c02
[Fix-17923][ApiServer] Apply VIEW visibility to batch workflow task r…
ruanwenjun Oct 30, 2026
514a283
docs: clarify project authorization boundaries
ruanwenjun Oct 30, 2026
2aaf1d4
[Fix-17923][API] Honor project-scoped delete permissions
ruanwenjun Nov 2, 2026
e51c111
[Fix-17923][ApiServer] Address project authorization review feedback
ruanwenjun Nov 4, 2026
9e9c19b
[Fix-17923][ApiServer] Simplify project permissions and close review …
ruanwenjun Nov 4, 2026
a1400b2
[Fix-17923][ApiServer] Avoid current child definition checks during r…
ruanwenjun Nov 4, 2026
34a4f3b
[Fix-17923][ApiServer] Fix authorization audit storage and recovery c…
ruanwenjun Nov 4, 2026
0182619
Merge branch 'dev' into Feature-17923
SbloodyS Nov 5, 2026
992e5c2
[Fix-17923][ApiServer] Validate child release states and clean delete…
ruanwenjun Nov 5, 2026
1856a02
[Fix-17923][Docs] Resolve upgrade note conflicts with dev
ruanwenjun Nov 6, 2026
680f586
[Fix-17923][ApiServer] Read task namespace without flattening nested …
ruanwenjun Nov 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/docs/en/guide/upgrade/incompatible.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ This document records the incompatible updates between each version. You need to

## 3.5.0

* Authorization now assigns project operation permissions to users and resource-use grants to projects. The upgrade creates `t_ds_project_user_permission` and `t_ds_project_resource_authorization`. Legacy authorization tables and data are retained, but historical permissions are not migrated automatically. Administrators must assign project member permissions and project resource grants before users can query, configure, publish or manually execute project tasks.
* Resource queries within a project must include `projectCode` and are filtered by the project's `VIEW` permission and resource grants. Global resource catalogs without a project parameter are restricted to administrators. Personal datasource and Kubernetes namespace authorization APIs are removed; grant these resources to projects instead.
* Project resource grants are checked when saving, publishing, manually starting or rerunning workflows. Revoking a resource grant does not recheck existing running or queued instances, scheduled triggers or automatic retries, and does not take existing schedules offline.
* `AlertSender#getAlertData` now explicitly rejects a null `alertType` by throwing `IllegalArgumentException` with an error log, instead of throwing a raw `NullPointerException`. During a rolling upgrade, **the Alert Server must be upgraded before Master/Worker**, otherwise an old Alert Server cannot recognize newly introduced `alert_type` enum values (e.g. `TASK_RESULT`) and alert delivery will fail. See the [upgrade guide](upgrade.md#rolling-upgrade-order) for details.([#18549](https://github.com/apache/dolphinscheduler/pull/18549))
* SQL task-result alert content is size-limited: by default at most 30 rows are kept, string field values longer than 100 characters are truncated with an ellipsis, and the serialized content is bounded to 60000 UTF-8 bytes (below the 65535-byte capacity of the MySQL TEXT column); rows beyond the budget are represented by an omission marker. These defaults are tunable via `sql.task.alert.max.rows`, `sql.task.alert.max.row.value.length` and `sql.task.alert.max.content.bytes` in `common.properties` (worker restart required). ([#18549](https://github.com/apache/dolphinscheduler/pull/18549))

### Project authorization model

The following diagrams compare the legacy authorization model with project-scoped authorization introduced in 3.5.0.

![Legacy authorization model](../../../../img/architecture-design/project-scoped-authorization-before.png)

![Project-scoped authorization model](../../../../img/architecture-design/project-scoped-authorization.png)

11 changes: 11 additions & 0 deletions docs/docs/zh/guide/upgrade/incompatible.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@

## 3.5.0

* 权限模型改为“用户拥有项目操作权限,项目获得资源使用授权”。升级程序新增 `t_ds_project_user_permission` 和 `t_ds_project_resource_authorization` 两张表,保留旧授权表及数据,但不自动迁移历史权限。管理员需要重新分配项目成员权限及项目资源授权,用户才能继续查询、配置、发布或人工执行项目任务。
* 项目内的资源查询需要传入 `projectCode`,按项目 `VIEW` 权限及资源授权过滤;不带项目参数的全局资源目录仅管理员可查询。移除数据源和命名空间的个人授权接口,改为向项目授权。
* 保存、发布和人工启动、重跑时检查项目资源授权。撤销资源授权不重新鉴权既有运行或排队实例、后台定时触发和自动重试,也不会自动下线已有定时。
* `AlertSender#getAlertData` 对 null `alertType` 增加了显式拒绝(抛 `IllegalArgumentException` 并记录错误日志),而非直接抛 `NullPointerException`。滚动升级时**必须先升级 Alert Server,再升级 Master/Worker**,否则旧 Alert Server 无法识别新增的 `alert_type` 枚举值(如 `TASK_RESULT`),会导致告警投递失败。详见[升级文档](upgrade.md#滚动升降级顺序)。([#18549](https://github.com/apache/dolphinscheduler/pull/18549))
* SQL 任务结果告警内容有大小限制:默认最多保留 30 行,超过 100 字符的字符串字段值会被截断并以省略号标记;序列化后内容总大小限制在 60000 UTF-8 字节以内(兼容 MySQL TEXT 列 65535 字节容量),超出预算的行以省略标记对象表示。以上默认值可通过 `common.properties` 中的 `sql.task.alert.max.rows`、`sql.task.alert.max.row.value.length`、`sql.task.alert.max.content.bytes` 配置调整,修改后需重启 Worker 生效。 ([#18549](https://github.com/apache/dolphinscheduler/pull/18549))

### 项目权限模型

以下架构图对比旧权限模型与 3.5.0 引入的项目权限模型。

![旧权限模型](../../../../img/architecture-design/project-scoped-authorization-before.png)

![项目权限模型](../../../../img/architecture-design/project-scoped-authorization.png)

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,10 @@ public Result<AlertGroup> createAlertGroup(@Parameter(hidden = true) @RequestAtt
@GetMapping(value = "/list")
@ResponseStatus(HttpStatus.OK)
@ApiException(QUERY_ALL_ALERTGROUP_ERROR)
public Result<List<AlertGroup>> list(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser) {
public Result<List<AlertGroup>> list(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@RequestParam(value = "projectCode", required = false) Long projectCode) {

List<AlertGroup> alertGroups = alertGroupService.queryAllAlertGroup(loginUser);
List<AlertGroup> alertGroups = alertGroupService.queryAllAlertGroup(loginUser, projectCode);
return Result.success(alertGroups);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

package org.apache.dolphinscheduler.api.controller;

import static org.apache.dolphinscheduler.api.enums.Status.AUTHORIZED_DATA_SOURCE;
import static org.apache.dolphinscheduler.api.enums.Status.CONNECTION_TEST_FAILURE;
import static org.apache.dolphinscheduler.api.enums.Status.CONNECT_DATASOURCE_FAILURE;
import static org.apache.dolphinscheduler.api.enums.Status.CREATE_DATASOURCE_ERROR;
Expand All @@ -27,7 +26,6 @@
import static org.apache.dolphinscheduler.api.enums.Status.GET_DATASOURCE_TABLE_COLUMNS_ERROR;
import static org.apache.dolphinscheduler.api.enums.Status.KERBEROS_STARTUP_STATE;
import static org.apache.dolphinscheduler.api.enums.Status.QUERY_DATASOURCE_ERROR;
import static org.apache.dolphinscheduler.api.enums.Status.UNAUTHORIZED_DATASOURCE;
import static org.apache.dolphinscheduler.api.enums.Status.UPDATE_DATASOURCE_ERROR;
import static org.apache.dolphinscheduler.api.enums.Status.VERIFY_DATASOURCE_NAME_FAILURE;

Expand All @@ -39,7 +37,6 @@
import org.apache.dolphinscheduler.api.service.DataSourceService;
import org.apache.dolphinscheduler.api.utils.PageInfo;
import org.apache.dolphinscheduler.api.utils.Result;
import org.apache.dolphinscheduler.api.vo.DataSourceSimpleInfoVO;
import org.apache.dolphinscheduler.common.constants.Constants;
import org.apache.dolphinscheduler.dao.entity.DataSource;
import org.apache.dolphinscheduler.dao.entity.User;
Expand Down Expand Up @@ -142,7 +139,7 @@ public Result<DataSource> updateDataSource(@Parameter(hidden = true) @RequestAtt
@Parameter(name = "id", description = "DATA_SOURCE_ID", required = true, schema = @Schema(implementation = int.class, example = "100"))

})
@GetMapping(value = "/{id}")
@GetMapping(value = "/{id:[0-9]+}")
@ResponseStatus(HttpStatus.OK)
@ApiException(QUERY_DATASOURCE_ERROR)
public Result<Object> queryDataSource(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
Expand All @@ -166,8 +163,9 @@ public Result<Object> queryDataSource(@Parameter(hidden = true) @RequestAttribut
@ResponseStatus(HttpStatus.OK)
@ApiException(QUERY_DATASOURCE_ERROR)
public Result<Object> queryDataSourceList(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@RequestParam("type") DbType type) {
List<DataSource> datasourceList = dataSourceService.queryDataSourceList(loginUser, type.getCode());
@RequestParam("type") DbType type,
@RequestParam(value = "projectCode", required = false) Long projectCode) {
List<DataSource> datasourceList = dataSourceService.queryDataSourceList(loginUser, type.getCode(), projectCode);
return Result.success(datasourceList);
}

Expand Down Expand Up @@ -212,11 +210,12 @@ public Result<Object> queryDataSourceListPaging(@Parameter(hidden = true) @Reque
@PostMapping(value = "/connect")
@ResponseStatus(HttpStatus.OK)
@ApiException(CONNECT_DATASOURCE_FAILURE)
public Result<Boolean> connectDataSource(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "dataSourceParam") @RequestBody String jsonStr) {
public Result<Boolean> connectDataSource(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "dataSourceParam") @RequestBody String jsonStr) {
BaseDataSourceParamDTO dataSourceParam = DataSourceUtils.buildDatasourceParam(jsonStr);
DataSourceUtils.checkDatasourceParam(dataSourceParam);
ConnectionParam connectionParams = DataSourceUtils.buildConnectionParams(dataSourceParam);
dataSourceService.checkConnection(dataSourceParam.getType(), connectionParams);
dataSourceService.checkConnection(loginUser, dataSourceParam.getType(), connectionParams);
return Result.success(true);
}

Expand Down Expand Up @@ -282,52 +281,6 @@ public Result<Boolean> verifyDataSourceName(@RequestParam(value = "name") String
return Result.success(true);
}

/**
* query the list of unauthorized data sources for a specific user
*
* @param loginUser login user
* @param userId user id
* @return a list of unauthorized data source summaries
*/
@Operation(summary = "unauthorizedDatasource", description = "UNAUTHORIZED_DATA_SOURCE_NOTES")
@Parameters({
@Parameter(name = "userId", description = "USER_ID", required = true, schema = @Schema(implementation = int.class, example = "100"))
})
@GetMapping(value = "/unauth-datasource")
@ResponseStatus(HttpStatus.OK)
@ApiException(UNAUTHORIZED_DATASOURCE)
public Result<List<DataSourceSimpleInfoVO>> getUnauthorizedDatasourceList(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@RequestParam("userId") Integer userId) {

List<DataSourceSimpleInfoVO> unAuthDatasourceList = dataSourceService.unAuthDatasource(loginUser, userId);
return Result.success(unAuthDatasourceList);
}

/**
* query the list of data sources authorized for a specific user
*
* @param loginUser login user
* @param userId user id
* @return a list of authorized data source summaries
*/
@Operation(summary = "authedDatasource", description = "AUTHORIZED_DATA_SOURCE_NOTES")
@Parameters({
@Parameter(name = "userId", description = "USER_ID", required = true, schema = @Schema(implementation = int.class, example = "100"))
})
@GetMapping(value = "/authed-datasource")
@ResponseStatus(HttpStatus.OK)
@ApiException(AUTHORIZED_DATA_SOURCE)
public Result<List<DataSourceSimpleInfoVO>> getAuthorizedDatasourceList(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@RequestParam("userId") Integer userId) {
List<DataSourceSimpleInfoVO> authedDatasourceList = dataSourceService.authedDatasource(loginUser, userId);
return Result.success(authedDatasourceList);
}

/**
* check the active status of Kerberos authentication
*
* @return {@code true} if Kerberos is active and configured for HDFS; {@code false} otherwise
*/
@Operation(summary = "getKerberosStartupState", description = "GET_KERBEROS_STARTUP_STATE")
@GetMapping(value = "/kerberos-startup-state")
@ResponseStatus(HttpStatus.OK)
Expand Down Expand Up @@ -355,8 +308,9 @@ public Result<Object> getKerberosStartupState() {
@ApiException(GET_DATASOURCE_TABLES_ERROR)
public Result<Object> getTables(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@RequestParam("datasourceId") Integer datasourceId,
@RequestParam("database") String database) {
List<ParamsOptions> options = dataSourceService.getTables(loginUser, datasourceId, database);
@RequestParam("database") String database,
@RequestParam(value = "projectCode", required = false) Long projectCode) {
List<ParamsOptions> options = dataSourceService.getTables(loginUser, datasourceId, database, projectCode);
return Result.success(options);
}

Expand All @@ -381,8 +335,10 @@ public Result<Object> getTables(@Parameter(hidden = true) @RequestAttribute(valu
public Result<Object> getTableColumns(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@RequestParam("datasourceId") Integer datasourceId,
@RequestParam("database") String database,
@RequestParam("tableName") String tableName) {
List<ParamsOptions> options = dataSourceService.getTableColumns(loginUser, datasourceId, database, tableName);
@RequestParam("tableName") String tableName,
@RequestParam(value = "projectCode", required = false) Long projectCode) {
List<ParamsOptions> options =
dataSourceService.getTableColumns(loginUser, datasourceId, database, tableName, projectCode);
return Result.success(options);
}

Expand All @@ -401,8 +357,9 @@ public Result<Object> getTableColumns(@Parameter(hidden = true) @RequestAttribut
@ResponseStatus(HttpStatus.OK)
@ApiException(GET_DATASOURCE_DATABASES_ERROR)
public Result<Object> getDatabases(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@RequestParam("datasourceId") Integer datasourceId) {
List<ParamsOptions> options = dataSourceService.getDatabases(loginUser, datasourceId);
@RequestParam("datasourceId") Integer datasourceId,
@RequestParam(value = "projectCode", required = false) Long projectCode) {
List<ParamsOptions> options = dataSourceService.getDatabases(loginUser, datasourceId, projectCode);
return Result.success(options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,9 @@ public Result<Environment> updateEnvironment(@Parameter(hidden = true) @RequestA
@ResponseStatus(HttpStatus.OK)
@ApiException(QUERY_ENVIRONMENT_BY_CODE_ERROR)
public Result<EnvironmentDto> queryEnvironmentByCode(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@RequestParam("environmentCode") Long environmentCode) {
EnvironmentDto dto = environmentService.queryEnvironmentByCode(environmentCode);
@RequestParam("environmentCode") Long environmentCode,
@RequestParam(value = "projectCode", required = false) Long projectCode) {
EnvironmentDto dto = environmentService.queryEnvironmentByCode(loginUser, environmentCode, projectCode);
return Result.success(dto);
}

Expand Down Expand Up @@ -198,8 +199,9 @@ public Result<Void> deleteEnvironment(@Parameter(hidden = true) @RequestAttribut
@GetMapping(value = "/query-environment-list")
@ResponseStatus(HttpStatus.OK)
@ApiException(QUERY_ENVIRONMENT_ERROR)
public Result<List<EnvironmentDto>> queryAllEnvironmentList(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser) {
List<EnvironmentDto> dtos = environmentService.queryAllEnvironmentList(loginUser);
public Result<List<EnvironmentDto>> queryAllEnvironmentList(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
@RequestParam(value = "projectCode", required = false) Long projectCode) {
List<EnvironmentDto> dtos = environmentService.queryAllEnvironmentList(loginUser, projectCode);
return Result.success(dtos);
}

Expand Down
Loading
Loading