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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy