Refactored events

This commit is contained in:
klizhentas
2015-07-08 20:57:34 -07:00
parent 79b31fab8a
commit 76f28e20a9
21 changed files with 746 additions and 268 deletions
+3 -1
View File
@@ -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
-170
View File
@@ -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 (
<div id="wrapper">
<LeftNavBar current="events"/>
<div id="page-wrapper" className="gray-bg">
<TopNavBar/>
<PageHeader title="Cluster Events" url="/events"/>
<div className="wrapper wrapper-content animated fadeInRight">
<Box>
<EventsBox entries={this.state.entries} onShowEvent={this.showEvent}/>
</Box>
</div>
<PageFooter/>
</div>
<EventForm ref="event"/>
</div>
);
}
});
var EventsBox = React.createClass({
render: function() {
if (this.props.entries.length == 0) {
return (
<div className="text-center m-t-lg">
<h1>Logged Events</h1>
<small>There are no events logged. Log in via SSH to watch the events.</small><br/><br/>
</div>);
}
return (
<div>
<EventsTable {...this.props}/>
</div>
);
}
});
var EventsTable = React.createClass({
render: function() {
var show = this.props.onShowEvent
var keyNodes = this.props.entries.map(function (event, index) {
return (
<EventRow event={event} key={index} onShowEvent={show}></EventRow>
);
});
return (
<table className="table table-striped">
<thead>
<tr>
<th></th>
<th>Time</th>
<th>Event</th>
<th>User</th>
<th>Client IP</th>
<th>Remote IP</th>
<th>Data</th>
</tr>
</thead>
<tbody>
{keyNodes}
</tbody>
</table>
);
}
});
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 (
<tr className="key">
<td><a href="#"><i className={d.icon}></i></a></td>
<td>{e.time}</td>
<td>{e.schema}</td>
<td>{e.properties.user}</td>
<td>{e.properties.localaddr}</td>
<td>{e.properties.remoteaddr}</td>
<td><a href="#" onClick={this.showEvent}>{d.text}</a></td>
</tr>
);
}
});
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 (
<BootstrapModal
dialogClass="modal-lg"
icon="fa-list"
ref="modal"
cancel="Close"
onCancel={this.close}
title="SSH Session Log">
<div ref="term" style={{width: '580px', height: '400px'}} className="text-center m-t-lg"></div>
</BootstrapModal>
);
}
});
React.render(
<EventsPage url="/api/events" pollInterval={2000}/>,
document.body
);
+183
View File
@@ -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 (
<div id="wrapper">
<LeftNavBar current="events"/>
<div id="page-wrapper" className="gray-bg">
<TopNavBar/>
<PageHeader title="Cluster Events" url="/events"/>
<div className="wrapper wrapper-content animated fadeInRight">
<Box>
<EventsBox entries={this.state.entries} onShowEvent={this.showEvent}/>
</Box>
</div>
<PageFooter/>
</div>
<EventForm ref="event"/>
</div>);
}
});
var EventsBox = React.createClass({
render: function() {
if (this.props.entries.length == 0) {
return (
<div className="text-center m-t-lg">
<h1>Logged Events</h1>
<small>There are no events logged. Log in via SSH to watch the events.</small><br/><br/>
</div>);
}
return (
<div>
<EventsTable {...this.props}/>
</div>
);
}
});
var EventsTable = React.createClass({
render: function() {
var show = this.props.onShowEvent
var keyNodes = this.props.entries.map(function (event, index) {
return (
<EventRow event={event} key={index} onShowEvent={show}></EventRow>
);
});
return (
<table className="table table-striped">
<thead>
<tr>
<th></th>
<th>Time</th>
<th>Event</th>
<th>User</th>
<th>Client IP</th>
<th>Remote IP</th>
<th>Data</th>
</tr>
</thead>
<tbody>
{keyNodes}
</tbody>
</table>
);
}
});
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 (
<tr className="key">
<td><a href="#"><i className={d.icon}></i></a></td>
<td>{e.time}</td>
<td>{e.schema}</td>
<td>{e.properties.user}</td>
<td>{e.properties.localaddr}</td>
<td>{e.properties.remoteaddr}</td>
<td><a href="#" onClick={this.showEvent}>{d.text}</a></td>
</tr>
);
}
});
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 (
<BootstrapModal dialogClass="modal-lg"
icon="fa-list"
ref="modal"
cancel="Close"
onCancel={this.close}
title="SSH Session Log">
<div ref="term" style={{width: '580px', height: '400px'}} className="text-center m-t-lg"></div>
</BootstrapModal>
);
}
});
React.render(
<EventsPage url="/api/events" pollInterval={2000}/>,
document.body
);
+16
View File
@@ -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;
}
+1 -1
View File
@@ -10,5 +10,5 @@
{{ define "script" }}
<script type="text/javascript" src="/static/js/term.js"></script>
<script type="text/jsx" src="/static/js/grv/events.js"></script>
<script type="text/jsx" src="/static/js/grv/events.jsx"></script>
{{ end }}
+6 -5
View File
@@ -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
+24 -35
View File
@@ -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()
}
+29 -9
View File
@@ -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 {
+11 -16
View File
@@ -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) {
+10 -3
View File
@@ -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{
+14 -14
View File
@@ -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{
+10 -2
View File
@@ -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)
}
+26
View File
@@ -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)
}
+153
View File
@@ -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
+38
View File
@@ -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)
}
+7
View File
@@ -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"`
+97
View File
@@ -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("<zero-time>")
}
et, err := f.End.MarshalText()
if err != nil {
return err.Error()
}
if f.End.IsZero() {
et = []byte("<zero-time>")
}
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
}
+77
View File
@@ -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
}
+27 -8
View File
@@ -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 {
+9 -1
View File
@@ -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{
+5 -3
View File
@@ -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 {