Skip to content

Commit

Permalink
[audit] define model for audit logs.
Browse files Browse the repository at this point in the history
  • Loading branch information
ymmt2005 committed Jun 24, 2018
1 parent 31b5e91 commit 1c1aa2b
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 5 deletions.
68 changes: 68 additions & 0 deletions audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package sabakan

import (
"context"
"time"
)

// AuditCategory is the type of audit categories.
type AuditCategory string

// Audit categories.
const (
AuditAssets = AuditCategory("assets")
AuditCrypts = AuditCategory("crypts")
AuditDHCP = AuditCategory("dhcp")
AuditIgnition = AuditCategory("ignition")
AuditImage = AuditCategory("image")
AuditIPAM = AuditCategory("ipam")
AuditIPXE = AuditCategory("ipxe")
AuditMachines = AuditCategory("machines")
)

// AuditLog represents an audit log entry.
type AuditLog struct {
Timestamp time.Time `json:"ts"`
Revision int64 `json:"rev,string"`
User string `json:"user"`
IP string `json:"ip"`
Host string `json:"host"`
Category AuditCategory `json:"category"`
Instance string `json:"instance"`
Action string `json:"action"`
Detail string `json:"detail"`
}

// AuditContextKey is the type of context keys for audit.
type AuditContextKey string

// Audit context keys. Values must be string.
const (
AuditKeyUser = AuditContextKey("user")
AuditKeyIP = AuditContextKey("ip")
AuditKeyHost = AuditContextKey("host")
)

// NewAuditLog creates an audit log entry and initializes it.
func NewAuditLog(ctx context.Context, ts time.Time, rev int64, cat AuditCategory,
instance, action, detail string) *AuditLog {

a := new(AuditLog)
a.Timestamp = ts.UTC()
a.Revision = rev
if v := ctx.Value(AuditKeyUser); v != nil {
a.User = v.(string)
}
if v := ctx.Value(AuditKeyIP); v != nil {
a.IP = v.(string)
}
if v := ctx.Value(AuditKeyHost); v != nil {
a.Host = v.(string)
}
a.Category = cat
a.Instance = instance
a.Action = action
a.Detail = detail

return a
}
6 changes: 6 additions & 0 deletions model.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ type IgnitionModel interface {
DeleteTemplate(ctx context.Context, role string, id string) error
}

// LogModel is an interface for audit logs.
type LogModel interface {
Dump(ctx context.Context, since, until time.Time, w io.Writer) error
}

// Runner is an interface to run the underlying threads.
//
// The caller must pass a channel as follows.
Expand All @@ -114,4 +119,5 @@ type Model struct {
Image ImageModel
Asset AssetModel
Ignition IgnitionModel
Log LogModel
}
2 changes: 2 additions & 0 deletions models/mock/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type driver struct {
ipam *sabakan.IPAMConfig
machines map[string]*sabakan.Machine
storage map[string][]byte
log *sabakan.AuditLog
}

// NewModel returns sabakan.Model
Expand All @@ -31,6 +32,7 @@ func NewModel() sabakan.Model {
Image: newImageDriver(),
Asset: newAssetDriver(),
Ignition: newIgnitionDriver(),
Log: logDriver{d},
}
}

Expand Down
2 changes: 2 additions & 0 deletions models/mock/ipam.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ func (d *driver) putIPAMConfig(ctx context.Context, config *sabakan.IPAMConfig)
}
copied := *config
d.ipam = &copied
d.log = sabakan.NewAuditLog(ctx, 1, sabakan.AuditIPAM, "config", "put", "test")

return nil
}

Expand Down
16 changes: 16 additions & 0 deletions models/mock/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package mock

import (
"context"
"encoding/json"
"io"
"time"
)

type logDriver struct {
*driver
}

func (d logDriver) Dump(ctx context.Context, since, until time.Time, w io.Writer) error {
return json.NewEncoder(w).Encode(d.driver.log)
}
39 changes: 39 additions & 0 deletions web/server.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
package web

import (
"context"
"net"
"net/http"
"net/url"
"os"
"strings"

"github.com/cybozu-go/sabakan"
)

const (
// HeaderSabactlUser is the HTTP header name to tell which user run sabactl.
HeaderSabactlUser = "X-Sabakan-User"
)

var (
hostnameAtStartup string
)

func init() {
hostnameAtStartup, _ = os.Hostname()
}

// Server is the sabakan server.
type Server struct {
Model sabakan.Model
Expand All @@ -27,6 +42,28 @@ func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
renderError(r.Context(), w, APIErrNotFound)
}

func auditContext(r *http.Request) context.Context {
ctx := r.Context()

u := r.Header.Get(HeaderSabactlUser)
if len(u) > 0 {
ctx = context.WithValue(ctx, sabakan.AuditKeyUser, u)
}

ip, _, _ := net.SplitHostPort(r.RemoteAddr)
if len(ip) > 0 {
ctx = context.WithValue(ctx, sabakan.AuditKeyIP, ip)
}

if len(hostnameAtStartup) > 0 {
ctx = context.WithValue(ctx, sabakan.AuditKeyHost, hostnameAtStartup)
} else {
ctx = context.WithValue(ctx, sabakan.AuditKeyHost, r.Host)
}

return ctx
}

func (s Server) handleAPIV1(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path[len("/api/v1/"):]

Expand All @@ -35,6 +72,8 @@ func (s Server) handleAPIV1(w http.ResponseWriter, r *http.Request) {
return
}

r = r.WithContext(auditContext(r))

switch {
case p == "assets" || strings.HasPrefix(p, "assets/"):
s.handleAssets(w, r)
Expand Down
91 changes: 86 additions & 5 deletions web/server_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package web

import (
"bytes"
"context"
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"

"github.com/cybozu-go/sabakan"
"github.com/cybozu-go/sabakan/models/mock"
)

Expand All @@ -26,11 +32,7 @@ func testHandleAPIV1(t *testing.T) {
}
}

func TestServeHTTP(t *testing.T) {
t.Run("APIV1", testHandleAPIV1)
}

func TestHandlePermission(t *testing.T) {
func testHandlePermission(t *testing.T) {
t.Parallel()

_, ipnet, err := net.ParseCIDR("127.0.0.1/32")
Expand Down Expand Up @@ -75,3 +77,82 @@ func TestHandlePermission(t *testing.T) {
}
}
}

func testAuditContext(t *testing.T) {
t.Parallel()

m := mock.NewModel()
handler := newTestServer(m)

good := `
{
"max-nodes-in-rack": 28,
"node-ipv4-pool": "10.69.0.0/20",
"node-ipv4-range-size": 6,
"node-ipv4-range-mask": 26,
"node-index-offset": 3,
"node-ip-per-node": 3,
"bmc-ipv4-pool": "10.72.16.0/20",
"bmc-ipv4-range-size": 5,
"bmc-ipv4-range-mask": 20
}
`

w := httptest.NewRecorder()
r := httptest.NewRequest("PUT", "/api/v1/config/ipam", strings.NewReader(good))
handler.ServeHTTP(w, r)

resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Fatal("request failed with " + http.StatusText(resp.StatusCode))
}

buf := new(bytes.Buffer)
err := m.Log.Dump(context.Background(), time.Time{}, time.Time{}, buf)
if err != nil {
t.Fatal(err)
}

a := new(sabakan.AuditLog)
err = json.Unmarshal(buf.Bytes(), a)
if err != nil {
t.Fatal(err)
}

if a.IP != "192.0.2.1" {
t.Error(`a.IP != "192.0.2.1"`, a.IP)
}
if a.Category != sabakan.AuditIPAM {
t.Error(`a.Category != sabakan.AuditIPAM`, a.Category)
}
if a.User != "" {
t.Error(`a.User != ""`, a.User)
}

w = httptest.NewRecorder()
r = httptest.NewRequest("PUT", "/api/v1/config/ipam", strings.NewReader(good))
r.Header.Set(HeaderSabactlUser, "cybozu")
handler.ServeHTTP(w, r)

buf.Reset()
err = m.Log.Dump(context.Background(), time.Time{}, time.Time{}, buf)
if err != nil {
t.Fatal(err)
}

a = new(sabakan.AuditLog)
err = json.Unmarshal(buf.Bytes(), a)
if err != nil {
t.Fatal(err)
}

if a.User != "cybozu" {
t.Error(`a.User != "cybozu"`, a.User)
}
}

func TestServeHTTP(t *testing.T) {
t.Run("APIV1", testHandleAPIV1)
t.Run("Permission", testHandlePermission)
t.Run("AuditContext", testAuditContext)
}

0 comments on commit 1c1aa2b

Please sign in to comment.