gitops role authorization changes for fleetctl gitops (#16710)

To support `fleetctl gitops`, gitops role can now read policies/queries
and write scripts.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Documented any permissions changes (docs/Using
Fleet/manage-access.md)
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Victor Lyuboslavsky 2024-02-12 16:44:35 -06:00 committed by GitHub
parent 5a9f5d86a5
commit 95437f9044
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 59 additions and 61 deletions

View file

@ -0,0 +1 @@
gitops role can now read queries/policies and write (but not execute) scripts

View file

@ -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

View file

@ -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
}

View file

@ -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},

View file

@ -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{})

View file

@ -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,
},
{

View file

@ -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,

View file

@ -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",