From 246f2f7879452475ec188afc4e9749bdd0c4aab4 Mon Sep 17 00:00:00 2001 From: Jacob Bednarz Date: Tue, 7 Jul 2020 01:58:00 +1000 Subject: [PATCH] Add support for Access Audit Logs (#496) Introduces support for querying the Access Audit Log endpoint. API reference: https://api.cloudflare.com/#access-requests-access-requests-audit --- access_audit_log.go | 84 +++++++++++++++++++++++++++++ access_audit_log_example_test.go | 24 +++++++++ access_audit_log_test.go | 92 ++++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 access_audit_log.go create mode 100644 access_audit_log_example_test.go create mode 100644 access_audit_log_test.go diff --git a/access_audit_log.go b/access_audit_log.go new file mode 100644 index 00000000000..c8eebb46baf --- /dev/null +++ b/access_audit_log.go @@ -0,0 +1,84 @@ +package cloudflare + +import ( + "encoding/json" + "net/url" + "strconv" + "time" + + "github.com/pkg/errors" +) + +// AccessAuditLogRecord is the structure of a single Access Audit Log entry. +type AccessAuditLogRecord struct { + UserEmail string `json:"user_email"` + IPAddress string `json:"ip_address"` + AppUID string `json:"app_uid"` + AppDomain string `json:"app_domain"` + Action string `json:"action"` + Connection string `json:"connection"` + Allowed bool `json:"allowed"` + CreatedAt *time.Time `json:"created_at"` + RayID string `json:"ray_id"` +} + +// AccessAuditLogListResponse represents the response from the list +// access applications endpoint. +type AccessAuditLogListResponse struct { + Result []AccessAuditLogRecord `json:"result"` + Response + ResultInfo `json:"result_info"` +} + +// AccessAuditLogFilterOptions provides the structure of available audit log +// filters. +type AccessAuditLogFilterOptions struct { + Direction string + Since *time.Time + Until *time.Time + Limit int +} + +// AccessAuditLogs retrieves all audit logs for the Access service. +// +// API reference: https://api.cloudflare.com/#access-requests-access-requests-audit +func (api *API) AccessAuditLogs(accountID string, opts AccessAuditLogFilterOptions) ([]AccessAuditLogRecord, error) { + uri := "/accounts/" + accountID + "/access/logs/access-requests?" + opts.Encode() + + res, err := api.makeRequest("GET", uri, nil) + if err != nil { + return []AccessAuditLogRecord{}, errors.Wrap(err, errMakeRequestError) + } + + var accessAuditLogListResponse AccessAuditLogListResponse + err = json.Unmarshal(res, &accessAuditLogListResponse) + if err != nil { + return []AccessAuditLogRecord{}, errors.Wrap(err, errUnmarshalError) + } + + return accessAuditLogListResponse.Result, nil +} + +// Encode is a custom method for encoding the filter options into a usable HTTP +// query parameter string. +func (a AccessAuditLogFilterOptions) Encode() string { + v := url.Values{} + + if a.Direction != "" { + v.Set("direction", a.Direction) + } + + if a.Limit > 0 { + v.Set("limit", strconv.Itoa(a.Limit)) + } + + if a.Since != nil { + v.Set("since", (*a.Since).Format(time.RFC3339)) + } + + if a.Until != nil { + v.Set("until", (*a.Until).Format(time.RFC3339)) + } + + return v.Encode() +} diff --git a/access_audit_log_example_test.go b/access_audit_log_example_test.go new file mode 100644 index 00000000000..51adce9827f --- /dev/null +++ b/access_audit_log_example_test.go @@ -0,0 +1,24 @@ +package cloudflare_test + +import ( + "encoding/json" + "fmt" + "log" + + cloudflare "github.com/cloudflare/cloudflare-go" +) + +func ExampleAPI_AccessAuditLogs() { + api, err := cloudflare.New("deadbeef", "test@example.org") + if err != nil { + log.Fatal(err) + } + + filterOpts := cloudflare.AccessAuditLogFilterOptions{} + results, _ := api.AccessAuditLogs("someaccountid", filterOpts) + + for _, record := range results { + b, _ := json.Marshal(record) + fmt.Println(string(b)) + } +} diff --git a/access_audit_log_test.go b/access_audit_log_test.go new file mode 100644 index 00000000000..753d68bc7d2 --- /dev/null +++ b/access_audit_log_test.go @@ -0,0 +1,92 @@ +package cloudflare + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestAccessAuditLogs(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "user_email": "michelle@example.com", + "ip_address": "198.41.129.166", + "app_uid": "df7e2w5f-02b7-4d9d-af26-8d1988fca630", + "app_domain": "test.example.com/admin", + "action": "login", + "connection": "saml", + "allowed": false, + "created_at": "2014-01-01T05:20:00.12345Z", + "ray_id": "187d944c61940c77" + } + ] +} + `) + } + + mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/access/logs/access-requests", handler) + createdAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") + + want := []AccessAuditLogRecord{{ + UserEmail: "michelle@example.com", + IPAddress: "198.41.129.166", + AppUID: "df7e2w5f-02b7-4d9d-af26-8d1988fca630", + AppDomain: "test.example.com/admin", + Action: "login", + Connection: "saml", + Allowed: false, + CreatedAt: &createdAt, + RayID: "187d944c61940c77", + }} + + actual, err := client.AccessAuditLogs("01a7362d577a6c3019a474fd6f485823", AccessAuditLogFilterOptions{}) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestAccessAuditLogsEncodeAllParametersDefined(t *testing.T) { + since, _ := time.Parse(time.RFC3339, "2020-07-01T00:00:00Z") + until, _ := time.Parse(time.RFC3339, "2020-07-02T00:00:00Z") + + opts := AccessAuditLogFilterOptions{ + Direction: "desc", + Since: &since, + Until: &until, + Limit: 10, + } + + assert.Equal(t, "direction=desc&limit=10&since=2020-07-01T00%3A00%3A00Z&until=2020-07-02T00%3A00%3A00Z", opts.Encode()) +} + +func TestAccessAuditLogsEncodeOnlyDatesDefined(t *testing.T) { + since, _ := time.Parse(time.RFC3339, "2020-07-01T00:00:00Z") + until, _ := time.Parse(time.RFC3339, "2020-07-02T00:00:00Z") + + opts := AccessAuditLogFilterOptions{ + Since: &since, + Until: &until, + } + + assert.Equal(t, "since=2020-07-01T00%3A00%3A00Z&until=2020-07-02T00%3A00%3A00Z", opts.Encode()) +} + +func TestAccessAuditLogsEncodeEmptyValues(t *testing.T) { + opts := AccessAuditLogFilterOptions{} + + assert.Equal(t, "", opts.Encode()) +}