diff --git a/bundle/config/mutator/resourcemutator/apply_bundle_permissions_test.go b/bundle/config/mutator/resourcemutator/apply_bundle_permissions_test.go index fc2d052f2c..0d453fe712 100644 --- a/bundle/config/mutator/resourcemutator/apply_bundle_permissions_test.go +++ b/bundle/config/mutator/resourcemutator/apply_bundle_permissions_test.go @@ -72,14 +72,38 @@ func TestApplyBundlePermissions(t *testing.T) { require.NoError(t, diags.Error()) require.Len(t, b.Config.Resources.Jobs["job_1"].Permissions, 3) - require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"}) - require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"}) - require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_MANAGE_RUN", ServicePrincipalName: "TestServicePrincipal"}) + require.Contains( + t, + b.Config.Resources.Jobs["job_1"].Permissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanManage, UserName: "TestUser"}, + ) + require.Contains( + t, + b.Config.Resources.Jobs["job_1"].Permissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanView, GroupName: "TestGroup"}, + ) + require.Contains( + t, + b.Config.Resources.Jobs["job_1"].Permissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanManageRun, ServicePrincipalName: "TestServicePrincipal"}, + ) require.Len(t, b.Config.Resources.Jobs["job_2"].Permissions, 3) - require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"}) - require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"}) - require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_MANAGE_RUN", ServicePrincipalName: "TestServicePrincipal"}) + require.Contains( + t, + b.Config.Resources.Jobs["job_2"].Permissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanManage, UserName: "TestUser"}, + ) + require.Contains( + t, + b.Config.Resources.Jobs["job_2"].Permissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanView, GroupName: "TestGroup"}, + ) + require.Contains( + t, + b.Config.Resources.Jobs["job_2"].Permissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanManageRun, ServicePrincipalName: "TestServicePrincipal"}, + ) require.Len(t, b.Config.Resources.Pipelines["pipeline_1"].Permissions, 3) require.Contains(t, b.Config.Resources.Pipelines["pipeline_1"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"}) @@ -142,16 +166,16 @@ func TestWarningOnOverlapPermission(t *testing.T) { JobSettings: &jobs.JobSettings{ Name: "job_1", }, - Permissions: []resources.Permission{ - {Level: permissions.CAN_VIEW, UserName: "TestUser"}, + Permissions: []resources.JobPermission{ + {Level: resources.JobPermissionLevelCanView, UserName: "TestUser"}, }, }, "job_2": { JobSettings: &jobs.JobSettings{ Name: "job_2", }, - Permissions: []resources.Permission{ - {Level: permissions.CAN_VIEW, UserName: "TestUser2"}, + Permissions: []resources.JobPermission{ + {Level: resources.JobPermissionLevelCanView, UserName: "TestUser2"}, }, }, }, @@ -162,11 +186,31 @@ func TestWarningOnOverlapPermission(t *testing.T) { diags := bundle.Apply(context.Background(), b, ApplyBundlePermissions()) require.NoError(t, diags.Error()) - require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_VIEW", UserName: "TestUser"}) - require.Contains(t, b.Config.Resources.Jobs["job_1"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"}) - require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_VIEW", UserName: "TestUser2"}) - require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_MANAGE", UserName: "TestUser"}) - require.Contains(t, b.Config.Resources.Jobs["job_2"].Permissions, resources.Permission{Level: "CAN_VIEW", GroupName: "TestGroup"}) + require.Contains( + t, + b.Config.Resources.Jobs["job_1"].Permissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanView, UserName: "TestUser"}, + ) + require.Contains( + t, + b.Config.Resources.Jobs["job_1"].Permissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanView, GroupName: "TestGroup"}, + ) + require.Contains( + t, + b.Config.Resources.Jobs["job_2"].Permissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanView, UserName: "TestUser2"}, + ) + require.Contains( + t, + b.Config.Resources.Jobs["job_2"].Permissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanManage, UserName: "TestUser"}, + ) + require.Contains( + t, + b.Config.Resources.Jobs["job_2"].Permissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanView, GroupName: "TestGroup"}, + ) } func TestAllResourcesExplicitlyDefinedForPermissionsSupport(t *testing.T) { diff --git a/bundle/config/mutator/resourcemutator/filter_current_user_test.go b/bundle/config/mutator/resourcemutator/filter_current_user_test.go index 0be09d4a2b..12e106f64c 100644 --- a/bundle/config/mutator/resourcemutator/filter_current_user_test.go +++ b/bundle/config/mutator/resourcemutator/filter_current_user_test.go @@ -29,7 +29,28 @@ var robot = resources.Permission{ ServicePrincipalName: "i-Robot", } +var jobAlice = resources.JobPermission{ + Level: resources.JobPermissionLevelCanManage, + UserName: "alice@databricks.com", +} + +var jobBob = resources.JobPermission{ + Level: resources.JobPermissionLevelCanView, + UserName: "bob@databricks.com", +} + +var jobRobot = resources.JobPermission{ + Level: resources.JobPermissionLevelCanManageRun, + ServicePrincipalName: "i-Robot", +} + func testFixture(userName string) *bundle.Bundle { + jobPermissions := []resources.JobPermission{ + jobAlice, + jobBob, + jobRobot, + } + p := []resources.Permission{ alice, bob, @@ -51,13 +72,13 @@ func testFixture(userName string) *bundle.Bundle { JobSettings: &jobs.JobSettings{ Name: "job1", }, - Permissions: p, + Permissions: jobPermissions, }, "job2": { JobSettings: &jobs.JobSettings{ Name: "job2", }, - Permissions: p, + Permissions: jobPermissions, }, }, Pipelines: map[string]*resources.Pipeline{ @@ -102,12 +123,12 @@ func TestFilterCurrentUser(t *testing.T) { // Assert current user is filtered out. assert.Len(t, b.Config.Resources.Jobs["job1"].Permissions, 2) - assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, robot) - assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, bob) + assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, jobRobot) + assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, jobBob) assert.Len(t, b.Config.Resources.Jobs["job2"].Permissions, 2) - assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, robot) - assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, bob) + assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, jobRobot) + assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, jobBob) assert.Len(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, 2) assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, robot) @@ -137,12 +158,12 @@ func TestFilterCurrentServicePrincipal(t *testing.T) { // Assert current user is filtered out. assert.Len(t, b.Config.Resources.Jobs["job1"].Permissions, 2) - assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, alice) - assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, bob) + assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, jobAlice) + assert.Contains(t, b.Config.Resources.Jobs["job1"].Permissions, jobBob) assert.Len(t, b.Config.Resources.Jobs["job2"].Permissions, 2) - assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, alice) - assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, bob) + assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, jobAlice) + assert.Contains(t, b.Config.Resources.Jobs["job2"].Permissions, jobBob) assert.Len(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, 2) assert.Contains(t, b.Config.Resources.Pipelines["pipeline1"].Permissions, alice) diff --git a/bundle/config/mutator/resourcemutator/validate_target_mode_test.go b/bundle/config/mutator/resourcemutator/validate_target_mode_test.go index 1676931d63..8c6c549dbd 100644 --- a/bundle/config/mutator/resourcemutator/validate_target_mode_test.go +++ b/bundle/config/mutator/resourcemutator/validate_target_mode_test.go @@ -49,13 +49,19 @@ func TestProcessTargetModeProduction(t *testing.T) { diags = validateProductionMode(b, false) require.ErrorContains(t, diags.Error(), "A common practice is to use a username or principal name in this path, i.e. use\n\n root_path: /Workspace/Users/lennart@company.com/.bundle/${bundle.name}/${bundle.target}") + jobPermissions := []resources.JobPermission{ + { + Level: resources.JobPermissionLevelCanManage, + UserName: "user@company.com", + }, + } permissions := []resources.Permission{ { Level: "CAN_MANAGE", UserName: "user@company.com", }, } - b.Config.Resources.Jobs["job1"].Permissions = permissions + b.Config.Resources.Jobs["job1"].Permissions = jobPermissions b.Config.Resources.Jobs["job1"].RunAs = &jobs.JobRunAs{UserName: "user@company.com"} b.Config.Resources.Jobs["job2"].RunAs = &jobs.JobRunAs{UserName: "user@company.com"} b.Config.Resources.Jobs["job3"].RunAs = &jobs.JobRunAs{UserName: "user@company.com"} diff --git a/bundle/config/resources/job.go b/bundle/config/resources/job.go index 3fadd76fe7..b8ced15b14 100644 --- a/bundle/config/resources/job.go +++ b/bundle/config/resources/job.go @@ -11,11 +11,30 @@ import ( "github.com/databricks/databricks-sdk-go/service/jobs" ) +type JobPermissionLevel string + +const ( + JobPermissionLevelCanManage JobPermissionLevel = `CAN_MANAGE` + JobPermissionLevelCanManageRun JobPermissionLevel = `CAN_MANAGE_RUN` + JobPermissionLevelCanView JobPermissionLevel = `CAN_VIEW` + JobPermissionLevelIsOwner JobPermissionLevel = `IS_OWNER` +) + +// JobPermission holds the permission level setting for a single principal. +// Multiple of these can be defined on any job. +type JobPermission struct { + Level JobPermissionLevel `json:"level"` + + UserName string `json:"user_name,omitempty"` + ServicePrincipalName string `json:"service_principal_name,omitempty"` + GroupName string `json:"group_name,omitempty"` +} + type Job struct { - ID string `json:"id,omitempty" bundle:"readonly"` - Permissions []Permission `json:"permissions,omitempty"` - ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"` - URL string `json:"url,omitempty" bundle:"internal"` + ID string `json:"id,omitempty" bundle:"readonly"` + Permissions []JobPermission `json:"permissions,omitempty"` + ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"` + URL string `json:"url,omitempty" bundle:"internal"` *jobs.JobSettings } diff --git a/bundle/deploy/terraform/convert_test.go b/bundle/deploy/terraform/convert_test.go index 53d861b321..d416da4d24 100644 --- a/bundle/deploy/terraform/convert_test.go +++ b/bundle/deploy/terraform/convert_test.go @@ -95,9 +95,9 @@ func TestBundleToTerraformJob(t *testing.T) { func TestBundleToTerraformJobPermissions(t *testing.T) { src := resources.Job{ - Permissions: []resources.Permission{ + Permissions: []resources.JobPermission{ { - Level: "CAN_VIEW", + Level: resources.JobPermissionLevelCanView, UserName: "jane@doe.com", }, }, diff --git a/bundle/deploy/terraform/tfdyn/convert_job_test.go b/bundle/deploy/terraform/tfdyn/convert_job_test.go index c73e530d48..080d6bb931 100644 --- a/bundle/deploy/terraform/tfdyn/convert_job_test.go +++ b/bundle/deploy/terraform/tfdyn/convert_job_test.go @@ -68,9 +68,9 @@ func TestConvertJob(t *testing.T) { }, }, }, - Permissions: []resources.Permission{ + Permissions: []resources.JobPermission{ { - Level: "CAN_VIEW", + Level: resources.JobPermissionLevelCanView, UserName: "jane@doe.com", }, }, diff --git a/bundle/deploy/terraform/tfdyn/convert_permissions_test.go b/bundle/deploy/terraform/tfdyn/convert_permissions_test.go index ba04384b57..643478541f 100644 --- a/bundle/deploy/terraform/tfdyn/convert_permissions_test.go +++ b/bundle/deploy/terraform/tfdyn/convert_permissions_test.go @@ -14,17 +14,17 @@ import ( func TestConvertPermissions(t *testing.T) { src := resources.Job{ - Permissions: []resources.Permission{ + Permissions: []resources.JobPermission{ { - Level: "CAN_VIEW", + Level: resources.JobPermissionLevelCanView, UserName: "jane@doe.com", }, { - Level: "CAN_MANAGE", + Level: resources.JobPermissionLevelCanManage, GroupName: "special admins", }, { - Level: "CAN_RUN", + Level: resources.JobPermissionLevelCanManageRun, ServicePrincipalName: "spn", }, }, @@ -50,7 +50,7 @@ func TestConvertPermissions(t *testing.T) { ServicePrincipalName: "", }, { - PermissionLevel: "CAN_RUN", + PermissionLevel: "CAN_MANAGE_RUN", UserName: "", GroupName: "", ServicePrincipalName: "spn", @@ -73,7 +73,7 @@ func TestConvertPermissionsNil(t *testing.T) { func TestConvertPermissionsEmpty(t *testing.T) { src := resources.Job{ - Permissions: []resources.Permission{}, + Permissions: []resources.JobPermission{}, } vin, err := convert.FromTyped(src, dyn.NilValue) diff --git a/bundle/internal/schema/annotations.yml b/bundle/internal/schema/annotations.yml index 04c4eaac0d..bd71a048e2 100644 --- a/bundle/internal/schema/annotations.yml +++ b/bundle/internal/schema/annotations.yml @@ -410,6 +410,19 @@ github.com/databricks/cli/bundle/config/resources.Grant: "privileges": "description": |- The privileges to grant to the specified entity +github.com/databricks/cli/bundle/config/resources.JobPermission: + "group_name": + "description": |- + PLACEHOLDER + "level": + "description": |- + PLACEHOLDER + "service_principal_name": + "description": |- + PLACEHOLDER + "user_name": + "description": |- + PLACEHOLDER github.com/databricks/cli/bundle/config/resources.Permission: "-": "description": |- diff --git a/bundle/internal/schema/annotations_openapi_overrides.yml b/bundle/internal/schema/annotations_openapi_overrides.yml index ffd1ba2634..d5f8c3ea78 100644 --- a/bundle/internal/schema/annotations_openapi_overrides.yml +++ b/bundle/internal/schema/annotations_openapi_overrides.yml @@ -141,6 +141,13 @@ github.com/databricks/cli/bundle/config/resources.Job: "run_as": "description": |- PLACEHOLDER +github.com/databricks/cli/bundle/config/resources.JobPermissionLevel: + "_": + "enum": + - CAN_MANAGE + - CAN_MANAGE_RUN + - CAN_VIEW + - IS_OWNER github.com/databricks/cli/bundle/config/resources.MlflowExperiment: "_": "markdown_description": |- diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index 72fed9fc3d..fcacbcf7de 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -449,7 +449,7 @@ "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/jobs.PerformanceTarget" }, "permissions": { - "$ref": "#/$defs/slice/github.com/databricks/cli/bundle/config/resources.Permission" + "$ref": "#/$defs/slice/github.com/databricks/cli/bundle/config/resources.JobPermission" }, "queue": { "description": "The queue settings of the job.", @@ -492,6 +492,52 @@ } ] }, + "resources.JobPermission": { + "oneOf": [ + { + "type": "object", + "properties": { + "group_name": { + "$ref": "#/$defs/string" + }, + "level": { + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.JobPermissionLevel" + }, + "service_principal_name": { + "$ref": "#/$defs/string" + }, + "user_name": { + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false, + "required": [ + "level" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "resources.JobPermissionLevel": { + "oneOf": [ + { + "type": "string", + "enum": [ + "CAN_MANAGE", + "CAN_MANAGE_RUN", + "CAN_VIEW", + "IS_OWNER" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "resources.MlflowExperiment": { "oneOf": [ { @@ -7153,6 +7199,20 @@ } ] }, + "resources.JobPermission": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.JobPermission" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "resources.Permission": { "oneOf": [ { diff --git a/bundle/tests/bundle_permissions_test.go b/bundle/tests/bundle_permissions_test.go index c669121ba3..0648608b6f 100644 --- a/bundle/tests/bundle_permissions_test.go +++ b/bundle/tests/bundle_permissions_test.go @@ -29,10 +29,26 @@ func TestBundlePermissions(t *testing.T) { assert.NotContains(t, pipelinePermissions, resources.Permission{Level: "CAN_RUN", UserName: "bot@company.com"}) jobsPermissions := b.Config.Resources.Jobs["pipeline_schedule"].Permissions - assert.Contains(t, jobsPermissions, resources.Permission{Level: "CAN_MANAGE_RUN", UserName: "test@company.com"}) - assert.NotContains(t, jobsPermissions, resources.Permission{Level: "CAN_MANAGE", GroupName: "devs"}) - assert.NotContains(t, jobsPermissions, resources.Permission{Level: "CAN_VIEW", ServicePrincipalName: "1234-abcd"}) - assert.NotContains(t, jobsPermissions, resources.Permission{Level: "CAN_RUN", UserName: "bot@company.com"}) + assert.Contains( + t, + jobsPermissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanManageRun, UserName: "test@company.com"}, + ) + assert.NotContains( + t, + jobsPermissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanManage, GroupName: "devs"}, + ) + assert.NotContains( + t, + jobsPermissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanView, ServicePrincipalName: "1234-abcd"}, + ) + assert.NotContains( + t, + jobsPermissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanManageRun, UserName: "bot@company.com"}, + ) } func TestBundlePermissionsDevTarget(t *testing.T) { @@ -52,8 +68,24 @@ func TestBundlePermissionsDevTarget(t *testing.T) { assert.Contains(t, pipelinePermissions, resources.Permission{Level: "CAN_RUN", UserName: "bot@company.com"}) jobsPermissions := b.Config.Resources.Jobs["pipeline_schedule"].Permissions - assert.Contains(t, jobsPermissions, resources.Permission{Level: "CAN_MANAGE_RUN", UserName: "test@company.com"}) - assert.Contains(t, jobsPermissions, resources.Permission{Level: "CAN_MANAGE", GroupName: "devs"}) - assert.Contains(t, jobsPermissions, resources.Permission{Level: "CAN_VIEW", ServicePrincipalName: "1234-abcd"}) - assert.Contains(t, jobsPermissions, resources.Permission{Level: "CAN_MANAGE_RUN", UserName: "bot@company.com"}) + assert.Contains( + t, + jobsPermissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanManageRun, UserName: "test@company.com"}, + ) + assert.Contains( + t, + jobsPermissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanManage, GroupName: "devs"}, + ) + assert.Contains( + t, + jobsPermissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanView, ServicePrincipalName: "1234-abcd"}, + ) + assert.Contains( + t, + jobsPermissions, + resources.JobPermission{Level: resources.JobPermissionLevelCanManageRun, UserName: "bot@company.com"}, + ) }