Skip to content
This repository was archived by the owner on Jan 28, 2021. It is now read-only.

Commit 5749664

Browse files
authored
Merge pull request #536 from jfontan/feature/audit-logs
auth: add Audit to log user interactions
2 parents 40216a6 + f554be6 commit 5749664

File tree

15 files changed

+521
-83
lines changed

15 files changed

+521
-83
lines changed

auth/audit.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package auth
2+
3+
import (
4+
"net"
5+
"time"
6+
7+
"gopkg.in/src-d/go-mysql-server.v0/sql"
8+
"gopkg.in/src-d/go-vitess.v1/mysql"
9+
10+
"github.com/sirupsen/logrus"
11+
)
12+
13+
// AuditMethod is called to log the audit trail of actions.
14+
type AuditMethod interface {
15+
// Authentication logs an authentication event.
16+
Authentication(user, address string, err error)
17+
// Authorization logs an authorization event.
18+
Authorization(ctx *sql.Context, p Permission, err error)
19+
// Query logs a query execution.
20+
Query(ctx *sql.Context, d time.Duration, err error)
21+
}
22+
23+
// MysqlAudit wraps mysql.AuthServer to emit audit trails.
24+
type MysqlAudit struct {
25+
mysql.AuthServer
26+
audit AuditMethod
27+
}
28+
29+
// ValidateHash sends authentication calls to an AuditMethod.
30+
func (m *MysqlAudit) ValidateHash(
31+
salt []byte,
32+
user string,
33+
resp []byte,
34+
addr net.Addr,
35+
) (mysql.Getter, error) {
36+
getter, err := m.AuthServer.ValidateHash(salt, user, resp, addr)
37+
m.audit.Authentication(user, addr.String(), err)
38+
39+
return getter, err
40+
}
41+
42+
// NewAudit creates a wrapped Auth that sends audit trails to the specified
43+
// method.
44+
func NewAudit(auth Auth, method AuditMethod) Auth {
45+
return &Audit{
46+
auth: auth,
47+
method: method,
48+
}
49+
}
50+
51+
// Audit is an Auth method proxy that sends audit trails to the specified
52+
// AuditMethod.
53+
type Audit struct {
54+
auth Auth
55+
method AuditMethod
56+
}
57+
58+
// Mysql implements Auth interface.
59+
func (a *Audit) Mysql() mysql.AuthServer {
60+
return &MysqlAudit{
61+
AuthServer: a.auth.Mysql(),
62+
audit: a.method,
63+
}
64+
}
65+
66+
// Allowed implements Auth interface.
67+
func (a *Audit) Allowed(ctx *sql.Context, permission Permission) error {
68+
err := a.auth.Allowed(ctx, permission)
69+
a.method.Authorization(ctx, permission, err)
70+
71+
return err
72+
}
73+
74+
// Query implements AuditQuery interface.
75+
func (a *Audit) Query(ctx *sql.Context, d time.Duration, err error) {
76+
if q, ok := a.auth.(*Audit); ok {
77+
q.Query(ctx, d, err)
78+
}
79+
80+
a.method.Query(ctx, d, err)
81+
}
82+
83+
// NewAuditLog creates a new AuditMethod that logs to a logrus.Logger.
84+
func NewAuditLog(l *logrus.Logger) AuditMethod {
85+
la := l.WithField("system", "audit")
86+
87+
return &AuditLog{
88+
log: la,
89+
}
90+
}
91+
92+
const auditLogMessage = "audit trail"
93+
94+
// AuditLog logs audit trails to a logrus.Logger.
95+
type AuditLog struct {
96+
log *logrus.Entry
97+
}
98+
99+
// Authentication implements AuditMethod interface.
100+
func (a *AuditLog) Authentication(user string, address string, err error) {
101+
fields := logrus.Fields{
102+
"action": "authentication",
103+
"user": user,
104+
"address": address,
105+
"success": true,
106+
}
107+
108+
if err != nil {
109+
fields["success"] = false
110+
fields["err"] = err
111+
}
112+
113+
a.log.WithFields(fields).Info(auditLogMessage)
114+
}
115+
116+
func auditInfo(ctx *sql.Context, err error) logrus.Fields {
117+
fields := logrus.Fields{
118+
"user": ctx.Client().User,
119+
"query": ctx.Query(),
120+
"address": ctx.Client().Address,
121+
"connection_id": ctx.Session.ID(),
122+
"pid": ctx.Pid(),
123+
"success": true,
124+
}
125+
126+
if err != nil {
127+
fields["success"] = false
128+
fields["err"] = err
129+
}
130+
131+
return fields
132+
}
133+
134+
// Authorization implements AuditMethod interface.
135+
func (a *AuditLog) Authorization(ctx *sql.Context, p Permission, err error) {
136+
fields := auditInfo(ctx, err)
137+
fields["action"] = "authorization"
138+
fields["permission"] = p.String()
139+
140+
a.log.WithFields(fields).Info(auditLogMessage)
141+
}
142+
143+
func (a *AuditLog) Query(ctx *sql.Context, d time.Duration, err error) {
144+
fields := auditInfo(ctx, err)
145+
fields["action"] = "query"
146+
fields["duration"] = d
147+
148+
a.log.WithFields(fields).Info(auditLogMessage)
149+
}

auth/audit_test.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package auth_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"gopkg.in/src-d/go-mysql-server.v0/auth"
9+
"gopkg.in/src-d/go-mysql-server.v0/sql"
10+
11+
"github.com/sanity-io/litter"
12+
"github.com/sirupsen/logrus"
13+
"github.com/sirupsen/logrus/hooks/test"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
type Authentication struct {
18+
user string
19+
address string
20+
err error
21+
}
22+
23+
type Authorization struct {
24+
ctx *sql.Context
25+
p auth.Permission
26+
err error
27+
}
28+
29+
type Query struct {
30+
ctx *sql.Context
31+
d time.Duration
32+
err error
33+
}
34+
35+
type auditTest struct {
36+
authentication Authentication
37+
authorization Authorization
38+
query Query
39+
}
40+
41+
func (a *auditTest) Authentication(user string, address string, err error) {
42+
a.authentication = Authentication{
43+
user: user,
44+
address: address,
45+
err: err,
46+
}
47+
}
48+
49+
func (a *auditTest) Authorization(ctx *sql.Context, p auth.Permission, err error) {
50+
a.authorization = Authorization{
51+
ctx: ctx,
52+
p: p,
53+
err: err,
54+
}
55+
}
56+
57+
func (a *auditTest) Query(ctx *sql.Context, d time.Duration, err error) {
58+
println("query!")
59+
a.query = Query{
60+
ctx: ctx,
61+
d: d,
62+
err: err,
63+
}
64+
}
65+
66+
func (a *auditTest) Clean() {
67+
a.authorization = Authorization{}
68+
a.authentication = Authentication{}
69+
a.query = Query{}
70+
}
71+
72+
func TestAuditAuthentication(t *testing.T) {
73+
a := auth.NewNativeSingle("user", "password", auth.AllPermissions)
74+
at := new(auditTest)
75+
audit := auth.NewAudit(a, at)
76+
77+
extra := func(t *testing.T, c authenticationTest) {
78+
a := at.authentication
79+
80+
require.Equal(t, c.user, a.user)
81+
require.NotEmpty(t, a.address)
82+
if c.success {
83+
require.NoError(t, a.err)
84+
} else {
85+
require.Error(t, a.err)
86+
require.Nil(t, at.authorization.ctx)
87+
require.Nil(t, at.query.ctx)
88+
}
89+
90+
at.Clean()
91+
}
92+
93+
testAuthentication(t, audit, nativeSingleTests, extra)
94+
}
95+
96+
func TestAuditAuthorization(t *testing.T) {
97+
a := auth.NewNativeSingle("user", "", auth.ReadPerm)
98+
at := new(auditTest)
99+
audit := auth.NewAudit(a, at)
100+
101+
tests := []authorizationTest{
102+
{"user", "invalid query", false},
103+
{"user", queries["select"], true},
104+
105+
{"user", queries["create_index"], false},
106+
{"user", queries["drop_index"], false},
107+
{"user", queries["insert"], false},
108+
{"user", queries["lock"], false},
109+
{"user", queries["unlock"], false},
110+
}
111+
112+
extra := func(t *testing.T, c authorizationTest) {
113+
a := at.authorization
114+
q := at.query
115+
116+
litter.Dump(q)
117+
require.NotNil(t, q.ctx)
118+
require.Equal(t, c.user, q.ctx.Client().User)
119+
require.NotEmpty(t, q.ctx.Client().Address)
120+
require.NotZero(t, q.d)
121+
require.Equal(t, c.user, at.authentication.user)
122+
123+
if c.success {
124+
require.Equal(t, c.user, a.ctx.Client().User)
125+
require.NotEmpty(t, a.ctx.Client().Address)
126+
require.NoError(t, a.err)
127+
require.NoError(t, q.err)
128+
} else {
129+
require.Error(t, q.err)
130+
131+
// if there's a syntax error authorization is not triggered
132+
if auth.ErrNotAuthorized.Is(q.err) {
133+
require.Equal(t, q.err, a.err)
134+
require.NotNil(t, a.ctx)
135+
require.Equal(t, c.user, a.ctx.Client().User)
136+
require.NotEmpty(t, a.ctx.Client().Address)
137+
} else {
138+
require.NoError(t, a.err)
139+
require.Nil(t, a.ctx)
140+
}
141+
}
142+
143+
at.Clean()
144+
}
145+
146+
testAudit(t, audit, tests, extra)
147+
}
148+
149+
func TestAuditLog(t *testing.T) {
150+
require := require.New(t)
151+
152+
logger, hook := test.NewNullLogger()
153+
l := auth.NewAuditLog(logger)
154+
155+
pid := uint64(303)
156+
id := uint32(42)
157+
158+
l.Authentication("user", "client", nil)
159+
e := hook.LastEntry()
160+
require.NotNil(e)
161+
require.Equal(logrus.InfoLevel, e.Level)
162+
m := logrus.Fields{
163+
"system": "audit",
164+
"action": "authentication",
165+
"user": "user",
166+
"address": "client",
167+
"success": true,
168+
}
169+
require.Equal(m, e.Data)
170+
171+
err := auth.ErrNoPermission.New(auth.ReadPerm)
172+
l.Authentication("user", "client", err)
173+
e = hook.LastEntry()
174+
m["success"] = false
175+
m["err"] = err
176+
require.Equal(m, e.Data)
177+
178+
s := sql.NewSession("server", "client", "user", id)
179+
ctx := sql.NewContext(context.TODO(),
180+
sql.WithSession(s),
181+
sql.WithPid(pid),
182+
sql.WithQuery("query"),
183+
)
184+
185+
l.Authorization(ctx, auth.ReadPerm, nil)
186+
e = hook.LastEntry()
187+
require.NotNil(e)
188+
require.Equal(logrus.InfoLevel, e.Level)
189+
m = logrus.Fields{
190+
"system": "audit",
191+
"action": "authorization",
192+
"permission": auth.ReadPerm.String(),
193+
"user": "user",
194+
"query": "query",
195+
"address": "client",
196+
"connection_id": id,
197+
"pid": pid,
198+
"success": true,
199+
}
200+
require.Equal(m, e.Data)
201+
202+
l.Authorization(ctx, auth.ReadPerm, err)
203+
e = hook.LastEntry()
204+
m["success"] = false
205+
m["err"] = err
206+
require.Equal(m, e.Data)
207+
208+
l.Query(ctx, 808*time.Second, nil)
209+
e = hook.LastEntry()
210+
require.NotNil(e)
211+
require.Equal(logrus.InfoLevel, e.Level)
212+
m = logrus.Fields{
213+
"system": "audit",
214+
"action": "query",
215+
"duration": 808 * time.Second,
216+
"user": "user",
217+
"query": "query",
218+
"address": "client",
219+
"connection_id": id,
220+
"pid": pid,
221+
"success": true,
222+
}
223+
require.Equal(m, e.Data)
224+
225+
l.Query(ctx, 808*time.Second, err)
226+
e = hook.LastEntry()
227+
m["success"] = false
228+
m["err"] = err
229+
require.Equal(m, e.Data)
230+
}

auth/auth.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"strings"
55

66
"gopkg.in/src-d/go-errors.v1"
7+
"gopkg.in/src-d/go-mysql-server.v0/sql"
78
"gopkg.in/src-d/go-vitess.v1/mysql"
89
)
910

@@ -57,5 +58,5 @@ type Auth interface {
5758
// Allowed checks user's permissions with needed permission. If the user
5859
// does not have enough permissions it returns ErrNotAuthorized.
5960
// Otherwise is an error using the authentication method.
60-
Allowed(user string, permission Permission) error
61+
Allowed(ctx *sql.Context, permission Permission) error
6162
}

0 commit comments

Comments
 (0)