Skip to content

Commit

Permalink
Add support for Access Audit Logs (cloudflare#496)
Browse files Browse the repository at this point in the history
Introduces support for querying the Access Audit Log endpoint.

API reference: https://api.cloudflare.com/#access-requests-access-requests-audit
  • Loading branch information
jacobbednarz authored Jul 6, 2020
1 parent f76f4c8 commit 246f2f7
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 0 deletions.
84 changes: 84 additions & 0 deletions access_audit_log.go
Original file line number Diff line number Diff line change
@@ -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()
}
24 changes: 24 additions & 0 deletions access_audit_log_example_test.go
Original file line number Diff line number Diff line change
@@ -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", "[email protected]")
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))
}
}
92 changes: 92 additions & 0 deletions access_audit_log_test.go
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"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: "[email protected]",
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())
}

0 comments on commit 246f2f7

Please sign in to comment.