Skip to content

Commit b3c5f6e

Browse files
authored
Merge pull request #79 from planetscale/fatih-audit-logs
Add audit-log service endpoint
2 parents 07f779b + 30ff561 commit b3c5f6e

File tree

4 files changed

+270
-5
lines changed

4 files changed

+270
-5
lines changed

planetscale/audit_logs.go

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package planetscale
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
"time"
9+
10+
"github.com/pkg/errors"
11+
)
12+
13+
// AuditLogEvent represents an audit log's event type
14+
type AuditLogEvent string
15+
16+
const (
17+
AuditLogEventBranchCreated AuditLogEvent = "branch.created"
18+
AuditLogEventBranchDeleted AuditLogEvent = "branch.deleted"
19+
AuditLogEventDatabaseCreated AuditLogEvent = "database.created"
20+
AuditLogEventDatabaseDeleted AuditLogEvent = "database.deleted"
21+
AuditLogEventDeployRequestApproved AuditLogEvent = "deploy_request.approved"
22+
AuditLogEventDeployRequestClosed AuditLogEvent = "deploy_request.closed"
23+
AuditLogEventDeployRequestCreated AuditLogEvent = "deploy_request.created"
24+
AuditLogEventDeployRequestDeleted AuditLogEvent = "deploy_request.deleted"
25+
AuditLogEventDeployRequestQueued AuditLogEvent = "deploy_request.queued"
26+
AuditLogEventDeployRequestUnqueued AuditLogEvent = "deploy_request.unqueued"
27+
AuditLogEventIntegrationCreated AuditLogEvent = "integration.created"
28+
AuditLogEventIntegrationDeleted AuditLogEvent = "integration.deleted"
29+
AuditLogEventOrganizationInvitationCreated AuditLogEvent = "organization_invitation.created"
30+
AuditLogEventOrganizationInvitationDeleted AuditLogEvent = "organization_invitation.deleted"
31+
AuditLogEventOrganizationMembershipCreated AuditLogEvent = "organization_membership.created"
32+
AuditLogEventOrganizationJoined AuditLogEvent = "organization.joined"
33+
AuditLogEventOrganizationRemovedMember AuditLogEvent = "organization.removed_member"
34+
AuditLogEventOrganizationDisabledSSO AuditLogEvent = "organization.disabled_sso"
35+
AuditLogEventOrganizationEnabledSSO AuditLogEvent = "organization.enabled_sso"
36+
AuditLogEventOrganizationUpdatedRole AuditLogEvent = "organization.updated_role"
37+
AuditLogEventServiceTokenCreated AuditLogEvent = "service_token.created"
38+
AuditLogEventServiceTokenDeleted AuditLogEvent = "service_token.deleted"
39+
AuditLogEventServiceTokenGrantedAccess AuditLogEvent = "service_token.granted_access"
40+
)
41+
42+
var _ AuditLogsService = &auditlogsService{}
43+
44+
// AuditLogsService is an interface for communicating with the PlanetScale
45+
// AuditLogs API endpoints.
46+
type AuditLogsService interface {
47+
List(context.Context, *ListAuditLogsRequest) ([]*AuditLog, error)
48+
}
49+
50+
// ListAuditLogsRequest encapsulates the request for listing the audit logs of
51+
// an organization.
52+
type ListAuditLogsRequest struct {
53+
Organization string
54+
55+
// Events can be used to filter out only the given audit log events.
56+
Events []AuditLogEvent
57+
}
58+
59+
// AuditLog represents a PlanetScale audit log.
60+
type AuditLog struct {
61+
ID string `json:"id"`
62+
Type string `json:"type"`
63+
64+
ActorID string `json:"actor_id"`
65+
ActorType string `json:"actor_type"`
66+
ActorDisplayName string `json:"actor_display_name"`
67+
68+
AuditableID string `json:"auditable_id"`
69+
AuditableType string `json:"auditable_type"`
70+
AuditableDisplayName string `json:"auditable_display_name"`
71+
72+
AuditAction string `json:"audit_action"`
73+
Action string `json:"action"`
74+
75+
Location string `json:"location"`
76+
RemoteIP string `json:"remote_ip"`
77+
78+
TargetID string `json:"target_id"`
79+
TargetType string `json:"target_type"`
80+
TargetDisplayName string `json:"target_display_name"`
81+
82+
Metadata map[string]string `json:"metadata"`
83+
84+
CreatedAt time.Time `json:"created_at"`
85+
UpdatedAt time.Time `json:"updated_at"`
86+
}
87+
88+
type auditlogsResponse struct {
89+
AuditLogs []*AuditLog `json:"data"`
90+
}
91+
92+
type auditlogsService struct {
93+
client *Client
94+
}
95+
96+
func NewAuditLogsService(client *Client) *auditlogsService {
97+
return &auditlogsService{
98+
client: client,
99+
}
100+
}
101+
102+
// List returns the audit logs for an organization.
103+
func (o *auditlogsService) List(ctx context.Context, listReq *ListAuditLogsRequest) ([]*AuditLog, error) {
104+
if listReq.Organization == "" {
105+
return nil, errors.New("organization is not set")
106+
}
107+
108+
path := auditlogsAPIPath(listReq.Organization)
109+
110+
v := url.Values{}
111+
if len(listReq.Events) != 0 {
112+
for _, action := range listReq.Events {
113+
v.Add("filters[]", fmt.Sprintf("audit_action:%s", action))
114+
}
115+
}
116+
117+
if vals := v.Encode(); vals != "" {
118+
path += "?" + vals
119+
}
120+
121+
req, err := o.client.newRequest(http.MethodGet, path, nil)
122+
if err != nil {
123+
return nil, errors.Wrap(err, "error creating request for listing audit logs")
124+
}
125+
126+
resp := &auditlogsResponse{}
127+
if err := o.client.do(ctx, req, &resp); err != nil {
128+
return nil, err
129+
}
130+
131+
return resp.AuditLogs, nil
132+
}
133+
134+
func auditlogsAPIPath(org string) string {
135+
return fmt.Sprintf("%s/%s/audit-log", organizationsAPIPath, org)
136+
}

planetscale/audit_logs_test.go

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package planetscale
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
"time"
9+
10+
qt "github.com/frankban/quicktest"
11+
)
12+
13+
func TestAuditLogs_List(t *testing.T) {
14+
c := qt.New(t)
15+
16+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17+
w.WriteHeader(200)
18+
out := `{
19+
"type": "list",
20+
"next_page": "https://api.planetscale.com/v1/organizations/planetscale/audit-log?page=2",
21+
"prev_page": null,
22+
"data": [
23+
{
24+
"id": "ecxuvovgfo95",
25+
"type": "AuditLogEvent",
26+
"actor_id": "d4hkujnkswjk",
27+
"actor_type": "User",
28+
"auditable_id": "kbog8qlq6lp4",
29+
"auditable_type": "DeployRequest",
30+
"target_id": "m40xz7x6gvvk",
31+
"target_type": "Database",
32+
"location": "Chicago, IL",
33+
"target_display_name": "planetscale",
34+
"metadata": {
35+
"from": "add-name-to-service-tokens",
36+
"into": "main"
37+
},
38+
"audit_action": "deploy_request.queued",
39+
"action": "queued",
40+
"actor_display_name": "Elom Gomez",
41+
"auditable_display_name": "deploy request #102",
42+
"remote_ip": "45.19.24.124",
43+
"created_at": "2021-07-19T17:13:45.000Z",
44+
"updated_at": "2021-07-19T17:13:45.000Z"
45+
}
46+
]
47+
}`
48+
_, err := w.Write([]byte(out))
49+
c.Assert(err, qt.IsNil)
50+
}))
51+
52+
client, err := NewClient(WithBaseURL(ts.URL))
53+
c.Assert(err, qt.IsNil)
54+
55+
ctx := context.Background()
56+
57+
auditLogs, err := client.AuditLogs.List(ctx, &ListAuditLogsRequest{
58+
Organization: testOrg,
59+
Events: []AuditLogEvent{
60+
AuditLogEventBranchDeleted,
61+
AuditLogEventOrganizationJoined,
62+
},
63+
})
64+
65+
want := []*AuditLog{
66+
{
67+
ID: "ecxuvovgfo95",
68+
Type: "AuditLogEvent",
69+
ActorID: "d4hkujnkswjk",
70+
ActorType: "User",
71+
AuditableID: "kbog8qlq6lp4",
72+
AuditableType: "DeployRequest",
73+
TargetID: "m40xz7x6gvvk",
74+
TargetType: "Database",
75+
Location: "Chicago, IL",
76+
TargetDisplayName: "planetscale",
77+
Metadata: map[string]string{
78+
"from": "add-name-to-service-tokens",
79+
"into": "main",
80+
},
81+
AuditAction: "deploy_request.queued",
82+
Action: "queued",
83+
ActorDisplayName: "Elom Gomez",
84+
AuditableDisplayName: "deploy request #102",
85+
RemoteIP: "45.19.24.124",
86+
CreatedAt: time.Date(2021, time.July, 19, 17, 13, 45, 000, time.UTC),
87+
UpdatedAt: time.Date(2021, time.July, 19, 17, 13, 45, 000, time.UTC),
88+
},
89+
}
90+
91+
c.Assert(err, qt.IsNil)
92+
c.Assert(auditLogs, qt.DeepEquals, want)
93+
}

planetscale/client.go

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type Client struct {
3838
// base URL for the API
3939
baseURL *url.URL
4040

41+
AuditLogs AuditLogsService
4142
Backups BackupsService
4243
Databases DatabasesService
4344
Certificates CertificatesService
@@ -131,6 +132,7 @@ func NewClient(opts ...ClientOption) (*Client, error) {
131132
}
132133
}
133134

135+
c.AuditLogs = &auditlogsService{client: c}
134136
c.Backups = &backupsService{client: c}
135137
c.Databases = &databasesService{client: c}
136138
c.Certificates = &certificatesService{client: c}

planetscale/integration_test.go

+39-5
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,7 @@ func TestIntegration_Databases_List(t *testing.T) {
7474

7575
_, err = client.Databases.Create(ctx, &CreateDatabaseRequest{
7676
Organization: org,
77-
Database: &Database{
78-
Name: dbName,
79-
Notes: "This is a test DB created from the planetscale-go API library",
80-
},
77+
Name: dbName,
8178
})
8279
if err != nil {
8380
t.Fatalf("create database failed: %s", err)
@@ -101,11 +98,48 @@ func TestIntegration_Databases_List(t *testing.T) {
10198
fmt.Printf("Notes: %q\n", db.Notes)
10299
}
103100

104-
_, err = client.Databases.Delete(ctx, &DeleteDatabaseRequest{
101+
err = client.Databases.Delete(ctx, &DeleteDatabaseRequest{
105102
Organization: org,
106103
Database: dbName,
107104
})
108105
if err != nil {
109106
t.Fatalf("delete database failed: %s", err)
110107
}
111108
}
109+
110+
func TestIntegration_AuditLogs_List(t *testing.T) {
111+
token := os.Getenv("PLANETSCALE_TOKEN")
112+
if token == "" {
113+
t.Fatalf("PLANETSCALE_TOKEN is not set")
114+
}
115+
116+
org := os.Getenv("PLANETSCALE_ORG")
117+
if org == "" {
118+
t.Fatalf("PLANETSCALE_ORG is not set")
119+
}
120+
121+
ctx := context.Background()
122+
123+
client, err := NewClient(
124+
WithAccessToken(token),
125+
)
126+
if err != nil {
127+
t.Fatal(err)
128+
}
129+
130+
auditLogs, err := client.AuditLogs.List(ctx, &ListAuditLogsRequest{
131+
Organization: org,
132+
Events: []AuditLogEvent{
133+
AuditLogEventBranchDeleted,
134+
AuditLogEventOrganizationJoined,
135+
},
136+
})
137+
if err != nil {
138+
t.Fatalf("get audit logs failed: %s", err)
139+
}
140+
141+
for _, l := range auditLogs {
142+
fmt.Printf("l. = %+v\n", l.AuditAction)
143+
}
144+
fmt.Printf("len(auditLogs) = %+v\n", len(auditLogs))
145+
}

0 commit comments

Comments
 (0)