diff --git a/changes/issue-2456-policies-yaml b/changes/issue-2456-policies-yaml new file mode 100644 index 000000000..821a8d8bb --- /dev/null +++ b/changes/issue-2456-policies-yaml @@ -0,0 +1 @@ +* fleetctl supports applying policy yaml specs diff --git a/cmd/fleetctl/apply.go b/cmd/fleetctl/apply.go index 6c7b42a76..dfa6be0b9 100644 --- a/cmd/fleetctl/apply.go +++ b/cmd/fleetctl/apply.go @@ -23,10 +23,11 @@ type specMetadata struct { } type specGroup struct { - Queries []*fleet.QuerySpec - Teams []*fleet.TeamSpec - Packs []*fleet.PackSpec - Labels []*fleet.LabelSpec + Queries []*fleet.QuerySpec + Teams []*fleet.TeamSpec + Packs []*fleet.PackSpec + Labels []*fleet.LabelSpec + Policies []*fleet.PolicySpec // This needs to be interface{} to allow for the patch logic. Otherwise we send a request that looks to the // server like the user explicitly set the zero values. AppConfig interface{} @@ -40,9 +41,10 @@ type TeamSpec struct { func specGroupFromBytes(b []byte) (*specGroup, error) { specs := &specGroup{ - Queries: []*fleet.QuerySpec{}, - Packs: []*fleet.PackSpec{}, - Labels: []*fleet.LabelSpec{}, + Queries: []*fleet.QuerySpec{}, + Packs: []*fleet.PackSpec{}, + Labels: []*fleet.LabelSpec{}, + Policies: []*fleet.PolicySpec{}, } for _, spec := range splitYaml(string(b)) { @@ -115,6 +117,13 @@ func specGroupFromBytes(b []byte) (*specGroup, error) { } specs.Teams = append(specs.Teams, teamSpec.Team) + case fleet.PolicyKind: + var policySpec *fleet.PolicySpec + if err := yaml.Unmarshal(s.Spec, &policySpec); err != nil { + return nil, errors.Wrap(err, "unmarshaling "+kind+" spec") + } + specs.Policies = append(specs.Policies, policySpec) + default: return nil, errors.Errorf("unknown kind %q", s.Kind) } @@ -201,7 +210,7 @@ func applyCommand() *cli.Command { if len(specs.Teams) > 0 { if err := fleetClient.ApplyTeams(specs.Teams); err != nil { - return errors.Wrap(err, "applying queries") + return errors.Wrap(err, "applying teams") } logf(c, "[+] applied %d teams\n", len(specs.Teams)) } @@ -213,6 +222,13 @@ func applyCommand() *cli.Command { log(c, "[+] applied user roles\n") } + if len(specs.Policies) > 0 { + if err := fleetClient.ApplyPolicies(specs.Policies); err != nil { + return errors.Wrap(err, "applying policies") + } + logf(c, "[+] applied %d policies\n", len(specs.Policies)) + } + return nil }, } diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index 84ec2fa99..087898031 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -234,3 +234,44 @@ spec: assert.True(t, savedAppConfig.HostSettings.EnableHostUsers) assert.True(t, savedAppConfig.HostSettings.EnableSoftwareInventory) } + +func TestApplyPolicySpecs(t *testing.T) { + _, ds := runServerWithMockedDS(t) + + var gotPolicies []*fleet.PolicySpec + + ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { + assert.Equal(t, "team1", name) + return &fleet.Team{ID: 123, Name: "team1"}, nil + } + ds.ApplyPolicySpecsFunc = func(ctx context.Context, specs []*fleet.PolicySpec) error { + gotPolicies = specs + return nil + } + + name := writeTmpYml(t, `--- +apiVersion: v1 +kind: policy +spec: + query: some query +--- +apiVersion: v1 +kind: policy +spec: + query: some other query + team: team1 + resolution: something something +`) + + assert.Equal(t, "[+] applied 2 policies\n", runAppForTest(t, []string{"apply", "-f", name})) + assert.Equal(t, []*fleet.PolicySpec{ + { + QueryName: "some query", + }, + { + QueryName: "some other query", + Team: "team1", + Resolution: "something something", + }, + }, gotPolicies) +} diff --git a/docs/01-Using-Fleet/03-REST-API.md b/docs/01-Using-Fleet/03-REST-API.md index 217a1e303..cb5833ac1 100644 --- a/docs/01-Using-Fleet/03-REST-API.md +++ b/docs/01-Using-Fleet/03-REST-API.md @@ -4366,6 +4366,7 @@ Returns the spec for the specified pack by pack name. - [Get policy by ID](#get-policy-by-id) - [Add policy](#add-policy) - [Remove policies](#remove-policies) +- [Apply policy specs](#apply-policy-specs) `In Fleet 4.3.0, the Policies feature was introduced.` @@ -4396,15 +4397,16 @@ Hosts that do not return results for a policy's query are "Failing." "id": 1, "query_id": 2, "query_name": "Gatekeeper enabled", + "resolution": "Resolution steps", "passing_host_count": 2000, - "failing_host_count": 300, + "failing_host_count": 300 }, { "id": 2, "query_id": 3, "query_name": "Primary disk encrypted", "passing_host_count": 2300, - "failing_host_count": 0, + "failing_host_count": 0 } ] } @@ -4418,7 +4420,7 @@ Hosts that do not return results for a policy's query are "Failing." | Name | Type | In | Description | | ------------------ | ------- | ---- | ------------------------------------------------------------------------------------------------------------- | -| id | integer | path | **Required.** The policy's ID. | +| id | integer | path | **Required.** The policy's ID. | #### Example @@ -4434,8 +4436,9 @@ Hosts that do not return results for a policy's query are "Failing." "id": 1, "query_id": 2, "query_name": "Gatekeeper enabled", + "resolution": "Resolution steps", "passing_host_count": 2000, - "failing_host_count": 300, + "failing_host_count": 300 } } ``` @@ -4446,9 +4449,10 @@ Hosts that do not return results for a policy's query are "Failing." #### Parameters -| Name | Type | In | Description | -| -------- | ------- | ---- | ------------------------------ | -| query_id | integer | body | **Required.** The query's ID. | +| Name | Type | In | Description | +| ---------- | ------- | ---- | ------------------------------------- | +| query_id | integer | body | **Required.** The query's ID. | +| resolution | string | body | The resolution steps for the policy. | #### Example @@ -4472,9 +4476,10 @@ Hosts that do not return results for a policy's query are "Failing." "id": 2, "query_id": 2, "query_name": "Primary disk encrypted", + "resolution": "Some resolution steps", "passing_host_count": 0, - "failing_host_count": 0, - }, + "failing_host_count": 0 + } } ``` @@ -4510,6 +4515,47 @@ Hosts that do not return results for a policy's query are "Failing." } ``` +### Apply policy specs + +Applies the supplied policy specs to Fleet. Each policy requires a `query` property, and optionally a `resolution` detail +to explain how to resolve the failure of the policy, and a `team` if the policy is at the specified team level. + +`POST /api/v1/fleet/spec/policies` + +#### Parameters + +| Name | Type | In | Description | +| ----- | ---- | ---- | ------------------------------------------------------------------------------------------------------------- | +| specs | list | body | A list of the policy to apply. Each policy requires a `query` and optionally `team` and `resolution`. | + +#### Example + +`POST /api/v1/fleet/spec/policies` + +##### Request body + +```json +{ + "specs": [ + { + "query": "query name" + }, + { + "query": "some other query name", + "team": "team1" + }, + { + "query": "query3", + "resolution": "Add something to your config" + } + ] +} +``` + +##### Default response + +`Status: 200` + --- ## Team Policies diff --git a/go.sum b/go.sum index e53eda985..3d7b25969 100644 --- a/go.sum +++ b/go.sum @@ -867,6 +867,7 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/zwass/kit v0.0.0-20210625184505-ec5b5c5cce9c h1:TWQ2UvXPkhPxI2KmApKBOCaV6yD2N4mlvqFQ/DlPtpQ= github.com/zwass/kit v0.0.0-20210625184505-ec5b5c5cce9c/go.mod h1:OYYulo9tUqRadRLwB0+LE914sa1ui2yL7OrcU3Q/1XY= diff --git a/server/authz/policy.rego b/server/authz/policy.rego index d61bf728f..914d09cee 100644 --- a/server/authz/policy.rego +++ b/server/authz/policy.rego @@ -414,11 +414,12 @@ allow { # Global Admin and Maintainer users can read and write policies allow { - object.type == ["policy","team_policy"][_] + object.type == "policy" subject.global_role == admin action == [read, write][_] } +# Global maintainer can read and write global policies allow { is_null(object.team_id) object.type == "policy" @@ -426,20 +427,14 @@ allow { action == [read, write][_] } +# Global Maintainer and Observer users can read any policies allow { object.type == "policy" - subject.global_role == maintainer - action == [read][_] + subject.global_role == [maintainer,observer][_] + action == read } -# Global Observer users can read policies -allow { - object.type == "policy" - subject.global_role == observer - action == [read][_] -} - -# Team admin and maintainers can read and write policies +# Team admin and maintainers can read and write policies for their teams allow { not is_null(object.team_id) object.team_id == subject.teams[_].id @@ -449,7 +444,6 @@ allow { } # Team admin and maintainers can read global policies - allow { is_null(object.team_id) object.type == "policy" diff --git a/server/authz/policy_test.go b/server/authz/policy_test.go index 44c6f307d..d68d0c865 100644 --- a/server/authz/policy_test.go +++ b/server/authz/policy_test.go @@ -458,6 +458,45 @@ func TestAuthorizeCarves(t *testing.T) { }) } +func TestAuthorizePolicies(t *testing.T) { + t.Parallel() + + policy := &fleet.Policy{} + teamPolicy := &fleet.Policy{TeamID: ptr.Uint(1)} + runTestCases(t, []authTestCase{ + {user: test.UserNoRoles, object: policy, action: write, allow: false}, + + {user: test.UserAdmin, object: policy, action: write, allow: true}, + {user: test.UserAdmin, object: policy, action: read, allow: true}, + {user: test.UserMaintainer, object: policy, action: write, allow: true}, + {user: test.UserMaintainer, object: policy, action: read, allow: true}, + {user: test.UserObserver, object: policy, action: write, allow: false}, + {user: test.UserObserver, object: policy, action: read, allow: true}, + + {user: test.UserAdmin, object: teamPolicy, action: write, allow: true}, + {user: test.UserAdmin, object: teamPolicy, action: read, allow: true}, + {user: test.UserMaintainer, object: teamPolicy, action: write, allow: false}, + {user: test.UserMaintainer, object: teamPolicy, action: read, allow: true}, + {user: test.UserObserver, object: teamPolicy, action: write, allow: false}, + {user: test.UserObserver, object: teamPolicy, action: read, allow: true}, + + {user: test.UserTeamAdminTeam1, object: teamPolicy, action: write, allow: true}, + {user: test.UserTeamAdminTeam1, object: teamPolicy, action: read, allow: true}, + {user: test.UserTeamAdminTeam2, object: teamPolicy, action: write, allow: false}, + {user: test.UserTeamAdminTeam2, object: teamPolicy, action: read, allow: false}, + + {user: test.UserTeamMaintainerTeam1, object: teamPolicy, action: write, allow: true}, + {user: test.UserTeamMaintainerTeam1, object: teamPolicy, action: read, allow: true}, + {user: test.UserTeamMaintainerTeam2, object: teamPolicy, action: write, allow: false}, + {user: test.UserTeamMaintainerTeam2, object: teamPolicy, action: read, allow: false}, + + {user: test.UserTeamObserverTeam1, object: teamPolicy, action: write, allow: false}, + {user: test.UserTeamObserverTeam1, object: teamPolicy, action: read, allow: true}, + {user: test.UserTeamObserverTeam2, object: teamPolicy, action: write, allow: false}, + {user: test.UserTeamObserverTeam2, object: teamPolicy, action: read, allow: false}, + }) +} + func assertAuthorized(t *testing.T, user *fleet.User, object, action interface{}) { t.Helper() diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 597cc4228..66a354266 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -1482,7 +1482,7 @@ func testHostsListByPolicy(t *testing.T, ds *Datastore) { filter := fleet.TeamFilter{User: test.UserAdmin} q := test.NewQuery(t, ds, "query1", "select 1", 0, true) - p, err := ds.NewGlobalPolicy(context.Background(), q.ID) + p, err := ds.NewGlobalPolicy(context.Background(), q.ID, "") require.NoError(t, err) // When policy response is null, we list all hosts that haven't reported at all for the policy, or errored out diff --git a/server/datastore/mysql/migrations/tables/20211013133706_AddResolutionColumnToPolicies.go b/server/datastore/mysql/migrations/tables/20211013133706_AddResolutionColumnToPolicies.go new file mode 100644 index 000000000..f1c42c317 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20211013133706_AddResolutionColumnToPolicies.go @@ -0,0 +1,26 @@ +package tables + +import ( + "database/sql" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20211013133706, Down_20211013133706) +} + +func Up_20211013133706(tx *sql.Tx) error { + if columnExists(tx, "policies", "resolution") { + return nil + } + + if _, err := tx.Exec(`ALTER TABLE policies ADD COLUMN resolution TEXT`); err != nil { + return errors.Wrap(err, "add column resolution to policies") + } + return nil +} + +func Down_20211013133706(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go index c7fd5c598..7c05bd6b6 100644 --- a/server/datastore/mysql/policies.go +++ b/server/datastore/mysql/policies.go @@ -2,6 +2,7 @@ package mysql import ( "context" + "database/sql" "fmt" "sort" "strings" @@ -12,8 +13,8 @@ import ( "github.com/pkg/errors" ) -func (ds *Datastore) NewGlobalPolicy(ctx context.Context, queryID uint) (*fleet.Policy, error) { - res, err := ds.writer.ExecContext(ctx, `INSERT INTO policies (query_id) VALUES (?)`, queryID) +func (ds *Datastore) NewGlobalPolicy(ctx context.Context, queryID uint, resolution string) (*fleet.Policy, error) { + res, err := ds.writer.ExecContext(ctx, `INSERT INTO policies (query_id, resolution) VALUES (?, ?)`, queryID, resolution) if err != nil { return nil, errors.Wrap(err, "inserting new policy") } @@ -184,8 +185,8 @@ func (ds *Datastore) PolicyQueriesForHost(ctx context.Context, host *fleet.Host) return results, nil } -func (ds *Datastore) NewTeamPolicy(ctx context.Context, teamID uint, queryID uint) (*fleet.Policy, error) { - res, err := ds.writer.ExecContext(ctx, `INSERT INTO policies (query_id, team_id) VALUES (?, ?)`, queryID, teamID) +func (ds *Datastore) NewTeamPolicy(ctx context.Context, teamID uint, queryID uint, resolution string) (*fleet.Policy, error) { + res, err := ds.writer.ExecContext(ctx, `INSERT INTO policies (query_id, team_id, resolution) VALUES (?, ?, ?)`, queryID, teamID, resolution) if err != nil { return nil, errors.Wrap(err, "inserting new team policy") } @@ -208,3 +209,48 @@ func (ds *Datastore) DeleteTeamPolicies(ctx context.Context, teamID uint, ids [] func (ds *Datastore) TeamPolicy(ctx context.Context, teamID uint, policyID uint) (*fleet.Policy, error) { return policyDB(ctx, ds.reader, policyID, &teamID) } + +func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, specs []*fleet.PolicySpec) error { + return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { + for _, spec := range specs { + if spec.QueryName == "" { + return errors.New("query name must not be empty") + } + + // We update by hand because team_id can be null and that means compound index wont work + + teamCheck := `team_id is NULL` + args := []interface{}{spec.QueryName} + if spec.Team != "" { + teamCheck = `team_id=(SELECT id FROM teams WHERE name=?)` + args = append(args, spec.Team) + } + row := tx.QueryRowxContext(ctx, + fmt.Sprintf(`SELECT 1 FROM policies WHERE query_id=(SELECT id FROM queries WHERE name=?) AND %s`, teamCheck), + args..., + ) + var exists int + err := row.Scan(&exists) + if err != nil && err != sql.ErrNoRows { + return errors.Wrap(err, "checking policy existence") + } + if exists > 0 { + _, err = tx.ExecContext(ctx, + fmt.Sprintf(`UPDATE policies SET resolution=? WHERE query_id=(SELECT id FROM queries WHERE name=?) AND %s`, teamCheck), + append([]interface{}{spec.Resolution}, args...)..., + ) + if err != nil { + return errors.Wrap(err, "exec ApplyPolicySpecs update") + } + } else { + _, err = tx.ExecContext(ctx, + `INSERT INTO policies (query_id, team_id, resolution) VALUES ((SELECT id FROM queries WHERE name=?), (SELECT id FROM teams WHERE name=?),?)`, + spec.QueryName, spec.Team, spec.Resolution) + if err != nil { + return errors.Wrap(err, "exec ApplyPolicySpecs insert") + } + } + } + return nil + }) +} diff --git a/server/datastore/mysql/policies_test.go b/server/datastore/mysql/policies_test.go index ef1d44106..ee86d5c8c 100644 --- a/server/datastore/mysql/policies_test.go +++ b/server/datastore/mysql/policies_test.go @@ -24,6 +24,7 @@ func TestPolicies(t *testing.T) { {"TeamPolicy", testTeamPolicy}, {"PolicyQueriesForHost", testPolicyQueriesForHost}, {"TeamPolicyTransfer", testTeamPolicyTransfer}, + {"ApplyPolicySpec", testApplyPolicySpec}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -41,7 +42,7 @@ func testPoliciesNewGlobalPolicy(t *testing.T, ds *Datastore) { Saved: true, }) require.NoError(t, err) - p, err := ds.NewGlobalPolicy(context.Background(), q.ID) + p, err := ds.NewGlobalPolicy(context.Background(), q.ID, "") require.NoError(t, err) assert.Equal(t, "query1", p.QueryName) @@ -53,7 +54,7 @@ func testPoliciesNewGlobalPolicy(t *testing.T, ds *Datastore) { Saved: true, }) require.NoError(t, err) - _, err = ds.NewGlobalPolicy(context.Background(), q2.ID) + _, err = ds.NewGlobalPolicy(context.Background(), q2.ID, "") require.NoError(t, err) policies, err := ds.ListGlobalPolicies(context.Background()) @@ -108,7 +109,7 @@ func testPoliciesMembershipView(t *testing.T, ds *Datastore) { Saved: true, }) require.NoError(t, err) - p, err := ds.NewGlobalPolicy(context.Background(), q.ID) + p, err := ds.NewGlobalPolicy(context.Background(), q.ID, "") require.NoError(t, err) q2, err := ds.NewQuery(context.Background(), &fleet.Query{ @@ -118,7 +119,7 @@ func testPoliciesMembershipView(t *testing.T, ds *Datastore) { Saved: true, }) require.NoError(t, err) - p2, err := ds.NewGlobalPolicy(context.Background(), q2.ID) + p2, err := ds.NewGlobalPolicy(context.Background(), q2.ID, "") require.NoError(t, err) assert.Equal(t, "query1", p.QueryName) @@ -192,19 +193,21 @@ func testTeamPolicy(t *testing.T, ds *Datastore) { prevPolicies, err := ds.ListGlobalPolicies(context.Background()) require.NoError(t, err) - _, err = ds.NewTeamPolicy(context.Background(), 99999999, q.ID) + _, err = ds.NewTeamPolicy(context.Background(), 99999999, q.ID, "") require.Error(t, err) - p, err := ds.NewTeamPolicy(context.Background(), team1.ID, q.ID) + p, err := ds.NewTeamPolicy(context.Background(), team1.ID, q.ID, "some resolution") require.NoError(t, err) assert.Equal(t, "query1", p.QueryName) + require.NotNil(t, p.Resolution) + assert.Equal(t, "some resolution", *p.Resolution) globalPolicies, err := ds.ListGlobalPolicies(context.Background()) require.NoError(t, err) require.Len(t, globalPolicies, len(prevPolicies)) - _, err = ds.NewTeamPolicy(context.Background(), team2.ID, q2.ID) + _, err = ds.NewTeamPolicy(context.Background(), team2.ID, q2.ID, "") require.NoError(t, err) teamPolicies, err := ds.ListTeamPolicies(context.Background(), team1.ID) @@ -264,7 +267,7 @@ func testPolicyQueriesForHost(t *testing.T, ds *Datastore) { Saved: true, }) require.NoError(t, err) - gp, err := ds.NewGlobalPolicy(context.Background(), q.ID) + gp, err := ds.NewGlobalPolicy(context.Background(), q.ID, "") require.NoError(t, err) q2, err := ds.NewQuery(context.Background(), &fleet.Query{ @@ -274,7 +277,7 @@ func testPolicyQueriesForHost(t *testing.T, ds *Datastore) { Saved: true, }) require.NoError(t, err) - tp, err := ds.NewTeamPolicy(context.Background(), team1.ID, q2.ID) + tp, err := ds.NewTeamPolicy(context.Background(), team1.ID, q2.ID, "") require.NoError(t, err) queries, err := ds.PolicyQueriesForHost(context.Background(), host1) @@ -337,10 +340,10 @@ func testTeamPolicyTransfer(t *testing.T, ds *Datastore) { Saved: true, }) require.NoError(t, err) - teamPolicy, err := ds.NewTeamPolicy(context.Background(), team1.ID, q.ID) + teamPolicy, err := ds.NewTeamPolicy(context.Background(), team1.ID, q.ID, "") require.NoError(t, err) - globalPolicy, err := ds.NewGlobalPolicy(context.Background(), q.ID) + globalPolicy, err := ds.NewGlobalPolicy(context.Background(), q.ID, "") require.NoError(t, err) require.NoError(t, ds.RecordPolicyQueryExecutions( @@ -371,3 +374,98 @@ func testTeamPolicyTransfer(t *testing.T, ds *Datastore) { checkPassingCount(0) } + +func testApplyPolicySpec(t *testing.T, ds *Datastore) { + team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"}) + require.NoError(t, err) + + q, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query1", + Description: "query1 desc", + Query: "select 1;", + Saved: true, + }) + require.NoError(t, err) + + q2, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query2", + Description: "query2 desc", + Query: "select 1;", + Saved: true, + }) + require.NoError(t, err) + + require.NoError(t, ds.ApplyPolicySpecs(context.Background(), []*fleet.PolicySpec{ + { + QueryName: "query1", + Resolution: "some resolution", + }, + { + QueryName: "query2", + Resolution: "some other resolution", + Team: "team1", + }, + { + QueryName: "query1", + Team: "team1", + }, + })) + + policies, err := ds.ListGlobalPolicies(context.Background()) + require.NoError(t, err) + require.Len(t, policies, 1) + assert.Equal(t, q.ID, policies[0].QueryID) + require.NotNil(t, policies[0].Resolution) + assert.Equal(t, "some resolution", *policies[0].Resolution) + + teamPolicies, err := ds.ListTeamPolicies(context.Background(), team1.ID) + require.NoError(t, err) + require.Len(t, teamPolicies, 2) + assert.Equal(t, q2.ID, teamPolicies[0].QueryID) + require.NotNil(t, teamPolicies[0].Resolution) + assert.Equal(t, "some other resolution", *teamPolicies[0].Resolution) + + assert.Equal(t, q.ID, teamPolicies[1].QueryID) + require.NotNil(t, teamPolicies[1].Resolution) + assert.Equal(t, "", *teamPolicies[1].Resolution) + + require.Error(t, ds.ApplyPolicySpecs(context.Background(), []*fleet.PolicySpec{ + { + QueryName: "query13", + }, + })) + + require.Error(t, ds.ApplyPolicySpecs(context.Background(), []*fleet.PolicySpec{ + { + Team: "team1", + }, + })) + + require.Error(t, ds.ApplyPolicySpecs(context.Background(), []*fleet.PolicySpec{ + { + QueryName: "query123", + Team: "team1", + }, + })) + + // Make sure apply is idempotent + require.NoError(t, ds.ApplyPolicySpecs(context.Background(), []*fleet.PolicySpec{ + { + QueryName: "query1", + Resolution: "some resolution", + }, + { + QueryName: "query2", + Resolution: "some other resolution", + Team: "team1", + }, + { + QueryName: "query1", + Team: "team1", + }, + })) + + policies, err = ds.ListGlobalPolicies(context.Background()) + require.NoError(t, err) + require.Len(t, policies, 1) +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index eec03be42..2d1f6da4a 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -309,9 +309,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=106 DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB AUTO_INCREMENT=107 DEFAULT CHARSET=utf8mb4; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `network_interfaces` ( @@ -400,6 +400,7 @@ CREATE TABLE `policies` ( `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `team_id` int(10) unsigned DEFAULT NULL, + `resolution` text, PRIMARY KEY (`id`), KEY `fk_policies_query_id` (`query_id`), KEY `fk_policies_team_id` (`team_id`), diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index f92ca83e3..6df9ba8d3 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -342,7 +342,7 @@ type Datastore interface { /////////////////////////////////////////////////////////////////////////////// // GlobalPoliciesStore - NewGlobalPolicy(ctx context.Context, queryID uint) (*Policy, error) + NewGlobalPolicy(ctx context.Context, queryID uint, resolution string) (*Policy, error) Policy(ctx context.Context, id uint) (*Policy, error) RecordPolicyQueryExecutions(ctx context.Context, host *Host, results map[uint]*bool, updated time.Time) error @@ -350,6 +350,7 @@ type Datastore interface { DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint, error) PolicyQueriesForHost(ctx context.Context, host *Host) (map[string]string, error) + ApplyPolicySpecs(ctx context.Context, specs []*PolicySpec) error // MigrateTables creates and migrates the table schemas MigrateTables(ctx context.Context) error @@ -363,7 +364,7 @@ type Datastore interface { /////////////////////////////////////////////////////////////////////////////// // Team Policies - NewTeamPolicy(ctx context.Context, teamID uint, queryID uint) (*Policy, error) + NewTeamPolicy(ctx context.Context, teamID uint, queryID uint, resolution string) (*Policy, error) ListTeamPolicies(ctx context.Context, teamID uint) ([]*Policy, error) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error) TeamPolicy(ctx context.Context, teamID uint, policyID uint) (*Policy, error) diff --git a/server/fleet/global_policies.go b/server/fleet/global_policies.go deleted file mode 100644 index 9b54603dd..000000000 --- a/server/fleet/global_policies.go +++ /dev/null @@ -1,23 +0,0 @@ -package fleet - -type Policy struct { - ID uint `json:"id"` - QueryID uint `json:"query_id" db:"query_id"` - QueryName string `json:"query_name" db:"query_name"` - PassingHostCount uint `json:"passing_host_count" db:"passing_host_count"` - FailingHostCount uint `json:"failing_host_count" db:"failing_host_count"` - TeamID *uint `json:"team_id" db:"team_id"` - - UpdateCreateTimestamps -} - -func (p Policy) AuthzType() string { - return "policy" -} - -type HostPolicy struct { - ID uint `json:"id" db:"id"` - QueryID uint `json:"query_id" db:"query_id"` - QueryName string `json:"query_name" db:"query_name"` - Response string `json:"response" db:"response"` -} diff --git a/server/fleet/policies.go b/server/fleet/policies.go new file mode 100644 index 000000000..ae4a0534a --- /dev/null +++ b/server/fleet/policies.go @@ -0,0 +1,34 @@ +package fleet + +type Policy struct { + ID uint `json:"id"` + QueryID uint `json:"query_id" db:"query_id"` + QueryName string `json:"query_name" db:"query_name"` + PassingHostCount uint `json:"passing_host_count" db:"passing_host_count"` + FailingHostCount uint `json:"failing_host_count" db:"failing_host_count"` + TeamID *uint `json:"team_id" db:"team_id"` + Resolution *string `json:"resolution,omitempty" db:"resolution"` + + UpdateCreateTimestamps +} + +func (p Policy) AuthzType() string { + return "policy" +} + +const ( + PolicyKind = "policy" +) + +type HostPolicy struct { + ID uint `json:"id" db:"id"` + QueryID uint `json:"query_id" db:"query_id"` + QueryName string `json:"query_name" db:"query_name"` + Response string `json:"response" db:"response"` +} + +type PolicySpec struct { + QueryName string `json:"query"` + Resolution string `json:"resolution,omitempty"` + Team string `json:"team,omitempty"` +} diff --git a/server/fleet/service.go b/server/fleet/service.go index 027cad8d0..1e3c1f764 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -399,10 +399,11 @@ type Service interface { /////////////////////////////////////////////////////////////////////////////// // GlobalPolicyService - NewGlobalPolicy(ctx context.Context, queryID uint) (*Policy, error) + NewGlobalPolicy(ctx context.Context, queryID uint, resolution string) (*Policy, error) ListGlobalPolicies(ctx context.Context) ([]*Policy, error) DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint, error) GetPolicyByIDQueries(ctx context.Context, policyID uint) (*Policy, error) + ApplyPolicySpecs(ctx context.Context, policies []*PolicySpec) error /////////////////////////////////////////////////////////////////////////////// // Software @@ -413,7 +414,7 @@ type Service interface { /////////////////////////////////////////////////////////////////////////////// // Team Policies - NewTeamPolicy(ctx context.Context, teamID uint, queryID uint) (*Policy, error) + NewTeamPolicy(ctx context.Context, teamID uint, queryID uint, resolution string) (*Policy, error) ListTeamPolicies(ctx context.Context, teamID uint) ([]*Policy, error) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error) GetTeamPolicyByIDQueries(ctx context.Context, teamID uint, policyID uint) (*Policy, error) diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 011e5515f..f9b4f01c0 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -281,6 +281,8 @@ type DeleteGlobalPoliciesFunc func(ctx context.Context, ids []uint) ([]uint, err type PolicyQueriesForHostFunc func(ctx context.Context, host *fleet.Host) (map[string]string, error) +type ApplyPolicySpecsFunc func(ctx context.Context, specs []*fleet.PolicySpec) error + type MigrateTablesFunc func(ctx context.Context) error type MigrateDataFunc func(ctx context.Context) error @@ -707,6 +709,9 @@ type DataStore struct { PolicyQueriesForHostFunc PolicyQueriesForHostFunc PolicyQueriesForHostFuncInvoked bool + ApplyPolicySpecsFunc ApplyPolicySpecsFunc + ApplyPolicySpecsFuncInvoked bool + MigrateTablesFunc MigrateTablesFunc MigrateTablesFuncInvoked bool @@ -1383,7 +1388,7 @@ func (s *DataStore) RecordStatisticsSent(ctx context.Context) error { return s.RecordStatisticsSentFunc(ctx) } -func (s *DataStore) NewGlobalPolicy(ctx context.Context, queryID uint) (*fleet.Policy, error) { +func (s *DataStore) NewGlobalPolicy(ctx context.Context, queryID uint, resolution string) (*fleet.Policy, error) { s.NewGlobalPolicyFuncInvoked = true return s.NewGlobalPolicyFunc(ctx, queryID) } @@ -1413,6 +1418,11 @@ func (s *DataStore) PolicyQueriesForHost(ctx context.Context, host *fleet.Host) return s.PolicyQueriesForHostFunc(ctx, host) } +func (s *DataStore) ApplyPolicySpecs(ctx context.Context, specs []*fleet.PolicySpec) error { + s.ApplyPolicySpecsFuncInvoked = true + return s.ApplyPolicySpecsFunc(ctx, specs) +} + func (s *DataStore) MigrateTables(ctx context.Context) error { s.MigrateTablesFuncInvoked = true return s.MigrateTablesFunc(ctx) @@ -1433,7 +1443,7 @@ func (s *DataStore) ListSoftware(ctx context.Context, teamId *uint, opt fleet.Li return s.ListSoftwareFunc(ctx, teamId, opt) } -func (s *DataStore) NewTeamPolicy(ctx context.Context, teamID uint, queryID uint) (*fleet.Policy, error) { +func (s *DataStore) NewTeamPolicy(ctx context.Context, teamID uint, queryID uint, resolution string) (*fleet.Policy, error) { s.NewTeamPolicyFuncInvoked = true return s.NewTeamPolicyFunc(ctx, teamID, queryID) } diff --git a/server/service/client_teams.go b/server/service/client_teams.go index a7faae3a1..8cb3667e3 100644 --- a/server/service/client_teams.go +++ b/server/service/client_teams.go @@ -23,3 +23,12 @@ func (c *Client) ApplyTeams(specs []*fleet.TeamSpec) error { var responseBody applyTeamSpecsResponse return c.authenticatedRequest(req, verb, path, &responseBody) } + +// ApplyPolicies sends the list of Policies to be applied to the +// Fleet instance. +func (c *Client) ApplyPolicies(specs []*fleet.PolicySpec) error { + req := applyPolicySpecsRequest{Specs: specs} + verb, path := "POST", "/api/v1/fleet/spec/policies" + var responseBody applyPolicySpecsResponse + return c.authenticatedRequest(req, verb, path, &responseBody) +} diff --git a/server/service/global_policies.go b/server/service/global_policies.go index b6943d03e..a7df9b6d6 100644 --- a/server/service/global_policies.go +++ b/server/service/global_policies.go @@ -4,6 +4,7 @@ import ( "context" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/pkg/errors" ) ///////////////////////////////////////////////////////////////////////////////// @@ -11,7 +12,8 @@ import ( ///////////////////////////////////////////////////////////////////////////////// type globalPolicyRequest struct { - QueryID uint `json:"query_id"` + QueryID uint `json:"query_id"` + Resolution string `json:"resolution"` } type globalPolicyResponse struct { @@ -23,19 +25,19 @@ func (r globalPolicyResponse) error() error { return r.Err } func globalPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) { req := request.(*globalPolicyRequest) - resp, err := svc.NewGlobalPolicy(ctx, req.QueryID) + resp, err := svc.NewGlobalPolicy(ctx, req.QueryID, req.Resolution) if err != nil { return globalPolicyResponse{Err: err}, nil } return globalPolicyResponse{Policy: resp}, nil } -func (svc Service) NewGlobalPolicy(ctx context.Context, queryID uint) (*fleet.Policy, error) { +func (svc Service) NewGlobalPolicy(ctx context.Context, queryID uint, resolution string) (*fleet.Policy, error) { if err := svc.authz.Authorize(ctx, &fleet.Policy{}, fleet.ActionWrite); err != nil { return nil, err } - return svc.ds.NewGlobalPolicy(ctx, queryID) + return svc.ds.NewGlobalPolicy(ctx, queryID, "") } ///////////////////////////////////////////////////////////////////////////////// @@ -133,3 +135,50 @@ func (svc Service) DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint return svc.ds.DeleteGlobalPolicies(ctx, ids) } + +///////////////////////////////////////////////////////////////////////////////// +// Apply Spec +///////////////////////////////////////////////////////////////////////////////// + +type applyPolicySpecsRequest struct { + Specs []*fleet.PolicySpec `json:"specs"` +} + +type applyPolicySpecsResponse struct { + Err error `json:"error,omitempty"` +} + +func (r applyPolicySpecsResponse) error() error { return r.Err } + +func applyPolicySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) { + req := request.(*applyPolicySpecsRequest) + err := svc.ApplyPolicySpecs(ctx, req.Specs) + if err != nil { + return applyPolicySpecsResponse{Err: err}, nil + } + return applyPolicySpecsResponse{}, nil +} + +func (svc Service) ApplyPolicySpecs(ctx context.Context, policies []*fleet.PolicySpec) error { + checkGlobalPolicyAuth := false + for _, policy := range policies { + if policy.Team != "" { + team, err := svc.ds.TeamByName(ctx, policy.Team) + if err != nil { + return errors.Wrap(err, "getting team by name") + } + if err := svc.authz.Authorize(ctx, &fleet.Policy{TeamID: &team.ID}, fleet.ActionWrite); err != nil { + return err + } + continue + } + checkGlobalPolicyAuth = true + } + if checkGlobalPolicyAuth { + if err := svc.authz.Authorize(ctx, &fleet.Policy{}, fleet.ActionWrite); err != nil { + return err + } + } + + return svc.ds.ApplyPolicySpecs(ctx, policies) +} diff --git a/server/service/global_policies_test.go b/server/service/global_policies_test.go index 27064762a..135a1f831 100644 --- a/server/service/global_policies_test.go +++ b/server/service/global_policies_test.go @@ -26,6 +26,12 @@ func TestGlobalPoliciesAuth(t *testing.T) { ds.DeleteGlobalPoliciesFunc = func(ctx context.Context, ids []uint) ([]uint, error) { return nil, nil } + ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { + return &fleet.Team{ID: 1}, nil + } + ds.ApplyPolicySpecsFunc = func(ctx context.Context, specs []*fleet.PolicySpec) error { + return nil + } var testCases = []struct { name string @@ -74,7 +80,7 @@ func TestGlobalPoliciesAuth(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tt.user}) - _, err := svc.NewGlobalPolicy(ctx, 2) + _, err := svc.NewGlobalPolicy(ctx, 2, "") checkAuthErr(t, tt.shouldFailWrite, err) _, err = svc.ListGlobalPolicies(ctx) @@ -85,6 +91,13 @@ func TestGlobalPoliciesAuth(t *testing.T) { _, err = svc.DeleteGlobalPolicies(ctx, []uint{1}) checkAuthErr(t, tt.shouldFailWrite, err) + + err = svc.ApplyPolicySpecs(ctx, []*fleet.PolicySpec{ + { + QueryName: "query1", + }, + }) + checkAuthErr(t, tt.shouldFailWrite, err) }) } } diff --git a/server/service/handler.go b/server/service/handler.go index b43a6d52e..467ca3f15 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -709,6 +709,8 @@ func attachNewStyleFleetAPIRoutes(r *mux.Router, svc fleet.Service, opts []kitht e.GET("/api/v1/fleet/teams/{team_id}/policies/{policy_id}", getTeamPolicyByIDEndpoint, getTeamPolicyByIDRequest{}) e.POST("/api/v1/fleet/teams/{team_id}/policies/delete", deleteTeamPoliciesEndpoint, deleteTeamPoliciesRequest{}) + e.POST("/api/v1/fleet/spec/policies", applyPolicySpecsEndpoint, applyPolicySpecsRequest{}) + e.GET("/api/v1/fleet/packs/{id:[0-9]+}", getPackEndpoint, getPackRequest{}) e.GET("/api/v1/fleet/software", listSoftwareEndpoint, listSoftwareRequest{}) diff --git a/server/service/team_policies.go b/server/service/team_policies.go index e4bd6fc5e..edf223966 100644 --- a/server/service/team_policies.go +++ b/server/service/team_policies.go @@ -12,8 +12,9 @@ import ( ///////////////////////////////////////////////////////////////////////////////// type teamPolicyRequest struct { - TeamID uint `url:"team_id"` - QueryID uint `json:"query_id"` + TeamID uint `url:"team_id"` + QueryID uint `json:"query_id"` + Resolution string `json:"resolution"` } type teamPolicyResponse struct { @@ -25,19 +26,19 @@ func (r teamPolicyResponse) error() error { return r.Err } func teamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) { req := request.(*teamPolicyRequest) - resp, err := svc.NewTeamPolicy(ctx, req.TeamID, req.QueryID) + resp, err := svc.NewTeamPolicy(ctx, req.TeamID, req.QueryID, "") if err != nil { return teamPolicyResponse{Err: err}, nil } return teamPolicyResponse{Policy: resp}, nil } -func (svc Service) NewTeamPolicy(ctx context.Context, teamID uint, queryID uint) (*fleet.Policy, error) { +func (svc Service) NewTeamPolicy(ctx context.Context, teamID uint, queryID uint, resolution string) (*fleet.Policy, error) { if err := svc.authz.Authorize(ctx, &fleet.Policy{TeamID: ptr.Uint(teamID)}, fleet.ActionWrite); err != nil { return nil, err } - return svc.ds.NewTeamPolicy(ctx, teamID, queryID) + return svc.ds.NewTeamPolicy(ctx, teamID, queryID, resolution) } ///////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/team_policies_test.go b/server/service/team_policies_test.go index 503edcf39..26714eb0f 100644 --- a/server/service/team_policies_test.go +++ b/server/service/team_policies_test.go @@ -28,6 +28,12 @@ func TestTeamPoliciesAuth(t *testing.T) { ds.DeleteTeamPoliciesFunc = func(ctx context.Context, teamID uint, ids []uint) ([]uint, error) { return nil, nil } + ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { + return &fleet.Team{ID: 1}, nil + } + ds.ApplyPolicySpecsFunc = func(ctx context.Context, specs []*fleet.PolicySpec) error { + return nil + } var testCases = []struct { name string @@ -94,7 +100,7 @@ func TestTeamPoliciesAuth(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tt.user}) - _, err := svc.NewTeamPolicy(ctx, 1, 2) + _, err := svc.NewTeamPolicy(ctx, 1, 2, "") checkAuthErr(t, tt.shouldFailWrite, err) _, err = svc.ListTeamPolicies(ctx, 1) @@ -105,6 +111,14 @@ func TestTeamPoliciesAuth(t *testing.T) { _, err = svc.DeleteTeamPolicies(ctx, 1, []uint{1}) checkAuthErr(t, tt.shouldFailWrite, err) + + err = svc.ApplyPolicySpecs(ctx, []*fleet.PolicySpec{ + { + QueryName: "query1", + Team: "team1", + }, + }) + checkAuthErr(t, tt.shouldFailWrite, err) }) } } diff --git a/server/test/users.go b/server/test/users.go index 39396aad5..e333006ab 100644 --- a/server/test/users.go +++ b/server/test/users.go @@ -21,4 +21,58 @@ var ( ID: 4, GlobalRole: ptr.String(fleet.RoleObserver), } + UserTeamAdminTeam1 = &fleet.User{ + ID: 5, + Teams: []fleet.UserTeam{ + { + Team: fleet.Team{ID: 1}, + Role: fleet.RoleAdmin, + }, + }, + } + UserTeamAdminTeam2 = &fleet.User{ + ID: 6, + Teams: []fleet.UserTeam{ + { + Team: fleet.Team{ID: 2}, + Role: fleet.RoleAdmin, + }, + }, + } + UserTeamMaintainerTeam1 = &fleet.User{ + ID: 7, + Teams: []fleet.UserTeam{ + { + Team: fleet.Team{ID: 1}, + Role: fleet.RoleMaintainer, + }, + }, + } + UserTeamMaintainerTeam2 = &fleet.User{ + ID: 8, + Teams: []fleet.UserTeam{ + { + Team: fleet.Team{ID: 2}, + Role: fleet.RoleMaintainer, + }, + }, + } + UserTeamObserverTeam1 = &fleet.User{ + ID: 9, + Teams: []fleet.UserTeam{ + { + Team: fleet.Team{ID: 1}, + Role: fleet.RoleObserver, + }, + }, + } + UserTeamObserverTeam2 = &fleet.User{ + ID: 10, + Teams: []fleet.UserTeam{ + { + Team: fleet.Team{ID: 2}, + Role: fleet.RoleObserver, + }, + }, + } )