diff --git a/changes/13643-gitops-role b/changes/13643-gitops-role new file mode 100644 index 0000000000..d7b4676b6b --- /dev/null +++ b/changes/13643-gitops-role @@ -0,0 +1 @@ +gitops role can now read queries/policies and write (but not execute) scripts diff --git a/docs/Using Fleet/manage-access.md b/docs/Using Fleet/manage-access.md index d31ca82d68..5d4fd9973b 100644 --- a/docs/Using Fleet/manage-access.md +++ b/docs/Using Fleet/manage-access.md @@ -50,10 +50,10 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines. | Run queries designated "**observer can run**" as live queries against all hosts | ✅ | ✅ | ✅ | ✅ | | | Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) against all hosts | | ✅ | ✅ | ✅ | | | Create, edit, and delete queries | | | ✅ | ✅ | ✅ | -| View all queries and their reports | ✅ | ✅ | ✅ | ✅ | | +| View all queries and their reports | ✅ | ✅ | ✅ | ✅ | ✅ | | Manage [query automations](https://fleetdm.com/docs/using-fleet/fleet-ui#schedule-a-query) | | | ✅ | ✅ | ✅ | | Create, edit, view, and delete packs | | | ✅ | ✅ | ✅ | -| View all policies | ✅ | ✅ | ✅ | ✅ | | +| View all policies | ✅ | ✅ | ✅ | ✅ | ✅ | | Run all policies | | ✅ | ✅ | ✅ | | | Filter hosts using policies | ✅ | ✅ | ✅ | ✅ | | | Create, edit, and delete policies for all hosts | | | ✅ | ✅ | ✅ | @@ -90,7 +90,7 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines. | Enable/disable MDM macOS setup end user authentication\* | | | ✅ | ✅ | ✅ | | Run arbitrary scripts on hosts\* | | | ✅ | ✅ | | | View saved scripts\* | ✅ | ✅ | ✅ | ✅ | | -| Edit/upload saved scripts\* | | | ✅ | ✅ | | +| Edit/upload saved scripts\* | | | ✅ | ✅ | ✅ | | Run saved scripts on hosts\* | ✅ | ✅ | ✅ | ✅ | | \* Applies only to Fleet Premium diff --git a/server/authz/policy.rego b/server/authz/policy.rego index e68208c287..3e3c9ce6ef 100644 --- a/server/authz/policy.rego +++ b/server/authz/policy.rego @@ -313,10 +313,10 @@ allow { action == write } -# Global admins, maintainers, observer_plus and observers can read queries. +# Global admins, maintainers, gitops, observer_plus and observers can read queries. allow { object.type == "query" - subject.global_role == [admin, maintainer, observer_plus, observer][_] + subject.global_role == [admin, maintainer, gitops, observer_plus, observer][_] action == read } @@ -328,11 +328,11 @@ allow { action == write } -# Team admins, maintainers, observer_plus and observers can read queries for their teams. +# Team admins, maintainers, gitops, observer_plus and observers can read queries for their teams. allow { object.type == "query" not is_null(object.team_id) - team_role(subject, object.team_id) == [admin, maintainer, observer_plus, observer][_] + team_role(subject, object.team_id) == [admin, maintainer, gitops, observer_plus, observer][_] action == read } @@ -537,20 +537,13 @@ allow { # Policies ## -# Global admins and maintainers can read and write policies. +# Global admins, maintainers, and gitops can read and write policies. allow { object.type == "policy" - subject.global_role == [admin, maintainer][_] + subject.global_role == [admin, maintainer, gitops][_] action == [read, write][_] } -# Global gitops can write policies. -allow { - object.type == "policy" - subject.global_role == gitops - action == write -} - # Global observer and observer_plus can read any policies. allow { object.type == "policy" @@ -558,22 +551,14 @@ allow { action == read } -# Team admin and maintainers can read and write policies for their teams. +# Team admin, maintainers, and gitops can read and write policies for their teams. allow { not is_null(object.team_id) object.type == "policy" - team_role(subject, object.team_id) == [admin, maintainer][_] + team_role(subject, object.team_id) == [admin, maintainer, gitops][_] action == [read, write][_] } -# Team gitops can write policies for their teams. -allow { - not is_null(object.team_id) - object.type == "policy" - team_role(subject, object.team_id) == gitops - action == write -} - # Team admin, maintainers, observers and observers_plus can read global policies allow { is_null(object.team_id) @@ -900,10 +885,10 @@ allow { # Scripts (saved script) ## -# Global admins and maintainers can write (upload) saved scripts. +# Global admins, maintainers, and gitops can write (upload) saved scripts. allow { object.type == "script" - subject.global_role == [admin, maintainer][_] + subject.global_role == [admin, maintainer, gitops][_] action == write } @@ -914,11 +899,11 @@ allow { action == read } -# Team admin and maintainers can write (upload) saved scripts for their teams. +# Team admin, maintainers, and gitops can write (upload) saved scripts for their teams. allow { object.type == "script" not is_null(object.team_id) - team_role(subject, object.team_id) == [admin, maintainer][_] + team_role(subject, object.team_id) == [admin, maintainer, gitops][_] action == write } diff --git a/server/authz/policy_test.go b/server/authz/policy_test.go index 49fa56559e..0c404068f0 100644 --- a/server/authz/policy_test.go +++ b/server/authz/policy_test.go @@ -900,15 +900,15 @@ func TestAuthorizeQuery(t *testing.T) { }, }, { - name: "Global GitOps cannot read, or run any query, but can write", + name: "Global GitOps cannot run any query, but can read or write", testCases: []authTestCase{ - {user: test.UserGitOps, object: globalQuery, action: read, allow: false}, + {user: test.UserGitOps, object: globalQuery, action: read, allow: true}, {user: test.UserGitOps, object: globalQuery, action: write, allow: true}, {user: test.UserGitOps, object: teamAdminQuery, action: write, allow: true}, {user: test.UserGitOps, object: globalQueryNoTargets, action: run, allow: false}, {user: test.UserGitOps, object: globalQueryTargetedToTeam1, action: run, allow: false}, {user: test.UserGitOps, object: globalQuery, action: runNew, allow: false}, - {user: test.UserGitOps, object: globalObserverQuery, action: read, allow: false}, + {user: test.UserGitOps, object: globalObserverQuery, action: read, allow: true}, {user: test.UserGitOps, object: globalObserverQuery, action: write, allow: true}, {user: test.UserGitOps, object: globalObserverQueryEmptyTargets, action: run, allow: false}, {user: test.UserGitOps, object: globalObserverQueryTargetedToTeam1, action: run, allow: false}, @@ -1201,7 +1201,7 @@ func TestAuthorizeGlobalPolicy(t *testing.T) { {user: test.UserObserverPlus, object: globalPolicy, action: read, allow: true}, {user: test.UserGitOps, object: globalPolicy, action: write, allow: true}, - {user: test.UserGitOps, object: globalPolicy, action: read, allow: false}, + {user: test.UserGitOps, object: globalPolicy, action: read, allow: true}, {user: test.UserTeamAdminTeam1, object: globalPolicy, action: write, allow: false}, {user: test.UserTeamAdminTeam1, object: globalPolicy, action: read, allow: true}, @@ -1253,7 +1253,7 @@ func TestAuthorizeTeamPolicy(t *testing.T) { {user: test.UserObserverPlus, object: team1Policy, action: read, allow: true}, {user: test.UserGitOps, object: team1Policy, action: write, allow: true}, - {user: test.UserGitOps, object: team1Policy, action: read, allow: false}, + {user: test.UserGitOps, object: team1Policy, action: read, allow: true}, {user: test.UserTeamAdminTeam1, object: team1Policy, action: write, allow: true}, {user: test.UserTeamAdminTeam1, object: team1Policy, action: read, allow: true}, @@ -1268,7 +1268,7 @@ func TestAuthorizeTeamPolicy(t *testing.T) { {user: test.UserTeamObserverPlusTeam1, object: team1Policy, action: read, allow: true}, {user: test.UserTeamGitOpsTeam1, object: team1Policy, action: write, allow: true}, - {user: test.UserTeamGitOpsTeam1, object: team1Policy, action: read, allow: false}, + {user: test.UserTeamGitOpsTeam1, object: team1Policy, action: read, allow: true}, {user: test.UserTeamAdminTeam1, object: team2Policy, action: write, allow: false}, {user: test.UserTeamAdminTeam1, object: team2Policy, action: read, allow: false}, @@ -2002,9 +2002,9 @@ func TestAuthorizeScript(t *testing.T) { {user: test.UserObserverPlus, object: team1Script, action: write, allow: false}, {user: test.UserObserverPlus, object: team1Script, action: read, allow: true}, - {user: test.UserGitOps, object: globalScript, action: write, allow: false}, + {user: test.UserGitOps, object: globalScript, action: write, allow: true}, {user: test.UserGitOps, object: globalScript, action: read, allow: false}, - {user: test.UserGitOps, object: team1Script, action: write, allow: false}, + {user: test.UserGitOps, object: team1Script, action: write, allow: true}, {user: test.UserGitOps, object: team1Script, action: read, allow: false}, {user: test.UserTeamAdminTeam1, object: globalScript, action: write, allow: false}, @@ -2049,7 +2049,7 @@ func TestAuthorizeScript(t *testing.T) { {user: test.UserTeamGitOpsTeam1, object: globalScript, action: write, allow: false}, {user: test.UserTeamGitOpsTeam1, object: globalScript, action: read, allow: false}, - {user: test.UserTeamGitOpsTeam1, object: team1Script, action: write, allow: false}, + {user: test.UserTeamGitOpsTeam1, object: team1Script, action: write, allow: true}, {user: test.UserTeamGitOpsTeam1, object: team1Script, action: read, allow: false}, {user: test.UserTeamGitOpsTeam2, object: globalScript, action: write, allow: false}, diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 5dc6a2295e..c3aff9cc30 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -3946,11 +3946,11 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() { }, }, http.StatusOK, &modifyQueryResponse{}) - // Attempt to view a query, should fail. - s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d", cqr.Query.ID), getQueryRequest{}, http.StatusForbidden, &getQueryResponse{}) + // Attempt to view a query, should work. + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d", cqr.Query.ID), getQueryRequest{}, http.StatusOK, &getQueryResponse{}) - // Attempt to list all queries, should fail. - s.DoJSON("GET", "/api/latest/fleet/queries", listQueriesRequest{}, http.StatusForbidden, &listQueriesResponse{}) + // Attempt to list all queries, should work. + s.DoJSON("GET", "/api/latest/fleet/queries", listQueriesRequest{}, http.StatusOK, &listQueriesResponse{}) // Attempt to delete queries, should allow. s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/queries/id/%d", cqr.Query.ID), deleteQueryByIDRequest{}, http.StatusOK, &deleteQueryByIDResponse{}) @@ -3975,8 +3975,8 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() { // Attempt to remove a query from the global schedule, should allow. s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/packs/schedule/%d", sqr.Scheduled.ID), deleteScheduledQueryRequest{}, http.StatusOK, &scheduleQueryResponse{}) - // Attempt to read the global schedule, should disallow. - s.DoJSON("GET", "/api/latest/fleet/schedule", nil, http.StatusForbidden, &getGlobalScheduleResponse{}) + // Attempt to read the global schedule, should allow. + s.DoJSON("GET", "/api/latest/fleet/schedule", nil, http.StatusOK, &getGlobalScheduleResponse{}) // Attempt to create a pack, should allow. cpr := createPackResponse{} @@ -4015,8 +4015,11 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() { }, }, http.StatusOK, &mgplr) - // Attempt to read a global policy, should fail. - s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/policies/%d", gplr.Policy.ID), getPolicyByIDRequest{}, http.StatusForbidden, &getPolicyByIDResponse{}) + // Attempt to read a global policy, should allow. + s.DoJSON( + "GET", fmt.Sprintf("/api/latest/fleet/policies/%d", gplr.Policy.ID), getPolicyByIDRequest{}, http.StatusOK, + &getPolicyByIDResponse{}, + ) // Attempt to delete a global policy, should allow. s.DoJSON("POST", "/api/latest/fleet/policies/delete", deleteGlobalPoliciesRequest{ @@ -4038,8 +4041,11 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() { }, }, http.StatusOK, &mtplr) - // Attempt to view a team policy, should fail. - s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/team/%d/policies/%d", t1.ID, tplr.Policy.ID), getTeamPolicyByIDRequest{}, http.StatusForbidden, &getTeamPolicyByIDResponse{}) + // Attempt to view a team policy, should allow. + s.DoJSON( + "GET", fmt.Sprintf("/api/latest/fleet/team/%d/policies/%d", t1.ID, tplr.Policy.ID), getTeamPolicyByIDRequest{}, http.StatusOK, + &getTeamPolicyByIDResponse{}, + ) // Attempt to delete a team policy, should allow. s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/delete", t1.ID), deleteTeamPoliciesRequest{ @@ -4213,8 +4219,11 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() { // Attempt to read the global schedule, should fail. s.DoJSON("GET", "/api/latest/fleet/schedule", nil, http.StatusForbidden, &getGlobalScheduleResponse{}) - // Attempt to read the team's schedule, should fail. - s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/schedule", t1.ID), getTeamScheduleRequest{}, http.StatusForbidden, &getTeamScheduleResponse{}) + // Attempt to read the team's schedule, should allow. + s.DoJSON( + "GET", fmt.Sprintf("/api/latest/fleet/teams/%d/schedule", t1.ID), getTeamScheduleRequest{}, http.StatusOK, + &getTeamScheduleResponse{}, + ) // Attempt to read other team's schedule, should fail. s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/schedule", t2.ID), getTeamScheduleRequest{}, http.StatusForbidden, &getTeamScheduleResponse{}) @@ -4280,8 +4289,11 @@ func (s *integrationEnterpriseTestSuite) TestGitOpsUserActions() { }, }, http.StatusForbidden, &modifyTeamPolicyResponse{}) - // Attempt to view a team policy, should fail. - s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/team/%d/policies/%d", t1.ID, ttplr.Policy.ID), getTeamPolicyByIDRequest{}, http.StatusForbidden, &getTeamPolicyByIDResponse{}) + // Attempt to view a team policy, should allow. + s.DoJSON( + "GET", fmt.Sprintf("/api/latest/fleet/team/%d/policies/%d", t1.ID, ttplr.Policy.ID), getTeamPolicyByIDRequest{}, http.StatusOK, + &getTeamPolicyByIDResponse{}, + ) // Attempt to view another team's policy, should fail. s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/team/%d/policies/%d", t2.ID, t2p.ID), getTeamPolicyByIDRequest{}, http.StatusForbidden, &getTeamPolicyByIDResponse{}) diff --git a/server/service/queries_test.go b/server/service/queries_test.go index 452c1df788..bae9a20eaf 100644 --- a/server/service/queries_test.go +++ b/server/service/queries_test.go @@ -469,7 +469,7 @@ func TestQueryAuth(t *testing.T) { &fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)}, globalQuery.ID, false, - true, + false, false, }, { @@ -477,7 +477,7 @@ func TestQueryAuth(t *testing.T) { &fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)}, teamQuery.ID, false, - true, + false, false, }, { @@ -589,7 +589,7 @@ func TestQueryAuth(t *testing.T) { teamGitOps, teamQuery.ID, false, - true, + false, false, }, { diff --git a/server/service/scripts_test.go b/server/service/scripts_test.go index a11cfb5633..e4ef2ef20c 100644 --- a/server/service/scripts_test.go +++ b/server/service/scripts_test.go @@ -563,8 +563,8 @@ func TestSavedScripts(t *testing.T) { { name: "global gitops", user: &fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)}, - shouldFailTeamWrite: true, - shouldFailGlobalWrite: true, + shouldFailTeamWrite: false, + shouldFailGlobalWrite: false, shouldFailTeamRead: true, shouldFailGlobalRead: true, }, @@ -603,7 +603,7 @@ func TestSavedScripts(t *testing.T) { { name: "team gitops, belongs to team", user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}}, - shouldFailTeamWrite: true, + shouldFailTeamWrite: false, shouldFailGlobalWrite: true, shouldFailTeamRead: true, shouldFailGlobalRead: true, diff --git a/server/service/team_schedule_test.go b/server/service/team_schedule_test.go index 16f74ab1c4..e348a95fe6 100644 --- a/server/service/team_schedule_test.go +++ b/server/service/team_schedule_test.go @@ -82,7 +82,7 @@ func TestTeamScheduleAuth(t *testing.T) { "global gitops", &fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)}, false, - true, + false, }, { "team admin, belongs to team", @@ -117,7 +117,7 @@ func TestTeamScheduleAuth(t *testing.T) { "team gitops, belongs to team", &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}}, false, - true, + false, }, { "team maintainer, DOES NOT belong to team",