|
| 1 | +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one |
| 2 | +// or more contributor license agreements. Licensed under the Elastic License; |
| 3 | +// you may not use this file except in compliance with the Elastic License. |
| 4 | + |
| 5 | +//go:build integration |
| 6 | + |
| 7 | +package server |
| 8 | + |
| 9 | +import ( |
| 10 | + "bytes" |
| 11 | + "context" |
| 12 | + "encoding/json" |
| 13 | + "fmt" |
| 14 | + "io" |
| 15 | + "net/http" |
| 16 | + "strings" |
| 17 | + "testing" |
| 18 | + "text/template" |
| 19 | + "time" |
| 20 | + |
| 21 | + "github.com/elastic/fleet-server/v7/internal/pkg/apikey" |
| 22 | + "github.com/elastic/fleet-server/v7/internal/pkg/dl" |
| 23 | + "github.com/elastic/fleet-server/v7/internal/pkg/model" |
| 24 | + "github.com/gofrs/uuid" |
| 25 | + "github.com/hashicorp/go-cleanhttp" |
| 26 | + "github.com/stretchr/testify/require" |
| 27 | +) |
| 28 | + |
| 29 | +func AgentCheckin(t *testing.T, ctx context.Context, srv *tserver, agentID, key string) string { |
| 30 | + cli := cleanhttp.DefaultClient() |
| 31 | + var obj map[string]interface{} |
| 32 | + |
| 33 | + t.Logf("Fake a checkin for agent %s", agentID) |
| 34 | + req, err := http.NewRequestWithContext(ctx, "POST", srv.baseURL()+"/api/fleet/agents/"+agentID+"/checkin", strings.NewReader(checkinBody)) |
| 35 | + require.NoError(t, err) |
| 36 | + req.Header.Set("Authorization", "ApiKey "+key) |
| 37 | + req.Header.Set("User-Agent", "elastic agent "+serverVersion) |
| 38 | + req.Header.Set("Content-Type", "application/json") |
| 39 | + res, err := cli.Do(req) |
| 40 | + require.NoError(t, err) |
| 41 | + |
| 42 | + require.Equal(t, http.StatusOK, res.StatusCode) |
| 43 | + t.Log("Checkin successful, verify body") |
| 44 | + p, _ := io.ReadAll(res.Body) |
| 45 | + res.Body.Close() |
| 46 | + err = json.Unmarshal(p, &obj) |
| 47 | + require.NoError(t, err) |
| 48 | + |
| 49 | + actionsRaw, ok := obj["actions"] |
| 50 | + require.True(t, ok, "expected actions is missing") |
| 51 | + actions, ok := actionsRaw.([]interface{}) |
| 52 | + require.True(t, ok, "expected actions to be an array") |
| 53 | + require.Equal(t, len(actions), 1, "expected 1 action") |
| 54 | + action, ok := actions[0].(map[string]interface{}) |
| 55 | + require.True(t, ok, "expected action to be an object") |
| 56 | + |
| 57 | + aIDRaw, ok := action["id"] |
| 58 | + require.True(t, ok, "expected action id attribute missing") |
| 59 | + actionID, ok := aIDRaw.(string) |
| 60 | + require.True(t, ok, "expected action id to be string") |
| 61 | + |
| 62 | + return actionID |
| 63 | +} |
| 64 | + |
| 65 | +func AgentAck(t *testing.T, ctx context.Context, srv *tserver, actionID, agentID, key string) { |
| 66 | + t.Logf("Fake an ack for action %s for agent %s", actionID, agentID) |
| 67 | + body := fmt.Sprintf(`{ |
| 68 | + "events": [{ |
| 69 | + "action_id": "%s", |
| 70 | + "agent_id": "%s", |
| 71 | + "message": "test-message", |
| 72 | + "type": "ACTION_RESULT", |
| 73 | + "subtype": "ACKNOWLEDGED" |
| 74 | + }] |
| 75 | + }`, actionID, agentID) |
| 76 | + req, err := http.NewRequestWithContext(ctx, "POST", srv.baseURL()+"/api/fleet/agents/"+agentID+"/acks", strings.NewReader(body)) |
| 77 | + require.NoError(t, err) |
| 78 | + req.Header.Set("Authorization", "ApiKey "+key) |
| 79 | + req.Header.Set("Content-Type", "application/json") |
| 80 | + cli := cleanhttp.DefaultClient() |
| 81 | + res, err := cli.Do(req) |
| 82 | + require.NoError(t, err) |
| 83 | + defer res.Body.Close() |
| 84 | + |
| 85 | + require.Equal(t, http.StatusOK, res.StatusCode) |
| 86 | + t.Log("Ack successful, verify body") |
| 87 | +} |
| 88 | + |
| 89 | +type GetAgentResponse struct { |
| 90 | + Agent model.Agent `json:"_source"` |
| 91 | +} |
| 92 | + |
| 93 | +func AssertAgentDocContainNamespace(t *testing.T, ctx context.Context, srv *tserver, agentID string, namespace string) { |
| 94 | + res, err := srv.bulker.Client().Get(".fleet-agents", agentID) |
| 95 | + require.NoError(t, err) |
| 96 | + |
| 97 | + defer res.Body.Close() |
| 98 | + var getAgentRes GetAgentResponse |
| 99 | + err = json.NewDecoder(res.Body).Decode(&getAgentRes) |
| 100 | + require.NoError(t, err) |
| 101 | + |
| 102 | + require.EqualValues(t, getAgentRes.Agent.Namespaces, []string{namespace}) |
| 103 | +} |
| 104 | + |
| 105 | +func CreateActionDocument(t *testing.T, ctx context.Context, srv *tserver, action model.Action) { |
| 106 | + body, err := json.Marshal(action) |
| 107 | + require.NoError(t, err) |
| 108 | + _, err = srv.bulker.Client().Index(".fleet-actions", bytes.NewReader(body)) |
| 109 | + require.NoError(t, err) |
| 110 | +} |
| 111 | + |
| 112 | +type GetActionResults struct { |
| 113 | + Hits struct { |
| 114 | + Hits []struct { |
| 115 | + Result model.ActionResult `json:"_source"` |
| 116 | + } `json:"hits"` |
| 117 | + } `json:"hits"` |
| 118 | +} |
| 119 | + |
| 120 | +func CheckActionResultsNamespace(t *testing.T, ctx context.Context, srv *tserver, actionID string, namespace string) { |
| 121 | + queryTmpl := `{ |
| 122 | + "query": { |
| 123 | + "bool" : { |
| 124 | + "must" : { |
| 125 | + "terms": { |
| 126 | + "action_id": [ "{{.}}" ] |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + } |
| 131 | + }` |
| 132 | + queryBuff := bytes.Buffer{} |
| 133 | + tmpl, err := template.New("").Parse(queryTmpl) |
| 134 | + require.NoError(t, err) |
| 135 | + err = tmpl.Execute(&queryBuff, actionID) |
| 136 | + require.NoError(t, err) |
| 137 | + |
| 138 | + client := srv.bulker.Client() |
| 139 | + |
| 140 | + res, err := client.Search( |
| 141 | + client.Search.WithIndex(".fleet-actions-results"), |
| 142 | + client.Search.WithBody(strings.NewReader(queryBuff.String())), |
| 143 | + ) |
| 144 | + defer res.Body.Close() |
| 145 | + require.NoError(t, err) |
| 146 | + |
| 147 | + var getActionResultsRes GetActionResults |
| 148 | + err = json.NewDecoder(res.Body).Decode(&getActionResultsRes) |
| 149 | + require.NoError(t, err) |
| 150 | + |
| 151 | + require.Len(t, getActionResultsRes.Hits.Hits, 1) |
| 152 | + require.EqualValues(t, getActionResultsRes.Hits.Hits[0].Result.Namespaces, []string{namespace}) |
| 153 | +} |
| 154 | + |
| 155 | +func Test_Agent_Namespace_test1(t *testing.T) { |
| 156 | + testNamespace := "test1" |
| 157 | + ctx, cancel := context.WithCancel(context.Background()) |
| 158 | + defer cancel() |
| 159 | + |
| 160 | + // Start test server |
| 161 | + srv, err := startTestServer(t, ctx, policyData) |
| 162 | + require.NoError(t, err) |
| 163 | + |
| 164 | + t.Log("Create policy with namespace test1") |
| 165 | + var policyRemoteID = uuid.Must(uuid.NewV4()).String() |
| 166 | + var policyDataNamespaceTest = model.PolicyData{ |
| 167 | + Outputs: map[string]map[string]interface{}{ |
| 168 | + "default": { |
| 169 | + "type": "elasticsearch", |
| 170 | + }, |
| 171 | + }, |
| 172 | + OutputPermissions: json.RawMessage(`{"default": {} }`), |
| 173 | + Inputs: []map[string]interface{}{}, |
| 174 | + Agent: json.RawMessage(`{"monitoring": {"use_output":"default"}}`), |
| 175 | + } |
| 176 | + |
| 177 | + _, err = dl.CreatePolicy(ctx, srv.bulker, model.Policy{ |
| 178 | + PolicyID: policyRemoteID, |
| 179 | + Namespaces: []string{testNamespace}, |
| 180 | + RevisionIdx: 1, |
| 181 | + DefaultFleetServer: false, |
| 182 | + Data: &policyDataNamespaceTest, |
| 183 | + }) |
| 184 | + if err != nil { |
| 185 | + t.Fatal(err) |
| 186 | + } |
| 187 | + |
| 188 | + t.Log("Create API key and enrollment key for new policy") |
| 189 | + newKey, err := apikey.Create(ctx, srv.bulker.Client(), "default", "", "true", []byte(`{ |
| 190 | + "fleet-apikey-enroll": { |
| 191 | + "cluster": [], |
| 192 | + "index": [], |
| 193 | + "applications": [{ |
| 194 | + "application": "fleet", |
| 195 | + "privileges": ["no-privileges"], |
| 196 | + "resources": ["*"] |
| 197 | + }] |
| 198 | + } |
| 199 | + }`), map[string]interface{}{ |
| 200 | + "managed_by": "fleet", |
| 201 | + "managed": true, |
| 202 | + "type": "enroll", |
| 203 | + "policy_id": policyRemoteID, |
| 204 | + }) |
| 205 | + if err != nil { |
| 206 | + t.Fatal(err) |
| 207 | + } |
| 208 | + |
| 209 | + _, err = dl.CreateEnrollmentAPIKey(ctx, srv.bulker, model.EnrollmentAPIKey{ |
| 210 | + Name: "TestNamespace1", |
| 211 | + Namespaces: []string{testNamespace}, |
| 212 | + APIKey: newKey.Key, |
| 213 | + APIKeyID: newKey.ID, |
| 214 | + PolicyID: policyRemoteID, |
| 215 | + Active: true, |
| 216 | + }) |
| 217 | + if err != nil { |
| 218 | + t.Fatal(err) |
| 219 | + } |
| 220 | + |
| 221 | + t.Log("Enroll agent") |
| 222 | + srvCopy := srv |
| 223 | + srvCopy.enrollKey = newKey.Token() |
| 224 | + agentID, key := EnrollAgent(enrollBody, t, ctx, srvCopy) |
| 225 | + |
| 226 | + AssertAgentDocContainNamespace(t, ctx, srv, agentID, testNamespace) |
| 227 | + // cleanup |
| 228 | + defer func() { |
| 229 | + err = srv.bulker.Delete(ctx, dl.FleetAgents, agentID) |
| 230 | + if err != nil { |
| 231 | + t.Log("could not clean up agent") |
| 232 | + } |
| 233 | + }() |
| 234 | + |
| 235 | + actionID := AgentCheckin(t, ctx, srvCopy, agentID, key) |
| 236 | + AgentAck(t, ctx, srvCopy, actionID, agentID, key) |
| 237 | + |
| 238 | + t.Log("Create SETTINGS Action") |
| 239 | + newActionID, _ := uuid.NewV4() |
| 240 | + var actionData = model.Action{ |
| 241 | + Agents: []string{agentID}, |
| 242 | + Expiration: time.Now().Add(time.Hour * 2000).Format(time.RFC3339), |
| 243 | + ActionID: newActionID.String(), |
| 244 | + Namespaces: []string{"test1"}, |
| 245 | + Type: "SETTINGS", |
| 246 | + Data: []byte("{\"log_level\": \"debug\"}"), |
| 247 | + } |
| 248 | + |
| 249 | + CreateActionDocument(t, ctx, srv, actionData) |
| 250 | + |
| 251 | + t.Log("Checkin so that agent gets the SETTINGS action") |
| 252 | + actionID = AgentCheckin(t, ctx, srvCopy, agentID, key) |
| 253 | + |
| 254 | + t.Log("Ack so that fleet create the action results") |
| 255 | + AgentAck(t, ctx, srvCopy, actionID, agentID, key) |
| 256 | + |
| 257 | + t.Log("Check action results has the correct namespace") |
| 258 | + CheckActionResultsNamespace(t, ctx, srv, actionID, "test1") |
| 259 | +} |
0 commit comments