diff --git a/Makefile b/Makefile index 5eebfb068ce..daa5b434641 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,8 @@ run-simple: install -authBackend=bolt\ -authBackendConfig='{"path": "/tmp/teleport.auth.db"}'\ -authDomain=gravitational.io\ + -authEventBackend=bolt\ + -authEventBackendConfig='{"path": "/tmp/teleport.event.db"}'\ -log=console\ -logSeverity=INFO\ -dataDir=/var/lib/teleport\ @@ -118,7 +120,7 @@ run-simple: install -authServer=tcp://auth.gravitational.io:33000\ -cp\ -cpAssetsDir=$(GOPATH)/src/github.com/gravitational/teleport\ - -cpDomain=gravitational.io\ + -cpDomain=gravitational.io run-ssh2: install tctl token generate --output=/tmp/token -fqdn=node1.gravitational.io diff --git a/assets/static/js/grv/events.js b/assets/static/js/grv/events.js deleted file mode 100644 index 000cd0ee487..00000000000 --- a/assets/static/js/grv/events.js +++ /dev/null @@ -1,170 +0,0 @@ -'use strict'; - -var EventsPage = React.createClass({ - showEvent: function(e){ - if(e.schema != "teleport.shell") { - return; - } - this.refs.event.show(atob(e.properties.log)); - }, - getInitialState: function(){ - return {entries: []}; - }, - componentDidMount: function() { - this.reload(); - setInterval(this.reload, this.props.pollInterval); - }, - reload: function(){ - $.ajax({ - url: this.props.url, - dataType: 'json', - success: function(data) { - this.setState({entries: data}); - }.bind(this), - error: function(xhr, status, err) { - console.error(this.props.url, status, err.toString()); - }.bind(this) - }); - }, - render: function() { - return ( -
- -
- - -
- - - -
- -
- -
- ); - } -}); - - -var EventsBox = React.createClass({ - render: function() { - if (this.props.entries.length == 0) { - return ( -
-

Logged Events

- There are no events logged. Log in via SSH to watch the events.

-
); - } - return ( -
- -
- ); - } -}); - - -var EventsTable = React.createClass({ - render: function() { - var show = this.props.onShowEvent - var keyNodes = this.props.entries.map(function (event, index) { - return ( - - ); - }); - return ( - - - - - - - - - - - - - - {keyNodes} - -
TimeEventUserClient IPRemote IPData
- ); - } -}); - - -var EventRow = React.createClass({ - showEvent: function(e) { - e.preventDefault(); - this.props.onShowEvent(this.props.event); - }, - describe: function(event) { - switch (event.schema) { - case "teleport.auth.attempt": - if (event.properties.success == "true") { - return {icon: "fa fa-user text-navy", text: "successfull auth"}; - } - return {icon: "fa fa-user text-warning", text: "unsucessfull auth: " + event.properties.error}; - case "teleport.shell": - return {icon: "fa fa-tty text-navy", text: "view session log"}; - case "teleport.exec": - return {icon: "fa fa-tty text-navy", text: event.properties.command + " " + atob(event.properties.log).substring(0, 100)}; - } - return {icon: "fa fa-question text-navy", text: "unrecognized event: "+event.schema}; - }, - render: function() { - var e = this.props.event; - var d = this.describe(e); - return ( - - - {e.time} - {e.schema} - {e.properties.user} - {e.properties.localaddr} - {e.properties.remoteaddr} - {d.text} - - ); - } -}); - -var EventForm = React.createClass({ - show: function(log) { - this.term = new Terminal({ - cols: 120, - rows: 24, - useStyle: true, - screenKeys: true, - cursorBlink: false - }); - this.term.open(React.findDOMNode(this.refs.term)); - this.term.write(log); - this.refs.modal.open(); - }, - close: function() { - this.term.destroy(); - this.refs.modal.close(); - }, - render: function() { - return ( - -
-
- ); - } -}); - - -React.render( - , - document.body -); diff --git a/assets/static/js/grv/events.jsx b/assets/static/js/grv/events.jsx new file mode 100644 index 00000000000..69023580a8e --- /dev/null +++ b/assets/static/js/grv/events.jsx @@ -0,0 +1,183 @@ +'use strict'; + +var EventsPage = React.createClass({ + showEvent: function(e){ + if(e.schema != "teleport.shell") { + return; + } + this.refs.event.show(atob(e.properties.log)); + }, + getInitialState: function(){ + return {entries: []}; + }, + componentDidMount: function() { + this.reload(); + setInterval(this.reload, this.props.pollInterval); + }, + reload: function(){ + var q = [ + {name: 'start', value: toRFC3339(new Date())}, + {name: 'limit', value: '20'}, + {name: 'order', value: '-1'}, + ]; + $.ajax({ + url: this.props.url+ "?"+ $.param(q), + dataType: 'json', + success: function(data) { + this.setState({entries: data}); + }.bind(this), + error: function(xhr, status, err) { + console.error(this.props.url, status, err.toString()); + }.bind(this) + }); + }, + render: function() { + return ( +
+ +
+ + +
+ + + +
+ +
+ +
); + } +}); + + +var EventsBox = React.createClass({ + render: function() { + if (this.props.entries.length == 0) { + return ( +
+

Logged Events

+ There are no events logged. Log in via SSH to watch the events.

+
); + } + return ( +
+ +
+ ); + } +}); + + +var EventsTable = React.createClass({ + render: function() { + var show = this.props.onShowEvent + var keyNodes = this.props.entries.map(function (event, index) { + return ( + + ); + }); + return ( + + + + + + + + + + + + + + {keyNodes} + +
TimeEventUserClient IPRemote IPData
+ ); + } +}); + + +var EventRow = React.createClass({ + showEvent: function(e) { + e.preventDefault(); + this.props.onShowEvent(this.props.event); + }, + describe: function(event) { + switch (event.schema) { + case "teleport.auth.attempt": + if (event.properties.success == "true") { + return {icon: "fa fa-user text-navy", text: "successfull auth"}; + } + return {icon: "fa fa-user text-warning", text: "unsucessfull auth: " + event.properties.error}; + case "teleport.shell": + return {icon: "fa fa-tty text-navy", text: "view session log"}; + case "teleport.exec": + return {icon: "fa fa-tty text-navy", text: event.properties.command + " " + atob(event.properties.log).substring(0, 100)}; + } + return {icon: "fa fa-question text-navy", text: "unrecognized event: "+event.schema}; + }, + render: function() { + var e = this.props.event; + var d = this.describe(e); + return ( + + + {e.time} + {e.schema} + {e.properties.user} + {e.properties.localaddr} + {e.properties.remoteaddr} + {d.text} + + ); + } +}); + +var EventForm = React.createClass({ + show: function(log) { + this.term = new Terminal({ + cols: 120, + rows: 24, + useStyle: true, + screenKeys: true, + cursorBlink: false + }); + this.term.open(React.findDOMNode(this.refs.term)); + var self = this; + var i = 0; + var write = function(){ + if(i >= log.length) { + return + } + self.term.write(log[i]); + i++; + setTimeout(write, 40); + } + this.refs.modal.open(); + write(); + }, + close: function() { + this.term.destroy(); + this.refs.modal.close(); + }, + render: function() { + return ( + +
+
+ ); + } +}); + + +React.render( + , + document.body +); diff --git a/assets/static/js/grv/lib.js b/assets/static/js/grv/lib.js index 14dad3fdf1c..521aacfaa12 100644 --- a/assets/static/js/grv/lib.js +++ b/assets/static/js/grv/lib.js @@ -35,3 +35,19 @@ function formatBytes(bytes,decimals) { var i = Math.floor(Math.log(bytes) / Math.log(k)); return (bytes / Math.pow(k, i)).toPrecision(dm) + ' ' + sizes[i]; } + + +function toRFC3339(dt) { + return (dt.getUTCFullYear() + "-" + zeroPad(dt.getUTCMonth() + 1, 2) + "-" + zeroPad(dt.getUTCDate(),2) + "T" + + zeroPad(dt.getUTCHours(),2) + ":" + zeroPad(dt.getUTCMinutes(),2) + ":" + zeroPad(dt.getUTCSeconds(),2) + "." + dt.getUTCMilliseconds() + "Z"); +} + +function zeroPad(num, numZeros) { + var n = Math.abs(num); + var zeros = Math.max(0, numZeros - Math.floor(n).toString().length ); + var zeroString = Math.pow(10,zeros).toString().substr(1); + if( num < 0 ) { + zeroString = '-' + zeroString; + } + return zeroString+n; +} diff --git a/assets/static/tpl/events.tpl b/assets/static/tpl/events.tpl index 7d6a195235d..1cb69c4ad1c 100644 --- a/assets/static/tpl/events.tpl +++ b/assets/static/tpl/events.tpl @@ -10,5 +10,5 @@ {{ define "script" }} - + {{ end }} diff --git a/auth/api.go b/auth/api.go index 08d39f4fba3..242abcb7760 100644 --- a/auth/api.go +++ b/auth/api.go @@ -4,20 +4,21 @@ import ( "net" "net/http" - "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/memlog" - "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/mailgun/log" - "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/mailgun/oxy/trace" + "github.com/gravitational/teleport/events" "github.com/gravitational/teleport/session" "github.com/gravitational/teleport/utils" + + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/mailgun/log" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/mailgun/oxy/trace" ) -func StartHTTPServer(a string, srv *AuthServer, se session.SessionServer) error { +func StartHTTPServer(a string, srv *AuthServer, elog events.Log, se session.SessionServer) error { addr, err := utils.ParseAddr(a) if err != nil { return err } t, err := trace.New( - NewAPIServer(srv, memlog.New(), se), + NewAPIServer(srv, elog, se), log.GetLogger().Writer(log.SeverityInfo)) if err != nil { return err diff --git a/auth/clt.go b/auth/clt.go index 968dd0e39b2..0dd31f1083c 100644 --- a/auth/clt.go +++ b/auth/clt.go @@ -10,9 +10,11 @@ import ( "time" "github.com/gravitational/teleport/backend" + "github.com/gravitational/teleport/events" "github.com/gravitational/teleport/session" "github.com/gravitational/teleport/utils" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/codahale/lunk" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/roundtrip" ) @@ -211,24 +213,32 @@ func (c *Client) GenerateToken(fqdn string, ttl time.Duration) (string, error) { return re.Token, nil } -// Submit events submits structured audit events in JSON serialized -// format to the auth server. -func (c *Client) SubmitEvents(events [][]byte) error { - files := make([]roundtrip.File, len(events)) - for i, e := range events { - files[i] = roundtrip.File{ - Name: "event", - Filename: "event.json", - Reader: bytes.NewReader(e), - } +func (c *Client) Log(id lunk.EventID, e lunk.Event) { + en := lunk.NewEntry(id, e) + en.Time = time.Now() + c.LogEntry(en) +} + +func (c *Client) LogEntry(en lunk.Entry) error { + bt, err := json.Marshal(en) + if err != nil { + return err } - _, err := c.PostForm(c.Endpoint("events"), url.Values{}, files...) + file := roundtrip.File{ + Name: "event", + Filename: "event.json", + Reader: bytes.NewReader(bt), + } + _, err = c.PostForm(c.Endpoint("events"), url.Values{}, file) return err } -// GetEvents returns last 20 audit events recorded by the auth server -func (c *Client) GetEvents() ([]interface{}, error) { - out, err := c.Get(c.Endpoint("events"), url.Values{}) +func (c *Client) GetEvents(filter events.Filter) ([]lunk.Entry, error) { + vals, err := events.FilterToURL(filter) + if err != nil { + return nil, err + } + out, err := c.Get(c.Endpoint("events"), vals) if err != nil { return nil, err } @@ -554,24 +564,3 @@ func (c *Client) ResetUserCA() error { _, err := c.PostForm(c.Endpoint("ca", "user", "keys"), url.Values{}) return err } - -// GetLogWriter returns a io.Writer - compatible object -// that can be used by lunk.EventLogger to ship audit logs to the auth server -func (c *Client) GetLogWriter() *LogWriter { - return &LogWriter{clt: c} -} - -type LogWriter struct { - clt *Client -} - -func (w *LogWriter) Write(data []byte) (int, error) { - if err := w.clt.SubmitEvents([][]byte{data}); err != nil { - return 0, err - } - return len(data), nil -} - -func (w *LogWriter) LastEvents() ([]interface{}, error) { - return w.clt.GetEvents() -} diff --git a/auth/srv.go b/auth/srv.go index 5ee7b672615..456da0c1603 100644 --- a/auth/srv.go +++ b/auth/srv.go @@ -7,14 +7,16 @@ import ( "net/http" "time" + "github.com/gravitational/teleport/backend" + "github.com/gravitational/teleport/events" + "github.com/gravitational/teleport/session" + + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/codahale/lunk" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/form" - "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/memlog" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/roundtrip" websession "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/session" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/julienschmidt/httprouter" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/mailgun/log" - "github.com/gravitational/teleport/backend" - "github.com/gravitational/teleport/session" ) type Config struct { @@ -26,11 +28,11 @@ type Config struct { type APIServer struct { httprouter.Router s *AuthServer - elog memlog.Logger + elog events.Log se session.SessionServer } -func NewAPIServer(s *AuthServer, elog memlog.Logger, se session.SessionServer) *APIServer { +func NewAPIServer(s *AuthServer, elog events.Log, se session.SessionServer) *APIServer { srv := &APIServer{ s: s, elog: elog, @@ -488,7 +490,14 @@ func (s *APIServer) submitEvents(w http.ResponseWriter, r *http.Request, _ httpr reply(w, http.StatusBadRequest, fmt.Errorf("failed to read event")) return } - if _, err := s.elog.Write(data); err != nil { + var e *lunk.Entry + if err := json.Unmarshal(data, &e); err != nil { + log.Errorf("failed to read event: %v", err) + reply(w, http.StatusBadRequest, fmt.Errorf("failed to read event")) + return + } + + if err := s.elog.LogEntry(*e); err != nil { log.Errorf("failed to write event: %v", err) reply(w, http.StatusInternalServerError, fmt.Errorf("failed to write event")) @@ -500,8 +509,19 @@ func (s *APIServer) submitEvents(w http.ResponseWriter, r *http.Request, _ httpr } func (s *APIServer) getEvents(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - roundtrip.ReplyJSON( - w, http.StatusOK, &eventsResponse{Events: s.elog.LastEvents()}) + f, err := events.FilterFromURL(r.URL.Query()) + if err != nil { + log.Errorf("failed to parse filter: %v", err) + reply(w, http.StatusBadRequest, fmt.Errorf("failed to parse filter")) + return + } + es, err := s.elog.GetEvents(*f) + if err != nil { + log.Errorf("failed to get events: %v", err) + reply(w, http.StatusInternalServerError, fmt.Errorf("failed to get events")) + return + } + roundtrip.ReplyJSON(w, http.StatusOK, &eventsResponse{Events: es}) } func replyErr(w http.ResponseWriter, e error) { @@ -699,7 +719,7 @@ type tokenResponse struct { } type eventsResponse struct { - Events []interface{} `json:"events"` + Events []lunk.Entry `json:"events"` } type partyResponse struct { diff --git a/auth/srv_test.go b/auth/srv_test.go index d269a9a87f2..148c04e1c45 100644 --- a/auth/srv_test.go +++ b/auth/srv_test.go @@ -6,14 +6,15 @@ import ( "testing" "time" - "github.com/gravitational/teleport/Godeps/_workspace/src/golang.org/x/crypto/ssh" authority "github.com/gravitational/teleport/auth/native" "github.com/gravitational/teleport/backend" "github.com/gravitational/teleport/backend/boltbk" + "github.com/gravitational/teleport/events/boltlog" + "github.com/gravitational/teleport/events/test" "github.com/gravitational/teleport/session" - "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/memlog" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/mailgun/lemma/secret" + "github.com/gravitational/teleport/Godeps/_workspace/src/golang.org/x/crypto/ssh" . "github.com/gravitational/teleport/Godeps/_workspace/src/gopkg.in/check.v1" ) @@ -23,6 +24,7 @@ type APISuite struct { srv *httptest.Server clt *Client bk *boltbk.BoltBackend + bl *boltlog.BoltLog scrt *secret.Service a *AuthServer dir string @@ -44,8 +46,11 @@ func (s *APISuite) SetUpTest(c *C) { s.bk, err = boltbk.New(filepath.Join(s.dir, "db")) c.Assert(err, IsNil) + s.bl, err = boltlog.New(filepath.Join(s.dir, "eventsdb")) + c.Assert(err, IsNil) + s.a = NewAuthServer(s.bk, authority.New(), s.scrt) - s.srv = httptest.NewServer(NewAPIServer(s.a, memlog.New(), session.New(s.bk))) + s.srv = httptest.NewServer(NewAPIServer(s.a, s.bl, session.New(s.bk))) clt, err := NewClient(s.srv.URL) c.Assert(err, IsNil) s.clt = clt @@ -54,6 +59,7 @@ func (s *APISuite) SetUpTest(c *C) { func (s *APISuite) TearDownTest(c *C) { s.srv.Close() s.bk.Close() + s.bl.Close() } func (s *APISuite) TestHostCACRUD(c *C) { @@ -254,19 +260,8 @@ func (s *APISuite) TestServers(c *C) { } func (s *APISuite) TestEvents(c *C) { - err := s.clt.SubmitEvents( - [][]byte{ - []byte(`{"e": "event 1"}`), - []byte(`{"e": "event 2"}`)}) - c.Assert(err, IsNil) - - out, err := s.clt.GetEvents() - c.Assert(err, IsNil) - expected := []interface{}{ - map[string]interface{}{"e": "event 2"}, - map[string]interface{}{"e": "event 1"}, - } - c.Assert(out, DeepEquals, expected) + suite := test.EventSuite{L: s.clt} + suite.EventsCRUD(c) } func (s *APISuite) TestTokens(c *C) { diff --git a/auth/tun_test.go b/auth/tun_test.go index 647a5856a98..3544dd5e1ae 100644 --- a/auth/tun_test.go +++ b/auth/tun_test.go @@ -11,11 +11,11 @@ import ( authority "github.com/gravitational/teleport/auth/native" "github.com/gravitational/teleport/backend" "github.com/gravitational/teleport/backend/membk" + "github.com/gravitational/teleport/events/boltlog" "github.com/gravitational/teleport/session" "github.com/gravitational/teleport/sshutils" "github.com/gravitational/teleport/utils" - "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/memlog" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/mailgun/lemma/secret" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/mailgun/log" "github.com/gravitational/teleport/Godeps/_workspace/src/golang.org/x/crypto/ssh" @@ -30,17 +30,24 @@ type TunSuite struct { tsrv *TunServer a *AuthServer signer ssh.Signer + bl *boltlog.BoltLog + dir string } var _ = Suite(&TunSuite{}) func (s *TunSuite) SetUpSuite(c *C) { + s.dir = c.MkDir() + key, err := secret.NewKey() c.Assert(err, IsNil) srv, err := secret.New(&secret.Config{KeyBytes: key}) c.Assert(err, IsNil) s.scrt = srv log.Init([]*log.LogConfig{&log.LogConfig{Name: "console"}}) + + s.bl, err = boltlog.New(filepath.Join(s.dir, "eventsdb")) + c.Assert(err, IsNil) } func (s *TunSuite) TearDownTest(c *C) { @@ -50,7 +57,7 @@ func (s *TunSuite) TearDownTest(c *C) { func (s *TunSuite) SetUpTest(c *C) { s.bk = membk.New() s.a = NewAuthServer(s.bk, authority.New(), s.scrt) - s.srv = httptest.NewServer(NewAPIServer(s.a, memlog.New(), session.New(s.bk))) + s.srv = httptest.NewServer(NewAPIServer(s.a, s.bl, session.New(s.bk))) // set up host private key and certificate c.Assert(s.a.ResetHostCA(""), IsNil) @@ -83,7 +90,7 @@ func (s *TunSuite) TestUnixServerClient(c *C) { l, err := net.Listen("unix", socketPath) c.Assert(err, IsNil) - h := NewAPIServer(s.a, memlog.New(), session.New(s.bk)) + h := NewAPIServer(s.a, s.bl, session.New(s.bk)) srv := &httptest.Server{ Listener: l, Config: &http.Server{ diff --git a/backend/boltbk/boltbk.go b/backend/boltbk/boltbk.go index bea54381a5f..5f8e74c70a0 100644 --- a/backend/boltbk/boltbk.go +++ b/backend/boltbk/boltbk.go @@ -135,7 +135,7 @@ func (b *BoltBackend) GetRemoteCerts(ctype string, fqdn string) ([]backend.Remot err := b.db.View(func(tx *bolt.Tx) error { hosts := []string{} if fqdn == "" { - bkt, err := getBucket(tx, []string{"certs", ctype, "hosts"}) + bkt, err := GetBucket(tx, []string{"certs", ctype, "hosts"}) if err != nil { if isNotFound(err) { return nil @@ -150,7 +150,7 @@ func (b *BoltBackend) GetRemoteCerts(ctype string, fqdn string) ([]backend.Remot hosts = []string{fqdn} } for _, h := range hosts { - bkt, err := getBucket(tx, []string{"certs", ctype, "hosts", h}) + bkt, err := GetBucket(tx, []string{"certs", ctype, "hosts", h}) if err != nil { return err } @@ -241,7 +241,7 @@ func (b *BoltBackend) GetUserKeys(user string) ([]backend.AuthorizedKey, error) } values := []backend.AuthorizedKey{} err := b.db.View(func(tx *bolt.Tx) error { - bkt, err := getBucket(tx, []string{"users", user, "keys"}) + bkt, err := GetBucket(tx, []string{"users", user, "keys"}) if err != nil { if _, ok := err.(*backend.NotFoundError); ok { return nil @@ -355,7 +355,7 @@ func (b *BoltBackend) GetWebSession(user, sid string) (*backend.WebSession, erro func (b *BoltBackend) GetWebSessionsKeys(user string) ([]backend.AuthorizedKey, error) { values := []backend.AuthorizedKey{} err := b.db.View(func(tx *bolt.Tx) error { - bkt, err := getBucket(tx, []string{"users", user, "web-sessions"}) + bkt, err := GetBucket(tx, []string{"users", user, "web-sessions"}) if err != nil { if _, ok := err.(*backend.NotFoundError); ok { return nil @@ -404,7 +404,7 @@ func (b *BoltBackend) DeleteWebTun(prefix string) error { func (b *BoltBackend) GetWebTuns() ([]backend.WebTun, error) { out := []backend.WebTun{} err := b.db.View(func(tx *bolt.Tx) error { - bkt, err := getBucket(tx, []string{"web-tuns"}) + bkt, err := GetBucket(tx, []string{"web-tuns"}) if err != nil { return err } @@ -426,7 +426,7 @@ func (b *BoltBackend) GetWebTuns() ([]backend.WebTun, error) { func (b *BoltBackend) deleteBucket(buckets []string, bucket string) error { return b.db.Update(func(tx *bolt.Tx) error { - bkt, err := getBucket(tx, buckets) + bkt, err := GetBucket(tx, buckets) if err != nil { return err } @@ -440,7 +440,7 @@ func (b *BoltBackend) deleteBucket(buckets []string, bucket string) error { func (b *BoltBackend) deleteKey(buckets []string, key string) error { return b.db.Update(func(tx *bolt.Tx) error { - bkt, err := getBucket(tx, buckets) + bkt, err := GetBucket(tx, buckets) if err != nil { return err } @@ -453,7 +453,7 @@ func (b *BoltBackend) deleteKey(buckets []string, key string) error { func (b *BoltBackend) upsertKey(buckets []string, key string, bytes []byte) error { return b.db.Update(func(tx *bolt.Tx) error { - bkt, err := upsertBucket(tx, buckets) + bkt, err := UpsertBucket(tx, buckets) if err != nil { return err } @@ -467,7 +467,7 @@ func (b *BoltBackend) upsertJSONKey(buckets []string, key string, val interface{ return err } return b.db.Update(func(tx *bolt.Tx) error { - bkt, err := upsertBucket(tx, buckets) + bkt, err := UpsertBucket(tx, buckets) if err != nil { return err } @@ -477,7 +477,7 @@ func (b *BoltBackend) upsertJSONKey(buckets []string, key string, val interface{ func (b *BoltBackend) getJSONKey(buckets []string, key string, val interface{}) error { return b.db.View(func(tx *bolt.Tx) error { - bkt, err := getBucket(tx, buckets) + bkt, err := GetBucket(tx, buckets) if err != nil { return err } @@ -493,7 +493,7 @@ func (b *BoltBackend) getJSONKey(buckets []string, key string, val interface{}) func (b *BoltBackend) getKey(buckets []string, key string, val *[]byte) error { return b.db.View(func(tx *bolt.Tx) error { - bkt, err := getBucket(tx, buckets) + bkt, err := GetBucket(tx, buckets) if err != nil { return err } @@ -511,7 +511,7 @@ func (b *BoltBackend) getKey(buckets []string, key string, val *[]byte) error { func (b *BoltBackend) getKeys(buckets []string) ([]string, error) { out := []string{} err := b.db.View(func(tx *bolt.Tx) error { - bkt, err := getBucket(tx, buckets) + bkt, err := GetBucket(tx, buckets) if err != nil { return err } @@ -527,7 +527,7 @@ func (b *BoltBackend) getKeys(buckets []string) ([]string, error) { return out, nil } -func upsertBucket(b *bolt.Tx, buckets []string) (*bolt.Bucket, error) { +func UpsertBucket(b *bolt.Tx, buckets []string) (*bolt.Bucket, error) { bkt, err := b.CreateBucketIfNotExists([]byte(buckets[0])) if err != nil { return nil, err @@ -541,7 +541,7 @@ func upsertBucket(b *bolt.Tx, buckets []string) (*bolt.Bucket, error) { return bkt, nil } -func getBucket(b *bolt.Tx, buckets []string) (*bolt.Bucket, error) { +func GetBucket(b *bolt.Tx, buckets []string) (*bolt.Bucket, error) { bkt := b.Bucket([]byte(buckets[0])) if bkt == nil { return nil, &backend.NotFoundError{ diff --git a/cp/cp.go b/cp/cp.go index 393601cd68e..6d6cc205858 100644 --- a/cp/cp.go +++ b/cp/cp.go @@ -16,6 +16,7 @@ import ( "code.google.com/p/go-uuid/uuid" "github.com/gravitational/teleport/auth" "github.com/gravitational/teleport/backend" + "github.com/gravitational/teleport/events" "github.com/gravitational/teleport/sshutils" "github.com/gravitational/teleport/sshutils/scp" "github.com/gravitational/teleport/utils" @@ -422,12 +423,19 @@ func (s *cpHandler) upsertWebTun(w http.ResponseWriter, r *http.Request, _ httpr } func (s *cpHandler) getEvents(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c *ctx) { - events, err := c.clt.GetEvents() + f, err := events.FilterFromURL(r.URL.Query()) if err != nil { - log.Errorf("failed to retrieve events: %v") + log.Errorf("failed to retrieve events: %v", err) replyErr(w, http.StatusInternalServerError, err) return } + events, err := c.clt.GetEvents(*f) + if err != nil { + log.Errorf("failed to retrieve events: %v", err) + replyErr(w, http.StatusInternalServerError, err) + return + } + log.Infof("got events: %v", events) roundtrip.ReplyJSON(w, http.StatusOK, events) } diff --git a/events/boltlog/bcfg.go b/events/boltlog/bcfg.go new file mode 100644 index 00000000000..4d84cd7df83 --- /dev/null +++ b/events/boltlog/bcfg.go @@ -0,0 +1,26 @@ +package boltlog + +import ( + "encoding/json" + "fmt" + + "github.com/gravitational/teleport/events" +) + +// cfg represents JSON config for bolt backlend +type cfg struct { + Path string `json:"path"` +} + +// FromString initialized the backend from backend-specific string +func FromString(v string) (events.Log, error) { + if len(v) == 0 { + return nil, fmt.Errorf( + `please supply a valid dictionary, e.g. {"path": "/opt/bolt.db"}`) + } + var c *cfg + if err := json.Unmarshal([]byte(v), &c); err != nil { + return nil, fmt.Errorf("invalid backend configuration format, err: %v", err) + } + return New(c.Path) +} diff --git a/events/boltlog/bl.go b/events/boltlog/bl.go new file mode 100644 index 00000000000..a4e7dfd6c4b --- /dev/null +++ b/events/boltlog/bl.go @@ -0,0 +1,153 @@ +package boltlog + +import ( + "bytes" + "encoding/json" + "fmt" + "time" + + "github.com/gravitational/teleport/backend" + "github.com/gravitational/teleport/backend/boltbk" + "github.com/gravitational/teleport/events" + + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/boltdb/bolt" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/codahale/lunk" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/mailgun/log" +) + +type BoltLog struct { + db *bolt.DB +} + +func New(path string) (*BoltLog, error) { + db, err := bolt.Open(path, 0600, nil) + if err != nil { + return nil, err + } + return &BoltLog{ + db: db, + }, nil +} + +func (b *BoltLog) Close() error { + return b.db.Close() +} + +func (b *BoltLog) Log(id lunk.EventID, e lunk.Event) { + en := lunk.NewEntry(id, e) + en.Time = time.Now() + b.LogEntry(en) +} + +func (b *BoltLog) LogEntry(en lunk.Entry) error { + sid := en.Properties[SessionID] + btm, err := en.Time.MarshalText() + if err != nil { + return err + } + + bytes, err := json.Marshal(en) + if err != nil { + return err + } + + return b.db.Update(func(tx *bolt.Tx) error { + if sid != "" { + bkt, err := boltbk.UpsertBucket(tx, []string{"sessions", sid}) + if err != nil { + return err + } + if err := bkt.Put([]byte(btm), bytes); err != nil { + return err + } + } + bkt, err := boltbk.UpsertBucket(tx, []string{"events"}) + if err != nil { + return err + } + return bkt.Put([]byte(btm), bytes) + }) +} + +func (b *BoltLog) GetEvents(f events.Filter) ([]lunk.Entry, error) { + log.Infof("Get events: %v", f) + if f.Start.IsZero() { + return nil, fmt.Errorf("supply either starting point") + } + if f.Limit == 0 { + f.Limit = events.DefaultLimit + } + + out := []lunk.Entry{} + err := b.db.View(func(tx *bolt.Tx) error { + var bkt *bolt.Bucket + var err error + if f.SessionID != "" { + bkt, err = boltbk.GetBucket(tx, []string{"sessions", f.SessionID}) + } else { + bkt, err = boltbk.GetBucket(tx, []string{"events"}) + } + if err != nil { + if backend.IsNotFound(err) { + return nil + } + return err + } + startKey, err := f.Start.MarshalText() + if err != nil { + return err + } + endKey, err := f.End.MarshalText() + if err != nil { + return err + } + // choose start key and iter function based on start or end + c := bkt.Cursor() + var count = 0 + var flimit limitfn + var fnext nextfn + var key, val []byte + if f.Order >= 0 { // ascending + fnext = c.Next + flimit = func() bool { + if !f.End.IsZero() && bytes.Compare(key, endKey) > 0 { + return false + } + return count < f.Limit + } + } else { + fnext = c.Prev // descending + flimit = func() bool { + if !f.End.IsZero() && bytes.Compare(key, endKey) <= 0 { + return false + } + return count < f.Limit + } + } + key, val = c.Seek(startKey) + // this can happen if we supplied the key after last key + // we jump to last key to check if it still matches + if key == nil && f.Order < 0 { + key, val = c.Last() + } + + for ; key != nil && flimit(); key, val = fnext() { + var e *lunk.Entry + if err := json.Unmarshal(val, &e); err != nil { + return err + } + out = append(out, *e) + count += 1 + } + return nil + }) + if err != nil { + return nil, err + } + return out, nil +} + +const SessionID = "sessionid" + +type nextfn func() ([]byte, []byte) +type limitfn func() bool diff --git a/events/boltlog/bl_test.go b/events/boltlog/bl_test.go new file mode 100644 index 00000000000..026d9db1144 --- /dev/null +++ b/events/boltlog/bl_test.go @@ -0,0 +1,38 @@ +package boltlog + +import ( + "path/filepath" + "testing" + + "github.com/gravitational/teleport/events/test" + + . "github.com/gravitational/teleport/Godeps/_workspace/src/gopkg.in/check.v1" +) + +func TestBolt(t *testing.T) { TestingT(t) } + +type BoltLogSuite struct { + l *BoltLog + suite test.EventSuite + dir string +} + +var _ = Suite(&BoltLogSuite{}) + +func (s *BoltLogSuite) SetUpTest(c *C) { + s.dir = c.MkDir() + + var err error + s.l, err = New(filepath.Join(s.dir, "db")) + c.Assert(err, IsNil) + + s.suite.L = s.l +} + +func (s *BoltLogSuite) TearDownTest(c *C) { + c.Assert(s.l.Close(), IsNil) +} + +func (s *BoltLogSuite) TestEventsCRUD(c *C) { + s.suite.EventsCRUD(c) +} diff --git a/events/events.go b/events/events.go index 9318de91dae..26a11446dde 100644 --- a/events/events.go +++ b/events/events.go @@ -54,6 +54,11 @@ func NewExec(command string, out io.Reader, code int, err error) *Exec { // Exec is a result of execution of a remote command on the target server type Exec struct { + // User is SSH user + User string `json:"user"` + + SessionID string `json:"sid"` + // Command is a command name with arguments Command string `json:"command"` @@ -85,6 +90,8 @@ func NewShell(conn ssh.ConnMetadata, shell string, log io.Reader, code int, err // Shell is a result of execution of a in interactive shell type Shell struct { + SessionID string `json:"sid"` + // Shell is a shell name Shell string `json:"command"` diff --git a/events/log.go b/events/log.go new file mode 100644 index 00000000000..ff2ae110e05 --- /dev/null +++ b/events/log.go @@ -0,0 +1,97 @@ +package events + +import ( + "fmt" + "net/url" + "strconv" + "time" + + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/codahale/lunk" +) + +type Filter struct { + Start time.Time + End time.Time + Limit int + Order int + SessionID string +} + +func (f Filter) String() string { + st, err := f.Start.MarshalText() + if err != nil { + return err.Error() + } + if f.Start.IsZero() { + st = []byte("") + } + et, err := f.End.MarshalText() + if err != nil { + return err.Error() + } + if f.End.IsZero() { + et = []byte("") + } + return fmt.Sprintf( + "Filter(start=%v, end=%v, limit=%v, order=%v, sid=%v)", + string(st), string(et), f.Limit, f.Order, f.SessionID) +} + +const ( + DefaultLimit = 20 + Asc = 1 + Desc = -1 +) + +type Log interface { + Log(id lunk.EventID, e lunk.Event) + LogEntry(lunk.Entry) error + GetEvents(filter Filter) ([]lunk.Entry, error) +} + +func FilterToURL(f Filter) (url.Values, error) { + st, err := f.Start.MarshalText() + if err != nil { + return nil, err + } + et, err := f.End.MarshalText() + if err != nil { + return nil, err + } + vals := make(url.Values) + vals.Set("start", string(st)) + vals.Set("end", string(et)) + vals.Set("limit", strconv.Itoa(f.Limit)) + vals.Set("order", strconv.Itoa(f.Order)) + vals.Set("sid", f.SessionID) + return vals, nil +} + +func FilterFromURL(vals url.Values) (*Filter, error) { + var f Filter + var err error + if vals.Get("start") != "" { + if err = f.Start.UnmarshalText([]byte(vals.Get("start"))); err != nil { + return nil, err + } + } + if vals.Get("end") != "" { + if err = f.End.UnmarshalText([]byte(vals.Get("end"))); err != nil { + return nil, err + } + } + + if vals.Get("limit") != "" { + if f.Limit, err = strconv.Atoi(vals.Get("limit")); err != nil { + return nil, err + } + } + + if vals.Get("order") != "" { + if f.Order, err = strconv.Atoi(vals.Get("order")); err != nil { + return nil, err + } + } + f.SessionID = vals.Get("sid") + return &f, nil +} diff --git a/events/test/logsuite.go b/events/test/logsuite.go new file mode 100644 index 00000000000..712bf3059fe --- /dev/null +++ b/events/test/logsuite.go @@ -0,0 +1,77 @@ +// package test contains a log backend acceptance test suite that is +// implementation independant each backend will use the suite to test itself +package test + +import ( + "testing" + "time" + + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/codahale/lunk" + "github.com/gravitational/teleport/events" + + . "github.com/gravitational/teleport/Godeps/_workspace/src/gopkg.in/check.v1" +) + +func TestEventLog(t *testing.T) { TestingT(t) } + +type EventSuite struct { + L events.Log +} + +func (s *EventSuite) EventsCRUD(c *C) { + start := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + + e1 := lunk.NewEntry(lunk.NewRootEventID(), + &events.AuthAttempt{SessionID: "sid1", User: "bob"}) + e1.Time = start + c.Assert(s.L.LogEntry(e1), IsNil) + + e2 := lunk.NewEntry( + lunk.NewRootEventID(), + &events.AuthAttempt{SessionID: "sid1", User: "alice"}) + e2.Time = start.Add(time.Second) + + c.Assert(s.L.LogEntry(e2), IsNil) + + e3 := lunk.NewEntry( + lunk.NewRootEventID(), + &events.AuthAttempt{SessionID: "sid2", User: "bob"}) + e3.Time = start.Add(2 * time.Second) + + c.Assert(s.L.LogEntry(e3), IsNil) + + // get last 2 events + es, err := s.L.GetEvents( + events.Filter{ + Start: start.Add(2 * time.Second), + Order: events.Desc, + Limit: 2, + }) + c.Assert(err, IsNil) + c.Assert(e2p(es...), DeepEquals, e2p(e3, e2)) + + // get last 2 events for session sid1 + es, err = s.L.GetEvents( + events.Filter{ + Start: start.Add(2 * time.Second), + Order: events.Desc, + Limit: 2, + SessionID: "sid1", + }) + c.Assert(err, IsNil) + c.Assert(e2p(es...), DeepEquals, e2p(e2, e1)) + + // get events in range from start to end + es, err = s.L.GetEvents( + events.Filter{Start: start, End: start.Add(time.Second)}) + c.Assert(err, IsNil) + c.Assert(e2p(es...), DeepEquals, e2p(e1, e2)) +} + +func e2p(es ...lunk.Entry) []map[string]string { + out := make([]map[string]string, len(es)) + for i, a := range es { + out[i] = a.Properties + } + return out +} diff --git a/service/service.go b/service/service.go index a0f9c502d25..b702b83deac 100644 --- a/service/service.go +++ b/service/service.go @@ -10,13 +10,14 @@ import ( "github.com/gravitational/teleport/backend/boltbk" "github.com/gravitational/teleport/backend/etcdbk" "github.com/gravitational/teleport/cp" + "github.com/gravitational/teleport/events" + "github.com/gravitational/teleport/events/boltlog" "github.com/gravitational/teleport/session" "github.com/gravitational/teleport/srv" "github.com/gravitational/teleport/tun" "github.com/gravitational/teleport/utils" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/codahale/lunk" - "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/memlog" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/mailgun/log" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/mailgun/oxy/trace" "github.com/gravitational/teleport/Godeps/_workspace/src/golang.org/x/crypto/ssh" @@ -79,6 +80,12 @@ func initAuth(t *TeleportService, cfg Config) error { return err } + elog, err := initEventBackend( + cfg.Auth.EventBackend, cfg.Auth.EventBackendConfig) + if err != nil { + return err + } + asrv, signer, err := auth.Init( b, authority.New(), cfg.FQDN, cfg.Auth.Domain, cfg.DataDir) if err != nil { @@ -88,7 +95,7 @@ func initAuth(t *TeleportService, cfg Config) error { // register HTTP API endpoint t.RegisterFunc(func() error { - apisrv := auth.NewAPIServer(asrv, memlog.New(), session.New(b)) + apisrv := auth.NewAPIServer(asrv, elog, session.New(b)) t, err := trace.New(apisrv, log.GetLogger().Writer(log.SeverityInfo)) if err != nil { log.Fatalf("failed to start: %v", err) @@ -172,7 +179,7 @@ func initSSHEndpoint(t *TeleportService, cfg Config) error { elog := &FanOutEventLogger{ Loggers: []lunk.EventLogger{ lunk.NewTextEventLogger(log.GetLogger().Writer(log.SeverityInfo)), - lunk.NewJSONEventLogger(client.GetLogWriter()), + client, }} s, err := srv.New(cfg.SSH.Addr, @@ -257,7 +264,7 @@ func initTunAgent(t *TeleportService, cfg Config) error { elog := &FanOutEventLogger{ Loggers: []lunk.EventLogger{ lunk.NewTextEventLogger(log.GetLogger().Writer(log.SeverityInfo)), - lunk.NewJSONEventLogger(client.GetLogWriter()), + client, }} a, err := tun.NewAgent( @@ -292,6 +299,14 @@ func initBackend(btype, bcfg string) (backend.Backend, error) { return nil, fmt.Errorf("unsupported backend type: %v", btype) } +func initEventBackend(btype, bcfg string) (events.Log, error) { + switch btype { + case "bolt": + return boltlog.FromString(bcfg) + } + return nil, fmt.Errorf("unsupported backend type: %v", btype) +} + func initLogging(ltype, severity string) error { s, err := log.SeverityFromString(severity) if err != nil { @@ -338,12 +353,16 @@ type Config struct { } type AuthConfig struct { - Enabled bool - HTTPAddr utils.NetAddr - SSHAddr utils.NetAddr - Domain string + Enabled bool + HTTPAddr utils.NetAddr + SSHAddr utils.NetAddr + Domain string + Backend string BackendConfig string + + EventBackend string + EventBackendConfig string } type SSHConfig struct { diff --git a/teleport/main.go b/teleport/main.go index 5b384e583b3..987c13bebb5 100644 --- a/teleport/main.go +++ b/teleport/main.go @@ -54,12 +54,20 @@ func main() { flag.StringVar( &cfg.Auth.Backend, "authBackend", "etcd", - "auth backend type, currently only 'etcd'") + "auth backend type, 'etcd' or 'bolt'") flag.StringVar( &cfg.Auth.BackendConfig, "authBackendConfig", "", "auth backend-specific configuration string") + flag.StringVar( + &cfg.Auth.EventBackend, "authEventBackend", "bolt", + "event backend type, currently only 'bolt'") + + flag.StringVar( + &cfg.Auth.EventBackendConfig, "authEventBackendConfig", "", + "event backend-specific configuration string") + flag.Var( utils.NewNetAddrVal( utils.NetAddr{ diff --git a/tun/srv.go b/tun/srv.go index ad8f13674d8..5e2b298949f 100644 --- a/tun/srv.go +++ b/tun/srv.go @@ -9,9 +9,11 @@ import ( "github.com/gravitational/teleport/auth" "github.com/gravitational/teleport/backend" + "github.com/gravitational/teleport/events" "github.com/gravitational/teleport/sshutils" "github.com/gravitational/teleport/utils" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/codahale/lunk" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/roundtrip" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/mailgun/log" "github.com/gravitational/teleport/Godeps/_workspace/src/golang.org/x/crypto/ssh" @@ -24,7 +26,7 @@ type RemoteSite interface { GetName() string GetServers() ([]backend.Server, error) GetStatus() string - GetEvents() ([]interface{}, error) + GetEvents(events.Filter) ([]lunk.Entry, error) } type Server interface { @@ -172,8 +174,8 @@ type remoteSite struct { clt *auth.Client } -func (s *remoteSite) GetEvents() ([]interface{}, error) { - return s.clt.GetEvents() +func (s *remoteSite) GetEvents(filter events.Filter) ([]lunk.Entry, error) { + return s.clt.GetEvents(filter) } func (s *remoteSite) String() string {