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

auth: add Audit to log user interactions #536

Merged
merged 5 commits into from
Nov 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions auth/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package auth

import (
"net"
"time"

"gopkg.in/src-d/go-mysql-server.v0/sql"
"gopkg.in/src-d/go-vitess.v1/mysql"

"github.com/sirupsen/logrus"
)

// AuditMethod is called to log the audit trail of actions.
type AuditMethod interface {
// Authentication logs an authentication event.
Authentication(user, address string, err error)
// Authorization logs an authorization event.
Authorization(ctx *sql.Context, p Permission, err error)
// Query logs a query execution.
Query(ctx *sql.Context, d time.Duration, err error)
}

// MysqlAudit wraps mysql.AuthServer to emit audit trails.
type MysqlAudit struct {
mysql.AuthServer
audit AuditMethod
}

// ValidateHash sends authentication calls to an AuditMethod.
func (m *MysqlAudit) ValidateHash(
salt []byte,
user string,
resp []byte,
addr net.Addr,
) (mysql.Getter, error) {
getter, err := m.AuthServer.ValidateHash(salt, user, resp, addr)
m.audit.Authentication(user, addr.String(), err)

return getter, err
}

// NewAudit creates a wrapped Auth that sends audit trails to the specified
// method.
func NewAudit(auth Auth, method AuditMethod) Auth {
return &Audit{
auth: auth,
method: method,
}
}

// Audit is an Auth method proxy that sends audit trails to the specified
// AuditMethod.
type Audit struct {
auth Auth
method AuditMethod
}

// Mysql implements Auth interface.
func (a *Audit) Mysql() mysql.AuthServer {
return &MysqlAudit{
AuthServer: a.auth.Mysql(),
audit: a.method,
}
}

// Allowed implements Auth interface.
func (a *Audit) Allowed(ctx *sql.Context, permission Permission) error {
err := a.auth.Allowed(ctx, permission)
a.method.Authorization(ctx, permission, err)

return err
}

// Query implements AuditQuery interface.
func (a *Audit) Query(ctx *sql.Context, d time.Duration, err error) {
if q, ok := a.auth.(*Audit); ok {
q.Query(ctx, d, err)
}

a.method.Query(ctx, d, err)
}

// NewAuditLog creates a new AuditMethod that logs to a logrus.Logger.
func NewAuditLog(l *logrus.Logger) AuditMethod {
la := l.WithField("system", "audit")

return &AuditLog{
log: la,
}
}

const auditLogMessage = "audit trail"

// AuditLog logs audit trails to a logrus.Logger.
type AuditLog struct {
log *logrus.Entry
}

// Authentication implements AuditMethod interface.
func (a *AuditLog) Authentication(user string, address string, err error) {
fields := logrus.Fields{
"action": "authentication",
"user": user,
"address": address,
"success": true,
}

if err != nil {
fields["success"] = false
fields["err"] = err
}

a.log.WithFields(fields).Info(auditLogMessage)
}

func auditInfo(ctx *sql.Context, err error) logrus.Fields {
fields := logrus.Fields{
"user": ctx.Client().User,
"query": ctx.Query(),
"address": ctx.Client().Address,
"connection_id": ctx.Session.ID(),
"pid": ctx.Pid(),
"success": true,
}

if err != nil {
fields["success"] = false
fields["err"] = err
}

return fields
}

// Authorization implements AuditMethod interface.
func (a *AuditLog) Authorization(ctx *sql.Context, p Permission, err error) {
fields := auditInfo(ctx, err)
fields["action"] = "authorization"
fields["permission"] = p.String()

a.log.WithFields(fields).Info(auditLogMessage)
}

func (a *AuditLog) Query(ctx *sql.Context, d time.Duration, err error) {
fields := auditInfo(ctx, err)
fields["action"] = "query"
fields["duration"] = d

a.log.WithFields(fields).Info(auditLogMessage)
}
230 changes: 230 additions & 0 deletions auth/audit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package auth_test

import (
"context"
"testing"
"time"

"gopkg.in/src-d/go-mysql-server.v0/auth"
"gopkg.in/src-d/go-mysql-server.v0/sql"

"github.com/sanity-io/litter"
"github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/require"
)

type Authentication struct {
user string
address string
err error
}

type Authorization struct {
ctx *sql.Context
p auth.Permission
err error
}

type Query struct {
ctx *sql.Context
d time.Duration
err error
}

type auditTest struct {
authentication Authentication
authorization Authorization
query Query
}

func (a *auditTest) Authentication(user string, address string, err error) {
a.authentication = Authentication{
user: user,
address: address,
err: err,
}
}

func (a *auditTest) Authorization(ctx *sql.Context, p auth.Permission, err error) {
a.authorization = Authorization{
ctx: ctx,
p: p,
err: err,
}
}

func (a *auditTest) Query(ctx *sql.Context, d time.Duration, err error) {
println("query!")
a.query = Query{
ctx: ctx,
d: d,
err: err,
}
}

func (a *auditTest) Clean() {
a.authorization = Authorization{}
a.authentication = Authentication{}
a.query = Query{}
}

func TestAuditAuthentication(t *testing.T) {
a := auth.NewNativeSingle("user", "password", auth.AllPermissions)
at := new(auditTest)
audit := auth.NewAudit(a, at)

extra := func(t *testing.T, c authenticationTest) {
a := at.authentication

require.Equal(t, c.user, a.user)
require.NotEmpty(t, a.address)
if c.success {
require.NoError(t, a.err)
} else {
require.Error(t, a.err)
require.Nil(t, at.authorization.ctx)
require.Nil(t, at.query.ctx)
}

at.Clean()
}

testAuthentication(t, audit, nativeSingleTests, extra)
}

func TestAuditAuthorization(t *testing.T) {
a := auth.NewNativeSingle("user", "", auth.ReadPerm)
at := new(auditTest)
audit := auth.NewAudit(a, at)

tests := []authorizationTest{
{"user", "invalid query", false},
{"user", queries["select"], true},

{"user", queries["create_index"], false},
{"user", queries["drop_index"], false},
{"user", queries["insert"], false},
{"user", queries["lock"], false},
{"user", queries["unlock"], false},
}

extra := func(t *testing.T, c authorizationTest) {
a := at.authorization
q := at.query

litter.Dump(q)
require.NotNil(t, q.ctx)
require.Equal(t, c.user, q.ctx.Client().User)
require.NotEmpty(t, q.ctx.Client().Address)
require.NotZero(t, q.d)
require.Equal(t, c.user, at.authentication.user)

if c.success {
require.Equal(t, c.user, a.ctx.Client().User)
require.NotEmpty(t, a.ctx.Client().Address)
require.NoError(t, a.err)
require.NoError(t, q.err)
} else {
require.Error(t, q.err)

// if there's a syntax error authorization is not triggered
if auth.ErrNotAuthorized.Is(q.err) {
require.Equal(t, q.err, a.err)
require.NotNil(t, a.ctx)
require.Equal(t, c.user, a.ctx.Client().User)
require.NotEmpty(t, a.ctx.Client().Address)
} else {
require.NoError(t, a.err)
require.Nil(t, a.ctx)
}
}

at.Clean()
}

testAudit(t, audit, tests, extra)
}

func TestAuditLog(t *testing.T) {
require := require.New(t)

logger, hook := test.NewNullLogger()
l := auth.NewAuditLog(logger)

pid := uint64(303)
id := uint32(42)

l.Authentication("user", "client", nil)
e := hook.LastEntry()
require.NotNil(e)
require.Equal(logrus.InfoLevel, e.Level)
m := logrus.Fields{
"system": "audit",
"action": "authentication",
"user": "user",
"address": "client",
"success": true,
}
require.Equal(m, e.Data)

err := auth.ErrNoPermission.New(auth.ReadPerm)
l.Authentication("user", "client", err)
e = hook.LastEntry()
m["success"] = false
m["err"] = err
require.Equal(m, e.Data)

s := sql.NewSession("server", "client", "user", id)
ctx := sql.NewContext(context.TODO(),
sql.WithSession(s),
sql.WithPid(pid),
sql.WithQuery("query"),
)

l.Authorization(ctx, auth.ReadPerm, nil)
e = hook.LastEntry()
require.NotNil(e)
require.Equal(logrus.InfoLevel, e.Level)
m = logrus.Fields{
"system": "audit",
"action": "authorization",
"permission": auth.ReadPerm.String(),
"user": "user",
"query": "query",
"address": "client",
"connection_id": id,
"pid": pid,
"success": true,
}
require.Equal(m, e.Data)

l.Authorization(ctx, auth.ReadPerm, err)
e = hook.LastEntry()
m["success"] = false
m["err"] = err
require.Equal(m, e.Data)

l.Query(ctx, 808*time.Second, nil)
e = hook.LastEntry()
require.NotNil(e)
require.Equal(logrus.InfoLevel, e.Level)
m = logrus.Fields{
"system": "audit",
"action": "query",
"duration": 808 * time.Second,
"user": "user",
"query": "query",
"address": "client",
"connection_id": id,
"pid": pid,
"success": true,
}
require.Equal(m, e.Data)

l.Query(ctx, 808*time.Second, err)
e = hook.LastEntry()
m["success"] = false
m["err"] = err
require.Equal(m, e.Data)
}
3 changes: 2 additions & 1 deletion auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strings"

"gopkg.in/src-d/go-errors.v1"
"gopkg.in/src-d/go-mysql-server.v0/sql"
"gopkg.in/src-d/go-vitess.v1/mysql"
)

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