diff options
Diffstat (limited to 'server')
37 files changed, 72 insertions, 3059 deletions
diff --git a/server/a_server-packr.go b/server/a_server-packr.go deleted file mode 100644 index 819e9c2..0000000 --- a/server/a_server-packr.go +++ /dev/null @@ -1,14 +0,0 @@ -// Code generated by github.com/gobuffalo/packr. DO NOT EDIT. - -package server - -import "github.com/gobuffalo/packr" - -// You can use the "packr clean" command to clean up this, -// and any other packr generated files. -func init() { - packr.PackJSONBytes("static", "css/normalize.css", "\"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\"") - packr.PackJSONBytes("static", "css/skeleton.css", "\"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\"") - packr.PackJSONBytes("static", "js/list.min.js", "\"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\"") - packr.PackJSONBytes("static", "js/table.js", "\"H4sIAAAAAAAA/3xU32/aMBB+56+4pg8xKjV03fowlk5ThcYmukoFaZOqCpn4AA9jp7bDD03875MdSNLC9oKI/X13332+u2muUie0AoMvA2EdKjSkCX8aACtmwGBqIYHvw4cfNGPGInFzYalBm2llcYQb1+zusY5NJEICXKf5EpWjLzma7RAlpk4bEp+naNxlQMUVaaL5FpKC/IYRnUthXRSw67mQCCTAqdFrSyWqmZvDLXQKuVDEohwlOnzUa9IJzF0DQFibI/f10VQiMyTc+OLoVJseS+ekNGLJFjjyagjKFogWMGMOGYIleu31hlxCWTTO57q8CiHBX++P71BK0mlSoRSa/uh+AAmgpAvcjgU/Cb46AqcGmUM+Zu4k4d0RATeZMGhPoq+P0JkRKhUZk6cJ748IS7SWzfAk+sMR2uBKL3Bfa7sN3xTHDSxwKzgwxaFKT8uAKUppnzrPNJXMWv9kkMBTFEjRc/cN7votrgpZB9dU3jTDqZgCOaskHh64Hvvm+VU98SehstyBdVuJSbRkZibUx043ArfNMInSOaaLid5EsGIyxySKL6rnhguII1Bs6YFo3FjwCASvfbRv40LvrtbLLMtQ8bu5kJwYvS76OfzWOtpg8NX39K7RKPtYasbv0DhLmJS1iYYEFK7h1/2g71z2iC85WkfKgUTFMy2UdzNuM74Uqu01WvrbahUkeuvKkFARLhKIPzMpE2dyjA+jZ6jOUJH4a28Ut0pwMX+Ucd5boXLl5om96rhV30Z7qEXFSdNX6FUO+w8/x18G/l2mTFrs1gp3ejaT2AuDwGu7bJI7p9W/F1R0XjAvQ73F2qnlOTv89+eVuYfT5sGairLXdjCqyP+qp6LhXK9hLzUKlgFKi/9h9AXH14zGrvE3AAD//0MsPubGBQAA\"") -} diff --git a/server/auth/github/github.go b/server/auth/github/github.go index 38009e1..a46876d 100644 --- a/server/auth/github/github.go +++ b/server/auth/github/github.go @@ -6,7 +6,6 @@ import ( "net/http" "time" - "github.com/nsheridan/cashier/server/auth" "github.com/nsheridan/cashier/server/config" "github.com/nsheridan/cashier/server/metrics" @@ -22,21 +21,23 @@ const ( // Config is an implementation of `auth.Provider` for authenticating using a // Github account. type Config struct { - config *oauth2.Config - organization string - whitelist map[string]bool + config *oauth2.Config + orgWhitelist map[string]bool + userWhitelist map[string]bool } -var _ auth.Provider = (*Config)(nil) - // New creates a new Github provider from a configuration. -func New(c *config.Auth) (*Config, error) { +func New(c *config.Github) (*Config, error) { uw := make(map[string]bool) for _, u := range c.UsersWhitelist { uw[u] = true } - if c.ProviderOpts["organization"] == "" && len(uw) == 0 { - return nil, errors.New("either GitHub organization or users whitelist must be specified") + ow := make(map[string]bool) + for _, o := range c.OrgsWhitelist { + ow[o] = true + } + if len(uw) == 0 && len(ow) == 0 { + return nil, errors.New("either GitHub organizations or users whitelist must be specified") } return &Config{ config: &oauth2.Config{ @@ -49,8 +50,8 @@ func New(c *config.Auth) (*Config, error) { string(githubapi.ScopeReadOrg), }, }, - organization: c.ProviderOpts["organization"], - whitelist: uw, + orgWhitelist: ow, + userWhitelist: uw, }, nil } @@ -66,34 +67,35 @@ func (c *Config) Name() string { // Valid validates the oauth token. func (c *Config) Valid(token *oauth2.Token) bool { - if len(c.whitelist) > 0 && !c.whitelist[c.Username(token)] { - return false - } - if !token.Valid() { - return false - } - if c.organization == "" { - // There's no organization and the token is valid. Can only reach here - // if there's a user whitelist set and the user is in the whitelist. - metrics.M.AuthValid.WithLabelValues("github").Inc() + if c.isUserWhitelisted(token) { return true } - client := githubapi.NewClient(c.newClient(token)) - member, _, err := client.Organizations.IsMember(context.TODO(), c.organization, c.Username(token)) - if err != nil { - return false - } - if member { - metrics.M.AuthValid.WithLabelValues("github").Inc() + if c.isMemberOfWhitelistedOrg(token) { + return true } - return member + return false +} + +func (c *Config) isUserWhitelisted(token *oauth2.Token) bool { + username := c.Username(token) + _, ok := c.userWhitelist[username] + return ok } -// Revoke is a no-op revoke method. GitHub doesn't seem to allow token -// revocation - tokens are indefinite and there are no refresh options etc. -// Returns nil to satisfy the Provider interface. -func (c *Config) Revoke(token *oauth2.Token) error { - return nil +func (c *Config) isMemberOfWhitelistedOrg(token *oauth2.Token) bool { + client := githubapi.NewClient(c.newClient(token)) + username := c.Username(token) + for org, _ := range c.orgWhitelist { + member, _, err := client.Organizations.IsMember(context.TODO(), org, username) + if err != nil { + return false + } + if member { + metrics.M.AuthValid.WithLabelValues("github").Inc() + return true + } + } + return false } // StartSession retrieves an authentication endpoint from Github. diff --git a/server/auth/gitlab/gitlab.go b/server/auth/gitlab/gitlab.go deleted file mode 100644 index 70d3d1c..0000000 --- a/server/auth/gitlab/gitlab.go +++ /dev/null @@ -1,225 +0,0 @@ -package gitlab - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "log" - "net/http" - "strconv" - - "github.com/nsheridan/cashier/server/config" - "github.com/nsheridan/cashier/server/metrics" - - "golang.org/x/oauth2" -) - -const ( - name = "gitlab" -) - -// Config is an implementation of `auth.Provider` for authenticating using a -// Gitlab account. -type Config struct { - config *oauth2.Config - group string - whitelist map[string]bool - allusers bool - apiurl string - log bool -} - -// Note on Gitlab REST API calls. We don't parse errors because it's -// kind of a pain: -// https://gitlab.com/help/api/README.md#data-validation-and-error-reporting -// The two v4 api calls used are /user and /groups/:group/members/:uid -// https://gitlab.com/help/api/users.md#for-normal-users-1 -// https://gitlab.com/help/api/members.md#get-a-member-of-a-group-or-project -type serviceUser struct { - ID int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` -} - -type serviceGroupMember struct { - ID int `json:"id"` - State string `json:"state"` - AccessLevel int `json:"access_level"` -} - -func (c *Config) logMsg(message error) { - if c.log { - log.Print(message) - } -} - -// A new oauth2 http client. -func (c *Config) newClient(token *oauth2.Token) *http.Client { - return c.config.Client(oauth2.NoContext, token) -} - -func (c *Config) getURL(token *oauth2.Token, url string) (*bytes.Buffer, error) { - client := c.newClient(token) - resp, err := client.Get(url) - if err != nil { - return nil, fmt.Errorf("Failed to get groups: %s", err) - } - defer resp.Body.Close() - var body bytes.Buffer - io.Copy(&body, resp.Body) - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Gitlab error(http: %d) getting %s: '%s'", - resp.StatusCode, url, body.String()) - } - return &body, nil -} - -// Gets info on the current user. -func (c *Config) getUser(token *oauth2.Token) *serviceUser { - url := c.apiurl + "user" - body, err := c.getURL(token, url) - if err != nil { - c.logMsg(err) - return nil - } - var user serviceUser - if err := json.NewDecoder(body).Decode(&user); err != nil { - c.logMsg(fmt.Errorf("Failed to decode user (%s): %s", url, err)) - return nil - } - return &user -} - -// Gets current user group membership info. -func (c *Config) checkGroupMembership(token *oauth2.Token, uid int, group string) bool { - url := fmt.Sprintf("%sgroups/%s/members/%d", c.apiurl, group, uid) - body, err := c.getURL(token, url) - if err != nil { - c.logMsg(err) - return false - } - var m serviceGroupMember - if err := json.NewDecoder(body).Decode(&m); err != nil { - c.logMsg(fmt.Errorf("Failed to parse groups (%s): %s", url, err)) - return false - } - return m.ID == uid -} - -// New creates a new Gitlab provider from a configuration. -func New(c *config.Auth) (*Config, error) { - logOpt, _ := strconv.ParseBool(c.ProviderOpts["log"]) - uw := make(map[string]bool) - for _, u := range c.UsersWhitelist { - uw[u] = true - } - allUsers, _ := strconv.ParseBool(c.ProviderOpts["allusers"]) - if !allUsers && c.ProviderOpts["group"] == "" && len(uw) == 0 { - return nil, errors.New("gitlab_opts group and the users whitelist must not be both empty if allusers isn't true") - } - siteURL := "https://gitlab.com/" - if c.ProviderOpts["siteurl"] != "" { - siteURL = c.ProviderOpts["siteurl"] - if siteURL[len(siteURL)-1] != '/' { - return nil, errors.New("gitlab_opts siteurl must end in /") - } - } else { - if allUsers { - return nil, errors.New("gitlab_opts if allusers is set, siteurl must be set") - } - } - // TODO: Should make sure siteURL is just the host bit. - oauth2.RegisterBrokenAuthHeaderProvider(siteURL) - - return &Config{ - config: &oauth2.Config{ - ClientID: c.OauthClientID, - ClientSecret: c.OauthClientSecret, - RedirectURL: c.OauthCallbackURL, - Endpoint: oauth2.Endpoint{ - AuthURL: siteURL + "oauth/authorize", - TokenURL: siteURL + "oauth/token", - }, - Scopes: []string{ - "api", - }, - }, - group: c.ProviderOpts["group"], - whitelist: uw, - allusers: allUsers, - apiurl: siteURL + "api/v4/", - log: logOpt, - }, nil -} - -// Name returns the name of the provider. -func (c *Config) Name() string { - return name -} - -// Valid validates the oauth token. -func (c *Config) Valid(token *oauth2.Token) bool { - if !token.Valid() { - log.Printf("Auth fail (oauth2 Valid failure)") - return false - } - if c.allusers { - log.Printf("Auth success (allusers)") - metrics.M.AuthValid.WithLabelValues("gitlab").Inc() - return true - } - u := c.getUser(token) - if u == nil { - return false - } - if len(c.whitelist) > 0 && !c.whitelist[c.Username(token)] { - c.logMsg(errors.New("Auth fail (not in whitelist)")) - return false - } - if c.group == "" { - // There's no group and token is valid. Can only reach - // here if user whitelist is set and user is in whitelist. - c.logMsg(errors.New("Auth success (no groups specified in server config)")) - metrics.M.AuthValid.WithLabelValues("gitlab").Inc() - return true - } - if !c.checkGroupMembership(token, u.ID, c.group) { - c.logMsg(errors.New("Auth failure (not in allowed group)")) - return false - } - metrics.M.AuthValid.WithLabelValues("gitlab").Inc() - c.logMsg(errors.New("Auth success (in allowed group)")) - return true -} - -// Revoke is a no-op revoke method. Gitlab doesn't allow token -// revocation - tokens live for an hour. -// Returns nil to satisfy the Provider interface. -func (c *Config) Revoke(token *oauth2.Token) error { - return nil -} - -// StartSession retrieves an authentication endpoint from Gitlab. -func (c *Config) StartSession(state string) string { - return c.config.AuthCodeURL(state) -} - -// Exchange authorizes the session and returns an access token. -func (c *Config) Exchange(code string) (*oauth2.Token, error) { - t, err := c.config.Exchange(oauth2.NoContext, code) - if err == nil { - metrics.M.AuthExchange.WithLabelValues("gitlab").Inc() - } - return t, err -} - -// Username retrieves the username of the Gitlab user. -func (c *Config) Username(token *oauth2.Token) string { - u := c.getUser(token) - if u == nil { - return "" - } - return u.Username -} diff --git a/server/auth/gitlab/gitlab_test.go b/server/auth/gitlab/gitlab_test.go deleted file mode 100644 index 93b348b..0000000 --- a/server/auth/gitlab/gitlab_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package gitlab - -import ( - "fmt" - "testing" - - "github.com/nsheridan/cashier/server/auth" - "github.com/nsheridan/cashier/server/config" - "github.com/stretchr/testify/assert" -) - -var ( - oauthClientID = "id" - oauthClientSecret = "secret" - oauthCallbackURL = "url" - allusers = "" - siteurl = "https://exampleorg/" - group = "exampleorg" -) - -func TestNew(t *testing.T) { - a := assert.New(t) - - p, _ := newGitlab() - g := p.(*Config) - a.Equal(g.config.ClientID, oauthClientID) - a.Equal(g.config.ClientSecret, oauthClientSecret) - a.Equal(g.config.RedirectURL, oauthCallbackURL) -} - -func TestNewBrokenSiteURL(t *testing.T) { - siteurl = "https://exampleorg" - a := assert.New(t) - - _, err := newGitlab() - a.EqualError(err, "gitlab_opts siteurl must end in /") - - siteurl = "https://exampleorg/" -} - -func TestBadAllUsers(t *testing.T) { - allusers = "true" - siteurl = "" - a := assert.New(t) - - _, err := newGitlab() - a.EqualError(err, "gitlab_opts if allusers is set, siteurl must be set") - - allusers = "" - siteurl = "https://exampleorg/" -} - -func TestGoodAllUsers(t *testing.T) { - allusers = "true" - a := assert.New(t) - - p, _ := newGitlab() - s := p.StartSession("test_state") - a.Contains(s, "exampleorg/oauth/authorize") - a.Contains(s, "state=test_state") - a.Contains(s, fmt.Sprintf("client_id=%s", oauthClientID)) - - allusers = "" -} - -func TestNewEmptyGroupList(t *testing.T) { - group = "" - a := assert.New(t) - - _, err := newGitlab() - a.EqualError(err, "gitlab_opts group and the users whitelist must not be both empty if allusers isn't true") - - group = "exampleorg" -} - -func TestStartSession(t *testing.T) { - a := assert.New(t) - - p, _ := newGitlab() - s := p.StartSession("test_state") - a.Contains(s, "exampleorg/oauth/authorize") - a.Contains(s, "state=test_state") - a.Contains(s, fmt.Sprintf("client_id=%s", oauthClientID)) -} - -func newGitlab() (auth.Provider, error) { - c := &config.Auth{ - OauthClientID: oauthClientID, - OauthClientSecret: oauthClientSecret, - OauthCallbackURL: oauthCallbackURL, - ProviderOpts: map[string]string{ - "group": group, - "siteurl": siteurl, - "allusers": allusers, - }, - } - return New(c) -} diff --git a/server/auth/google/google.go b/server/auth/google/google.go deleted file mode 100644 index b707310..0000000 --- a/server/auth/google/google.go +++ /dev/null @@ -1,135 +0,0 @@ -package google - -import ( - "errors" - "fmt" - "net/http" - "strings" - - "github.com/nsheridan/cashier/server/auth" - "github.com/nsheridan/cashier/server/config" - "github.com/nsheridan/cashier/server/metrics" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" - googleapi "google.golang.org/api/oauth2/v2" -) - -const ( - revokeURL = "https://accounts.google.com/o/oauth2/revoke?token=%s" - name = "google" -) - -// Config is an implementation of `auth.Provider` for authenticating using a -// Google account. -type Config struct { - config *oauth2.Config - domain string - whitelist map[string]bool -} - -var _ auth.Provider = (*Config)(nil) - -// New creates a new Google provider from a configuration. -func New(c *config.Auth) (*Config, error) { - uw := make(map[string]bool) - for _, u := range c.UsersWhitelist { - uw[u] = true - } - if c.ProviderOpts["domain"] == "" && len(uw) == 0 { - return nil, errors.New("either Google Apps domain or users whitelist must be specified") - } - - return &Config{ - config: &oauth2.Config{ - ClientID: c.OauthClientID, - ClientSecret: c.OauthClientSecret, - RedirectURL: c.OauthCallbackURL, - Endpoint: google.Endpoint, - Scopes: []string{googleapi.UserinfoEmailScope, googleapi.UserinfoProfileScope}, - }, - domain: c.ProviderOpts["domain"], - whitelist: uw, - }, nil -} - -// A new oauth2 http client. -func (c *Config) newClient(token *oauth2.Token) *http.Client { - return c.config.Client(oauth2.NoContext, token) -} - -// Name returns the name of the provider. -func (c *Config) Name() string { - return name -} - -// Valid validates the oauth token. -func (c *Config) Valid(token *oauth2.Token) bool { - if len(c.whitelist) > 0 && !c.whitelist[c.Email(token)] { - return false - } - if !token.Valid() { - return false - } - svc, err := googleapi.New(c.newClient(token)) - if err != nil { - return false - } - t := svc.Tokeninfo() - t.AccessToken(token.AccessToken) - ti, err := t.Do() - if err != nil { - return false - } - if ti.Audience != c.config.ClientID { - return false - } - ui, err := svc.Userinfo.Get().Do() - if err != nil { - return false - } - if c.domain != "" && ui.Hd != c.domain { - return false - } - metrics.M.AuthValid.WithLabelValues("google").Inc() - return true -} - -// Revoke disables the access token. -func (c *Config) Revoke(token *oauth2.Token) error { - h := c.newClient(token) - _, err := h.Get(fmt.Sprintf(revokeURL, token.AccessToken)) - return err -} - -// StartSession retrieves an authentication endpoint from Google. -func (c *Config) StartSession(state string) string { - return c.config.AuthCodeURL(state, oauth2.SetAuthURLParam("hd", c.domain)) -} - -// Exchange authorizes the session and returns an access token. -func (c *Config) Exchange(code string) (*oauth2.Token, error) { - t, err := c.config.Exchange(oauth2.NoContext, code) - if err == nil { - metrics.M.AuthExchange.WithLabelValues("google").Inc() - } - return t, err -} - -// Email retrieves the email address of the user. -func (c *Config) Email(token *oauth2.Token) string { - svc, err := googleapi.New(c.newClient(token)) - if err != nil { - return "" - } - ui, err := svc.Userinfo.Get().Do() - if err != nil { - return "" - } - return ui.Email -} - -// Username retrieves the username portion of the user's email address. -func (c *Config) Username(token *oauth2.Token) string { - return strings.Split(c.Email(token), "@")[0] -} diff --git a/server/auth/google/google_test.go b/server/auth/google/google_test.go deleted file mode 100644 index 92e4ca0..0000000 --- a/server/auth/google/google_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package google - -import ( - "fmt" - "testing" - - "github.com/nsheridan/cashier/server/config" - "github.com/stretchr/testify/assert" -) - -var ( - oauthClientID = "id" - oauthClientSecret = "secret" - oauthCallbackURL = "url" - domain = "example.com" - users = []string{"user"} -) - -func TestNew(t *testing.T) { - a := assert.New(t) - p, err := newGoogle() - a.NoError(err) - a.Equal(p.config.ClientID, oauthClientID) - a.Equal(p.config.ClientSecret, oauthClientSecret) - a.Equal(p.config.RedirectURL, oauthCallbackURL) - a.Equal(p.domain, domain) - a.Equal(p.whitelist, map[string]bool{"user": true}) -} - -func TestWhitelist(t *testing.T) { - c := &config.Auth{ - OauthClientID: oauthClientID, - OauthClientSecret: oauthClientSecret, - OauthCallbackURL: oauthCallbackURL, - ProviderOpts: map[string]string{"domain": ""}, - UsersWhitelist: []string{}, - } - if _, err := New(c); err == nil { - t.Error("creating a provider without a domain set should return an error") - } - // Set a user whitelist but no domain - c.UsersWhitelist = users - if _, err := New(c); err != nil { - t.Error("creating a provider with users but no domain should not return an error") - } - // Unset the user whitelist and set a domain - c.UsersWhitelist = []string{} - c.ProviderOpts = map[string]string{"domain": domain} - if _, err := New(c); err != nil { - t.Error("creating a provider with a domain set but without a user whitelist should not return an error") - } -} - -func TestStartSession(t *testing.T) { - a := assert.New(t) - - p, err := newGoogle() - a.NoError(err) - s := p.StartSession("test_state") - a.Contains(s, "accounts.google.com/o/oauth2/auth") - a.Contains(s, "state=test_state") - a.Contains(s, fmt.Sprintf("hd=%s", domain)) - a.Contains(s, fmt.Sprintf("client_id=%s", oauthClientID)) -} - -func newGoogle() (*Config, error) { - c := &config.Auth{ - OauthClientID: oauthClientID, - OauthClientSecret: oauthClientSecret, - OauthCallbackURL: oauthCallbackURL, - ProviderOpts: map[string]string{"domain": domain}, - UsersWhitelist: users, - } - return New(c) -} diff --git a/server/auth/microsoft/microsoft.go b/server/auth/microsoft/microsoft.go deleted file mode 100644 index 8463ccf..0000000 --- a/server/auth/microsoft/microsoft.go +++ /dev/null @@ -1,201 +0,0 @@ -package microsoft - -import ( - "encoding/json" - "errors" - "net/http" - "path" - "strings" - - "github.com/nsheridan/cashier/server/auth" - "github.com/nsheridan/cashier/server/config" - "github.com/nsheridan/cashier/server/metrics" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/microsoft" -) - -const ( - name = "microsoft" -) - -// Config is an implementation of `auth.Provider` for authenticating using a -// Office 365 account. -type Config struct { - config *oauth2.Config - tenant string - groups map[string]bool - whitelist map[string]bool -} - -var _ auth.Provider = (*Config)(nil) - -// New creates a new Microsoft provider from a configuration. -func New(c *config.Auth) (*Config, error) { - whitelist := make(map[string]bool) - for _, u := range c.UsersWhitelist { - whitelist[u] = true - } - if c.ProviderOpts["tenant"] == "" && len(whitelist) == 0 { - return nil, errors.New("either Office 365 tenant or users whitelist must be specified") - } - groupMap := make(map[string]bool) - if groups, ok := c.ProviderOpts["groups"]; ok { - for _, group := range strings.Split(groups, ",") { - groupMap[strings.Trim(group, " ")] = true - } - } - - return &Config{ - config: &oauth2.Config{ - ClientID: c.OauthClientID, - ClientSecret: c.OauthClientSecret, - RedirectURL: c.OauthCallbackURL, - Endpoint: microsoft.AzureADEndpoint(c.ProviderOpts["tenant"]), - Scopes: []string{"user.Read.All", "Directory.Read.All"}, - }, - tenant: c.ProviderOpts["tenant"], - whitelist: whitelist, - groups: groupMap, - }, nil -} - -// A new oauth2 http client. -func (c *Config) newClient(token *oauth2.Token) *http.Client { - return c.config.Client(oauth2.NoContext, token) -} - -// Gets a response for an graph api call. -func (c *Config) getDocument(token *oauth2.Token, pathElements ...string) map[string]interface{} { - client := c.newClient(token) - url := "https://" + path.Join("graph.microsoft.com/v1.0", path.Join(pathElements...)) - resp, err := client.Get(url) - if err != nil { - return nil - } - defer resp.Body.Close() - var document map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&document); err != nil { - return nil - } - return document -} - -// Get info from the "/me" endpoint of the Microsoft Graph API (MSG-API). -// https://developer.microsoft.com/en-us/graph/docs/concepts/v1-overview -func (c *Config) getMe(token *oauth2.Token, item string) string { - document := c.getDocument(token, "/me") - if value, ok := document[item].(string); ok { - return value - } - return "" -} - -// Check against verified domains from "/organization" endpoint of MSG-API. -func (c *Config) verifyTenant(token *oauth2.Token) bool { - document := c.getDocument(token, "/organization") - // The domains for an organisation are in an array of structs under - // verifiedDomains, which is in a struct which is in turn an array - // of such structs under value in the document. Which in json looks - // like this: - // { "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#organization", - // "value": [ { - // ... - // "verifiedDomains": [ { - // ... - // "name": "M365x214355.onmicrosoft.com", - // } ] - // } ] - //} - var value []interface{} - var ok bool - if value, ok = document["value"].([]interface{}); !ok { - return false - } - for _, valueEntry := range value { - if value, ok = valueEntry.(map[string]interface{})["verifiedDomains"].([]interface{}); !ok { - continue - } - for _, val := range value { - domain := val.(map[string]interface{})["name"].(string) - if domain == c.tenant { - return true - } - } - } - return false -} - -// Check against groups from /users/{id}/memberOf endpoint of MSG-API. -func (c *Config) verifyGroups(token *oauth2.Token) bool { - document := c.getDocument(token, "/users/me/memberOf") - var value []interface{} - var ok bool - if value, ok = document["value"].([]interface{}); !ok { - return false - } - for _, valueEntry := range value { - if group, ok := valueEntry.(map[string]interface{})["displayName"].(string); ok { - if c.groups[group] { - return true - } - } - } - return false -} - -// Name returns the name of the provider. -func (c *Config) Name() string { - return name -} - -// Valid validates the oauth token. -func (c *Config) Valid(token *oauth2.Token) bool { - if len(c.whitelist) > 0 && !c.whitelist[c.Email(token)] { - return false - } - if !token.Valid() { - return false - } - metrics.M.AuthValid.WithLabelValues("microsoft").Inc() - if c.tenant != "" { - if c.verifyTenant(token) { - if len(c.groups) > 0 { - return c.verifyGroups(token) - } - return true - } - } - return false -} - -// Revoke disables the access token. -func (c *Config) Revoke(token *oauth2.Token) error { - return nil -} - -// StartSession retrieves an authentication endpoint from Microsoft. -func (c *Config) StartSession(state string) string { - return c.config.AuthCodeURL(state, - oauth2.SetAuthURLParam("hd", c.tenant), - oauth2.SetAuthURLParam("prompt", "login")) -} - -// Exchange authorizes the session and returns an access token. -func (c *Config) Exchange(code string) (*oauth2.Token, error) { - t, err := c.config.Exchange(oauth2.NoContext, code) - if err == nil { - metrics.M.AuthExchange.WithLabelValues("microsoft").Inc() - } - return t, err -} - -// Email retrieves the email address of the user. -func (c *Config) Email(token *oauth2.Token) string { - return c.getMe(token, "mail") -} - -// Username retrieves the username portion of the user's email address. -func (c *Config) Username(token *oauth2.Token) string { - return strings.Split(c.Email(token), "@")[0] -} diff --git a/server/auth/microsoft/microsoft_test.go b/server/auth/microsoft/microsoft_test.go deleted file mode 100644 index e362ef9..0000000 --- a/server/auth/microsoft/microsoft_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package microsoft - -import ( - "fmt" - "testing" - - "github.com/nsheridan/cashier/server/config" - "github.com/stretchr/testify/assert" -) - -var ( - oauthClientID = "id" - oauthClientSecret = "secret" - oauthCallbackURL = "url" - tenant = "example.com" - users = []string{"user"} -) - -func TestNew(t *testing.T) { - a := assert.New(t) - p, err := newMicrosoft() - a.NoError(err) - a.Equal(p.config.ClientID, oauthClientID) - a.Equal(p.config.ClientSecret, oauthClientSecret) - a.Equal(p.config.RedirectURL, oauthCallbackURL) - a.Equal(p.tenant, tenant) - a.Equal(p.whitelist, map[string]bool{"user": true}) -} - -func TestWhitelist(t *testing.T) { - c := &config.Auth{ - OauthClientID: oauthClientID, - OauthClientSecret: oauthClientSecret, - OauthCallbackURL: oauthCallbackURL, - ProviderOpts: map[string]string{"tenant": ""}, - UsersWhitelist: []string{}, - } - if _, err := New(c); err == nil { - t.Error("creating a provider without a tenant set should return an error") - } - // Set a user whitelist but no tenant - c.UsersWhitelist = users - if _, err := New(c); err != nil { - t.Error("creating a provider with users but no tenant should not return an error") - } - // Unset the user whitelist and set a tenant - c.UsersWhitelist = []string{} - c.ProviderOpts = map[string]string{"tenant": tenant} - if _, err := New(c); err != nil { - t.Error("creating a provider with a tenant set but without a user whitelist should not return an error") - } -} - -func TestStartSession(t *testing.T) { - a := assert.New(t) - - p, err := newMicrosoft() - a.NoError(err) - s := p.StartSession("test_state") - a.Contains(s, fmt.Sprintf("login.microsoftonline.com/%s/oauth2/v2.0/authorize", tenant)) -} - -func newMicrosoft() (*Config, error) { - c := &config.Auth{ - OauthClientID: oauthClientID, - OauthClientSecret: oauthClientSecret, - OauthCallbackURL: oauthCallbackURL, - ProviderOpts: map[string]string{"tenant": tenant}, - UsersWhitelist: users, - } - return New(c) -} diff --git a/server/auth/provider.go b/server/auth/provider.go deleted file mode 100644 index 9d1c8bd..0000000 --- a/server/auth/provider.go +++ /dev/null @@ -1,13 +0,0 @@ -package auth - -import "golang.org/x/oauth2" - -// Provider is an abstraction of different auth methods. -type Provider interface { - Name() string - StartSession(string) string - Exchange(string) (*oauth2.Token, error) - Username(*oauth2.Token) string - Valid(*oauth2.Token) bool - Revoke(*oauth2.Token) error -} diff --git a/server/auth/testprovider/testprovider.go b/server/auth/testprovider/testprovider.go deleted file mode 100644 index f785081..0000000 --- a/server/auth/testprovider/testprovider.go +++ /dev/null @@ -1,56 +0,0 @@ -package testprovider - -import ( - "time" - - "github.com/nsheridan/cashier/server/auth" - - "golang.org/x/oauth2" -) - -const ( - name = "testprovider" -) - -// Config is an implementation of `auth.Provider` for testing. -type Config struct{} - -var _ auth.Provider = (*Config)(nil) - -// New creates a new provider. -func New() *Config { - return &Config{} -} - -// Name returns the name of the provider. -func (c *Config) Name() string { - return name -} - -// Valid validates the oauth token. -func (c *Config) Valid(token *oauth2.Token) bool { - return true -} - -// Revoke disables the access token. -func (c *Config) Revoke(token *oauth2.Token) error { - return nil -} - -// StartSession retrieves an authentication endpoint. -func (c *Config) StartSession(state string) string { - return "https://www.example.com/auth" -} - -// Exchange authorizes the session and returns an access token. -func (c *Config) Exchange(code string) (*oauth2.Token, error) { - return &oauth2.Token{ - AccessToken: "token", - Expiry: time.Now().Add(1 * time.Hour), - }, nil -} - -// Username retrieves the username portion of the user's email address. -func (c *Config) Username(token *oauth2.Token) string { - return "test" -} diff --git a/server/config/config.go b/server/config/config.go index 1985800..82ddfec 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -3,51 +3,37 @@ package config import ( "os" "strconv" - "strings" "github.com/hashicorp/go-multierror" "github.com/homemade/scl" - "github.com/nsheridan/cashier/server/helpers/vault" "github.com/pkg/errors" ) // Config holds the final server configuration. type Config struct { Server *Server `hcl:"server"` - Auth *Auth `hcl:"auth"` + Github *Github `hcl:"github"` SSH *SSH `hcl:"ssh"` - AWS *AWS `hcl:"aws"` - Vault *Vault `hcl:"vault"` } -// Database holds database configuration. -type Database map[string]string - // Server holds the configuration specific to the web server and sessions. type Server struct { - UseTLS bool `hcl:"use_tls"` - TLSKey string `hcl:"tls_key"` - TLSCert string `hcl:"tls_cert"` - LetsEncryptServername string `hcl:"letsencrypt_servername"` - LetsEncryptCache string `hcl:"letsencrypt_cachedir"` - Addr string `hcl:"address"` - Port int `hcl:"port"` - User string `hcl:"user"` - CookieSecret string `hcl:"cookie_secret"` - CSRFSecret string `hcl:"csrf_secret"` - HTTPLogFile string `hcl:"http_logfile"` - Database Database `hcl:"database"` - RequireReason bool `hcl:"require_reason"` + Addr string `hcl:"address"` + Port int `hcl:"port"` + User string `hcl:"user"` + CookieSecret string `hcl:"cookie_secret"` + SecureCookie bool `hcl:"secure_cookie"` + CSRFSecret string `hcl:"csrf_secret"` + HTTPLogFile string `hcl:"http_logfile"` } // Auth holds the configuration specific to the OAuth provider. -type Auth struct { - OauthClientID string `hcl:"oauth_client_id"` - OauthClientSecret string `hcl:"oauth_client_secret"` - OauthCallbackURL string `hcl:"oauth_callback_url"` - Provider string `hcl:"provider"` - ProviderOpts map[string]string `hcl:"provider_opts"` - UsersWhitelist []string `hcl:"users_whitelist"` +type Github struct { + OauthClientID string `hcl:"oauth_client_id"` + OauthClientSecret string `hcl:"oauth_client_secret"` + OauthCallbackURL string `hcl:"oauth_callback_url"` + UsersWhitelist []string `hcl:"users_whitelist"` + OrgsWhitelist []string `hcl:"orgs_whitelist"` } // SSH holds the configuration specific to signing ssh keys. @@ -58,27 +44,13 @@ type SSH struct { Permissions []string `hcl:"permissions"` } -// AWS holds Amazon AWS configuration. -// AWS can also be configured using SDK methods. -type AWS struct { - Region string `hcl:"region"` - AccessKey string `hcl:"access_key"` - SecretKey string `hcl:"secret_key"` -} - -// Vault holds Hashicorp Vault configuration. -type Vault struct { - Address string `hcl:"address"` - Token string `hcl:"token"` -} - func verifyConfig(c *Config) error { var err error if c.SSH == nil { err = multierror.Append(err, errors.New("missing ssh config section")) } - if c.Auth == nil { - err = multierror.Append(err, errors.New("missing auth config section")) + if c.Github == nil { + err = multierror.Append(err, errors.New("missing github config section")) } if c.Server == nil { err = multierror.Append(err, errors.New("missing server config section")) @@ -92,10 +64,10 @@ func setFromEnvironment(c *Config) { c.Server.Port = port } if os.Getenv("OAUTH_CLIENT_ID") != "" { - c.Auth.OauthClientID = os.Getenv("OAUTH_CLIENT_ID") + c.Github.OauthClientID = os.Getenv("OAUTH_CLIENT_ID") } if os.Getenv("OAUTH_CLIENT_SECRET") != "" { - c.Auth.OauthClientSecret = os.Getenv("OAUTH_CLIENT_SECRET") + c.Github.OauthClientSecret = os.Getenv("OAUTH_CLIENT_SECRET") } if os.Getenv("CSRF_SECRET") != "" { c.Server.CSRFSecret = os.Getenv("CSRF_SECRET") @@ -105,48 +77,12 @@ func setFromEnvironment(c *Config) { } } -func setFromVault(c *Config) error { - if c.Vault == nil || c.Vault.Token == "" || c.Vault.Address == "" { - return nil - } - v, err := vault.NewClient(c.Vault.Address, c.Vault.Token) - if err != nil { - return errors.Wrap(err, "vault error") - } - var errs error - get := func(value string) string { - if strings.HasPrefix(value, "/vault/") { - s, err := v.Read(value) - if err != nil { - errs = multierror.Append(errs, err) - } - return s - } - return value - } - c.Auth.OauthClientID = get(c.Auth.OauthClientID) - c.Auth.OauthClientSecret = get(c.Auth.OauthClientSecret) - c.Server.CSRFSecret = get(c.Server.CSRFSecret) - c.Server.CookieSecret = get(c.Server.CookieSecret) - if len(c.Server.Database) != 0 { - c.Server.Database["password"] = get(c.Server.Database["password"]) - } - if c.AWS != nil { - c.AWS.AccessKey = get(c.AWS.AccessKey) - c.AWS.SecretKey = get(c.AWS.SecretKey) - } - return errors.Wrap(errs, "errors reading from vault") -} - // ReadConfig parses a hcl configuration file into a Config struct. func ReadConfig(f string) (*Config, error) { config := &Config{} if err := scl.DecodeFile(config, f); err != nil { return nil, errors.Wrapf(err, "unable to load config from file %s", f) } - if err := setFromVault(config); err != nil { - return nil, err - } setFromEnvironment(config) if err := verifyConfig(config); err != nil { return nil, errors.Wrap(err, "unable to verify config") diff --git a/server/handlers.go b/server/handlers.go index 3f3543e..f3b25aa 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -9,14 +9,10 @@ import ( "io" "log" "net/http" - "strconv" "strings" - "github.com/gorilla/csrf" "github.com/nsheridan/cashier/lib" - "github.com/nsheridan/cashier/server/store" "github.com/nsheridan/cashier/server/templates" - "github.com/pkg/errors" "golang.org/x/oauth2" ) @@ -46,15 +42,7 @@ func (a *app) sign(w http.ResponseWriter, r *http.Request) { return } - if a.requireReason && req.Message == "" { - w.Header().Add("X-Need-Reason", "required") - w.WriteHeader(http.StatusForbidden) - fmt.Fprint(w, http.StatusText(http.StatusForbidden)) - return - } - username := a.authprovider.Username(token) - a.authprovider.Revoke(token) // We don't need this anymore. cert, err := a.keysigner.SignUserKey(req, username) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -62,11 +50,6 @@ func (a *app) sign(w http.ResponseWriter, r *http.Request) { return } - rec := store.MakeRecord(cert) - rec.Message = req.Message - if err := a.certstore.SetRecord(rec); err != nil { - log.Printf("Error recording cert: %v", err) - } if err := json.NewEncoder(w).Encode(&lib.SignResponse{ Status: "ok", Response: string(lib.GetPublicKey(cert)), @@ -123,52 +106,3 @@ func (a *app) index(w http.ResponseWriter, r *http.Request) { tmpl := template.Must(template.New("token.html").Parse(templates.Token)) tmpl.Execute(w, page) } - -func (a *app) revoked(w http.ResponseWriter, r *http.Request) { - revoked, err := a.certstore.GetRevoked() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, errors.Wrap(err, "error retrieving revoked certs").Error()) - return - } - rl, err := a.keysigner.GenerateRevocationList(revoked) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, errors.Wrap(err, "unable to generate KRL").Error()) - return - } - w.Header().Set("Content-Type", "application/octet-stream") - w.Write(rl) -} - -func (a *app) getAllCerts(w http.ResponseWriter, r *http.Request) { - tmpl := template.Must(template.New("certs.html").Parse(templates.Certs)) - tmpl.Execute(w, map[string]interface{}{ - csrf.TemplateTag: csrf.TemplateField(r), - }) -} - -func (a *app) getCertsJSON(w http.ResponseWriter, r *http.Request) { - includeExpired, _ := strconv.ParseBool(r.URL.Query().Get("all")) - certs, err := a.certstore.List(includeExpired) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprint(w, http.StatusText(http.StatusInternalServerError)) - return - } - if err := json.NewEncoder(w).Encode(certs); err != nil { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprint(w, http.StatusText(http.StatusInternalServerError)) - return - } -} - -func (a *app) revoke(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - if err := a.certstore.Revoke(r.Form["cert_id"]); err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Unable to revoke certs")) - } else { - http.Redirect(w, r, "/admin/certs", http.StatusSeeOther) - } -} diff --git a/server/helpers/vault/vault.go b/server/helpers/vault/vault.go deleted file mode 100644 index e522d51..0000000 --- a/server/helpers/vault/vault.go +++ /dev/null @@ -1,62 +0,0 @@ -package vault - -import ( - "fmt" - "strings" - - "github.com/hashicorp/vault/api" -) - -// NewClient returns a new vault client. -func NewClient(address, token string) (*Client, error) { - config := &api.Config{ - Address: address, - } - client, err := api.NewClient(config) - if err != nil { - return nil, err - } - client.SetToken(token) - return &Client{ - vault: client, - }, nil -} - -func parseName(name string) (path, key string) { - name = strings.TrimPrefix(name, "/vault/") - i := strings.LastIndex(name, "/") - if i < 0 { - return name, "" - } - return name[:i], name[i+1:] -} - -// Client is a simple client for vault. -type Client struct { - vault *api.Client -} - -// Read returns a secret for a given path and key of the form `/vault/secret/path/key`. -// If the requested key cannot be read the original string is returned along with an error. -func (c *Client) Read(value string) (string, error) { - p, k := parseName(value) - data, err := c.vault.Logical().Read(p) - if err != nil { - return value, err - } - if data == nil { - return value, fmt.Errorf("no such key %s", k) - } - secret, ok := data.Data[k] - if !ok { - return value, fmt.Errorf("no such key %s", k) - } - return secret.(string), nil -} - -// Delete deletes the secret from vault. -func (c *Client) Delete(value string) error { - p, _ := parseName(value) - _, err := c.vault.Logical().Delete(p) - return err -} diff --git a/server/server.go b/server/server.go index 2a6af15..d9cdf3a 100644 --- a/server/server.go +++ b/server/server.go @@ -2,7 +2,6 @@ package server import ( "bytes" - "crypto/tls" "encoding/base64" "encoding/json" "fmt" @@ -12,8 +11,6 @@ import ( "os" "time" - "github.com/gorilla/csrf" - "github.com/gobuffalo/packr" "github.com/gorilla/handlers" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -22,36 +19,16 @@ import ( "github.com/gorilla/sessions" "github.com/pkg/errors" - "go4.org/wkfs" - "golang.org/x/crypto/acme/autocert" "golang.org/x/oauth2" - wkfscache "github.com/nsheridan/autocert-wkfs-cache" "github.com/nsheridan/cashier/lib" - "github.com/nsheridan/cashier/server/auth" "github.com/nsheridan/cashier/server/auth/github" - "github.com/nsheridan/cashier/server/auth/gitlab" - "github.com/nsheridan/cashier/server/auth/google" - "github.com/nsheridan/cashier/server/auth/microsoft" "github.com/nsheridan/cashier/server/config" "github.com/nsheridan/cashier/server/metrics" "github.com/nsheridan/cashier/server/signer" - "github.com/nsheridan/cashier/server/store" "github.com/sid77/drop" ) -func loadCerts(certFile, keyFile string) (tls.Certificate, error) { - key, err := wkfs.ReadFile(keyFile) - if err != nil { - return tls.Certificate{}, errors.Wrap(err, "error reading TLS private key") - } - cert, err := wkfs.ReadFile(certFile) - if err != nil { - return tls.Certificate{}, errors.Wrap(err, "error reading TLS certificate") - } - return tls.X509KeyPair(cert, key) -} - // Run the server. func Run(conf *config.Config) { var err error @@ -62,30 +39,6 @@ func Run(conf *config.Config) { log.Fatal(errors.Wrapf(err, "unable to listen on %s:%d", conf.Server.Addr, conf.Server.Port)) } - tlsConfig := &tls.Config{} - if conf.Server.UseTLS { - if conf.Server.LetsEncryptServername != "" { - m := autocert.Manager{ - Prompt: autocert.AcceptTOS, - HostPolicy: autocert.HostWhitelist(conf.Server.LetsEncryptServername), - } - if conf.Server.LetsEncryptCache != "" { - m.Cache = wkfscache.Cache(conf.Server.LetsEncryptCache) - } - tlsConfig = m.TLSConfig() - } else { - if conf.Server.TLSCert == "" || conf.Server.TLSKey == "" { - log.Fatal("TLS cert or key not specified in config") - } - tlsConfig.Certificates = make([]tls.Certificate, 1) - tlsConfig.Certificates[0], err = loadCerts(conf.Server.TLSCert, conf.Server.TLSKey) - if err != nil { - log.Fatal(errors.Wrap(err, "unable to create TLS listener")) - } - } - l = tls.NewListener(l, tlsConfig) - } - if conf.Server.User != "" { log.Print("Dropping privileges...") if err := drop.DropPrivileges(conf.Server.User); err != nil { @@ -96,21 +49,9 @@ func Run(conf *config.Config) { // Unprivileged section metrics.Register() - var authprovider auth.Provider - switch conf.Auth.Provider { - case "github": - authprovider, err = github.New(conf.Auth) - case "gitlab": - authprovider, err = gitlab.New(conf.Auth) - case "google": - authprovider, err = google.New(conf.Auth) - case "microsoft": - authprovider, err = microsoft.New(conf.Auth) - default: - log.Fatalf("Unknown provider %s\n", conf.Auth.Provider) - } + authprovider, err := github.New(conf.Github) if err != nil { - log.Fatal(errors.Wrapf(err, "unable to use provider '%s'", conf.Auth.Provider)) + log.Fatal(errors.Wrap(err, "unable to setup github auth provider")) } keysigner, err := signer.New(conf.SSH) @@ -118,24 +59,17 @@ func Run(conf *config.Config) { log.Fatal(err) } - certstore, err := store.New(conf.Server.Database) - if err != nil { - log.Fatal(err) - } - ctx := &app{ - cookiestore: sessions.NewCookieStore([]byte(conf.Server.CookieSecret)), - requireReason: conf.Server.RequireReason, - keysigner: keysigner, - certstore: certstore, - authprovider: authprovider, - config: conf.Server, - router: mux.NewRouter(), + cookiestore: sessions.NewCookieStore([]byte(conf.Server.CookieSecret)), + keysigner: keysigner, + authprovider: authprovider, + config: conf.Server, + router: mux.NewRouter(), } ctx.cookiestore.Options = &sessions.Options{ MaxAge: 900, Path: "/", - Secure: conf.Server.UseTLS, + Secure: conf.Server.SecureCookie, HttpOnly: true, } @@ -190,30 +124,23 @@ func encodeString(s string) string { // app contains local context - cookiestore, authsession etc. type app struct { - cookiestore *sessions.CookieStore - authprovider auth.Provider - certstore store.CertStorer - keysigner *signer.KeySigner - router *mux.Router - config *config.Server - requireReason bool + cookiestore *sessions.CookieStore + authprovider *github.Config + keysigner *signer.KeySigner + router *mux.Router + config *config.Server } func (a *app) routes() { // login required - csrfHandler := csrf.Protect([]byte(a.config.CSRFSecret), csrf.Secure(a.config.UseTLS)) a.router.Methods("GET").Path("/").Handler(a.authed(http.HandlerFunc(a.index))) - a.router.Methods("POST").Path("/admin/revoke").Handler(a.authed(csrfHandler(http.HandlerFunc(a.revoke)))) - a.router.Methods("GET").Path("/admin/certs").Handler(a.authed(csrfHandler(http.HandlerFunc(a.getAllCerts)))) - a.router.Methods("GET").Path("/admin/certs.json").Handler(a.authed(http.HandlerFunc(a.getCertsJSON))) // no login required a.router.Methods("GET").Path("/auth/login").HandlerFunc(a.auth) a.router.Methods("GET").Path("/auth/callback").HandlerFunc(a.auth) - a.router.Methods("GET").Path("/revoked").HandlerFunc(a.revoked) a.router.Methods("POST").Path("/sign").HandlerFunc(a.sign) - a.router.Methods("GET").Path("/healthcheck").HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + a.router.Methods("GET").Path("/health").HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "ok") }) diff --git a/server/signer/signer.go b/server/signer/signer.go index 2a15849..e4ed789 100644 --- a/server/signer/signer.go +++ b/server/signer/signer.go @@ -8,12 +8,9 @@ import ( "time" "go4.org/wkfs" - _ "go4.org/wkfs/gcs" // Register "/gcs/" as a wkfs. "github.com/nsheridan/cashier/lib" "github.com/nsheridan/cashier/server/config" - "github.com/nsheridan/cashier/server/store" - "github.com/stripe/krl" "golang.org/x/crypto/ssh" ) @@ -78,22 +75,6 @@ func (s *KeySigner) SignUserKey(req *lib.SignRequest, username string) (*ssh.Cer return cert, nil } -// GenerateRevocationList returns an SSH key revocation list (KRL). -func (s *KeySigner) GenerateRevocationList(certs []*store.CertRecord) ([]byte, error) { - revoked := &krl.KRLCertificateSection{ - CA: s.ca.PublicKey(), - } - ids := krl.KRLCertificateKeyID{} - for _, c := range certs { - ids = append(ids, c.KeyID) - } - revoked.Sections = append(revoked.Sections, &ids) - k := &krl.KRL{ - Sections: []krl.KRLSection{revoked}, - } - return k.Marshal(rand.Reader) -} - // New creates a new KeySigner from the supplied configuration. func New(conf *config.SSH) (*KeySigner, error) { data, err := wkfs.ReadFile(conf.SigningKey) diff --git a/server/static/css/normalize.css b/server/static/css/normalize.css deleted file mode 100644 index 81c6f31..0000000 --- a/server/static/css/normalize.css +++ /dev/null @@ -1,427 +0,0 @@ -/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ - -/** - * 1. Set default font family to sans-serif. - * 2. Prevent iOS text size adjust after orientation change, without disabling - * user zoom. - */ - -html { - font-family: sans-serif; /* 1 */ - -ms-text-size-adjust: 100%; /* 2 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/** - * Remove default margin. - */ - -body { - margin: 0; -} - -/* HTML5 display definitions - ========================================================================== */ - -/** - * Correct `block` display not defined for any HTML5 element in IE 8/9. - * Correct `block` display not defined for `details` or `summary` in IE 10/11 - * and Firefox. - * Correct `block` display not defined for `main` in IE 11. - */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} - -/** - * 1. Correct `inline-block` display not defined in IE 8/9. - * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. - */ - -audio, -canvas, -progress, -video { - display: inline-block; /* 1 */ - vertical-align: baseline; /* 2 */ -} - -/** - * Prevent modern browsers from displaying `audio` without controls. - * Remove excess height in iOS 5 devices. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Address `[hidden]` styling not present in IE 8/9/10. - * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. - */ - -[hidden], -template { - display: none; -} - -/* Links - ========================================================================== */ - -/** - * Remove the gray background color from active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * Improve readability when focused and also mouse hovered in all browsers. - */ - -a:active, -a:hover { - outline: 0; -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Address styling not present in IE 8/9/10/11, Safari, and Chrome. - */ - -abbr[title] { - border-bottom: 1px dotted; -} - -/** - * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. - */ - -b, -strong { - font-weight: bold; -} - -/** - * Address styling not present in Safari and Chrome. - */ - -dfn { - font-style: italic; -} - -/** - * Address variable `h1` font-size and margin within `section` and `article` - * contexts in Firefox 4+, Safari, and Chrome. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/** - * Address styling not present in IE 8/9. - */ - -mark { - background: #ff0; - color: #000; -} - -/** - * Address inconsistent and variable font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` affecting `line-height` in all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove border when inside `a` element in IE 8/9/10. - */ - -img { - border: 0; -} - -/** - * Correct overflow not hidden in IE 9/10/11. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Grouping content - ========================================================================== */ - -/** - * Address margin not present in IE 8/9 and Safari. - */ - -figure { - margin: 1em 40px; -} - -/** - * Address differences between Firefox and other browsers. - */ - -hr { - -moz-box-sizing: content-box; - box-sizing: content-box; - height: 0; -} - -/** - * Contain overflow in all browsers. - */ - -pre { - overflow: auto; -} - -/** - * Address odd `em`-unit font size rendering in all browsers. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} - -/* Forms - ========================================================================== */ - -/** - * Known limitation: by default, Chrome and Safari on OS X allow very limited - * styling of `select`, unless a `border` property is set. - */ - -/** - * 1. Correct color not being inherited. - * Known issue: affects color of disabled elements. - * 2. Correct font properties not being inherited. - * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. - */ - -button, -input, -optgroup, -select, -textarea { - color: inherit; /* 1 */ - font: inherit; /* 2 */ - margin: 0; /* 3 */ -} - -/** - * Address `overflow` set to `hidden` in IE 8/9/10/11. - */ - -button { - overflow: visible; -} - -/** - * Address inconsistent `text-transform` inheritance for `button` and `select`. - * All other form control elements do not inherit `text-transform` values. - * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. - * Correct `select` style inheritance in Firefox. - */ - -button, -select { - text-transform: none; -} - -/** - * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` - * and `video` controls. - * 2. Correct inability to style clickable `input` types in iOS. - * 3. Improve usability and consistency of cursor style between image-type - * `input` and others. - */ - -button, -html input[type="button"], /* 1 */ -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; /* 2 */ - cursor: pointer; /* 3 */ -} - -/** - * Re-set default cursor for disabled elements. - */ - -button[disabled], -html input[disabled] { - cursor: default; -} - -/** - * Remove inner padding and border in Firefox 4+. - */ - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/** - * Address Firefox 4+ setting `line-height` on `input` using `!important` in - * the UA stylesheet. - */ - -input { - line-height: normal; -} - -/** - * It's recommended that you don't attempt to style these elements. - * Firefox's implementation doesn't respect box-sizing, padding, or width. - * - * 1. Address box sizing set to `content-box` in IE 8/9/10. - * 2. Remove excess padding in IE 8/9/10. - */ - -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Fix the cursor style for Chrome's increment/decrement buttons. For certain - * `font-size` values of the `input`, it causes the cursor style of the - * decrement button to change from `default` to `text`. - */ - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Address `appearance` set to `searchfield` in Safari and Chrome. - * 2. Address `box-sizing` set to `border-box` in Safari and Chrome - * (include `-moz` to future-proof). - */ - -input[type="search"] { - -webkit-appearance: textfield; /* 1 */ - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; /* 2 */ - box-sizing: content-box; -} - -/** - * Remove inner padding and search cancel button in Safari and Chrome on OS X. - * Safari (but not Chrome) clips the cancel button when the search input has - * padding (and `textfield` appearance). - */ - -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * Define consistent border, margin, and padding. - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** - * 1. Correct `color` not being inherited in IE 8/9/10/11. - * 2. Remove padding so people aren't caught out if they zero out fieldsets. - */ - -legend { - border: 0; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Remove default vertical scrollbar in IE 8/9/10/11. - */ - -textarea { - overflow: auto; -} - -/** - * Don't inherit the `font-weight` (applied by a rule above). - * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. - */ - -optgroup { - font-weight: bold; -} - -/* Tables - ========================================================================== */ - -/** - * Remove most spacing between table cells. - */ - -table { - border-collapse: collapse; - border-spacing: 0; -} - -td, -th { - padding: 0; -}
\ No newline at end of file diff --git a/server/static/css/skeleton.css b/server/static/css/skeleton.css deleted file mode 100644 index 43a95e4..0000000 --- a/server/static/css/skeleton.css +++ /dev/null @@ -1,418 +0,0 @@ -/* -* Skeleton V2.0.4 -* Copyright 2014, Dave Gamache -* www.getskeleton.com -* Free to use under the MIT license. -* http://www.opensource.org/licenses/mit-license.php -* 12/29/2014 -*/ - - -/* Table of contents -–––––––––––––––––––––––––––––––––––––––––––––––––– -- Grid -- Base Styles -- Typography -- Links -- Buttons -- Forms -- Lists -- Code -- Tables -- Spacing -- Utilities -- Clearing -- Media Queries -*/ - - -/* Grid -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.container { - position: relative; - width: 100%; - max-width: 960px; - margin: 0 auto; - padding: 0 20px; - box-sizing: border-box; } -.column, -.columns { - width: 100%; - float: left; - box-sizing: border-box; } - -/* For devices larger than 400px */ -@media (min-width: 400px) { - .container { - width: 85%; - padding: 0; } -} - -/* For devices larger than 550px */ -@media (min-width: 550px) { - .container { - width: 80%; } - .column, - .columns { - margin-left: 4%; } - .column:first-child, - .columns:first-child { - margin-left: 0; } - - .one.column, - .one.columns { width: 4.66666666667%; } - .two.columns { width: 13.3333333333%; } - .three.columns { width: 22%; } - .four.columns { width: 30.6666666667%; } - .five.columns { width: 39.3333333333%; } - .six.columns { width: 48%; } - .seven.columns { width: 56.6666666667%; } - .eight.columns { width: 65.3333333333%; } - .nine.columns { width: 74.0%; } - .ten.columns { width: 82.6666666667%; } - .eleven.columns { width: 91.3333333333%; } - .twelve.columns { width: 100%; margin-left: 0; } - - .one-third.column { width: 30.6666666667%; } - .two-thirds.column { width: 65.3333333333%; } - - .one-half.column { width: 48%; } - - /* Offsets */ - .offset-by-one.column, - .offset-by-one.columns { margin-left: 8.66666666667%; } - .offset-by-two.column, - .offset-by-two.columns { margin-left: 17.3333333333%; } - .offset-by-three.column, - .offset-by-three.columns { margin-left: 26%; } - .offset-by-four.column, - .offset-by-four.columns { margin-left: 34.6666666667%; } - .offset-by-five.column, - .offset-by-five.columns { margin-left: 43.3333333333%; } - .offset-by-six.column, - .offset-by-six.columns { margin-left: 52%; } - .offset-by-seven.column, - .offset-by-seven.columns { margin-left: 60.6666666667%; } - .offset-by-eight.column, - .offset-by-eight.columns { margin-left: 69.3333333333%; } - .offset-by-nine.column, - .offset-by-nine.columns { margin-left: 78.0%; } - .offset-by-ten.column, - .offset-by-ten.columns { margin-left: 86.6666666667%; } - .offset-by-eleven.column, - .offset-by-eleven.columns { margin-left: 95.3333333333%; } - - .offset-by-one-third.column, - .offset-by-one-third.columns { margin-left: 34.6666666667%; } - .offset-by-two-thirds.column, - .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } - - .offset-by-one-half.column, - .offset-by-one-half.columns { margin-left: 52%; } - -} - - -/* Base Styles -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -/* NOTE -html is set to 62.5% so that all the REM measurements throughout Skeleton -are based on 10px sizing. So basically 1.5rem = 15px :) */ -html { - font-size: 62.5%; } -body { - font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ - line-height: 1.6; - font-weight: 400; - font-family: "Source Sans Pro", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; - color: #222; } - - -/* Typography -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -h1, h2, h3, h4, h5, h6 { - margin-top: 0; - margin-bottom: 2rem; - font-weight: 300; } -h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} -h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } -h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } -h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } -h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } -h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } - -/* Larger than phablet */ -@media (min-width: 550px) { - h1 { font-size: 5.0rem; } - h2 { font-size: 4.2rem; } - h3 { font-size: 3.6rem; } - h4 { font-size: 3.0rem; } - h5 { font-size: 2.4rem; } - h6 { font-size: 1.5rem; } -} - -p { - margin-top: 0; } - - -/* Links -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -a { - color: #1EAEDB; } -a:hover { - color: #0FA0CE; } - - -/* Buttons -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.button, -button, -input[type="submit"], -input[type="reset"], -input[type="button"] { - display: inline-block; - height: 38px; - padding: 0 30px; - color: #555; - text-align: center; - font-size: 11px; - font-weight: 600; - line-height: 38px; - letter-spacing: .1rem; - text-transform: uppercase; - text-decoration: none; - white-space: nowrap; - background-color: transparent; - border-radius: 4px; - border: 1px solid #bbb; - cursor: pointer; - box-sizing: border-box; } -.button:hover, -button:hover, -input[type="submit"]:hover, -input[type="reset"]:hover, -input[type="button"]:hover, -.button:focus, -button:focus, -input[type="submit"]:focus, -input[type="reset"]:focus, -input[type="button"]:focus { - color: #333; - border-color: #888; - outline: 0; } -.button.button-primary, -button.button-primary, -input[type="submit"].button-primary, -input[type="reset"].button-primary, -input[type="button"].button-primary { - color: #FFF; - background-color: #33C3F0; - border-color: #33C3F0; } -.button.button-primary:hover, -button.button-primary:hover, -input[type="submit"].button-primary:hover, -input[type="reset"].button-primary:hover, -input[type="button"].button-primary:hover, -.button.button-primary:focus, -button.button-primary:focus, -input[type="submit"].button-primary:focus, -input[type="reset"].button-primary:focus, -input[type="button"].button-primary:focus { - color: #FFF; - background-color: #1EAEDB; - border-color: #1EAEDB; } - - -/* Forms -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -input[type="email"], -input[type="number"], -input[type="search"], -input[type="text"], -input[type="tel"], -input[type="url"], -input[type="password"], -textarea, -select { - height: 38px; - padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ - background-color: #fff; - border: 1px solid #D1D1D1; - border-radius: 4px; - box-shadow: none; - box-sizing: border-box; } -/* Removes awkward default styles on some inputs for iOS */ -input[type="email"], -input[type="number"], -input[type="search"], -input[type="text"], -input[type="tel"], -input[type="url"], -input[type="password"], -textarea { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; } -textarea { - min-height: 65px; - padding-top: 6px; - padding-bottom: 6px; } -input[type="email"]:focus, -input[type="number"]:focus, -input[type="search"]:focus, -input[type="text"]:focus, -input[type="tel"]:focus, -input[type="url"]:focus, -input[type="password"]:focus, -textarea:focus, -select:focus { - border: 1px solid #33C3F0; - outline: 0; } -label, -legend { - display: block; - margin-bottom: .5rem; - font-weight: 600; } -fieldset { - padding: 0; - border-width: 0; } -input[type="checkbox"], -input[type="radio"] { - display: inline; } -label > .label-body { - display: inline-block; - margin-left: .5rem; - font-weight: normal; } - - -/* Lists -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -ul { - list-style: circle inside; } -ol { - list-style: decimal inside; } -ol, ul { - padding-left: 0; - margin-top: 0; } -ul ul, -ul ol, -ol ol, -ol ul { - margin: 1.5rem 0 1.5rem 3rem; - font-size: 90%; } -li { - margin-bottom: 1rem; } - - -/* Code -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -code { - padding: .2rem .5rem; - margin: 0 .2rem; - font-size: 90%; - white-space: nowrap; - background: #F1F1F1; - border: 1px solid #E1E1E1; - border-radius: 4px; } -pre > code { - display: block; - padding: 1rem 1.5rem; - white-space: pre; } - - -/* Tables -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -th, -td { - padding: 6px 7px; - text-align: left; - border-bottom: 1px solid #E1E1E1; } -th:first-child, -td:first-child { - padding-left: 0; } -th:last-child, -td:last-child { - padding-right: 0; } - - -/* Spacing -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -button, -.button { - margin-bottom: 1rem; } -input, -textarea, -select, -fieldset { - margin-bottom: 1.5rem; } -pre, -blockquote, -dl, -figure, -table, -p, -ul, -ol, -form { - margin-bottom: 2.5rem; } - - -/* Utilities -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -.u-full-width { - width: 100%; - box-sizing: border-box; } -.u-max-full-width { - max-width: 100%; - box-sizing: border-box; } -.u-pull-right { - float: right; } -.u-pull-left { - float: left; } - - -/* Misc -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -hr { - margin-top: 3rem; - margin-bottom: 3.5rem; - border-width: 0; - border-top: 1px solid #E1E1E1; } - - -/* Clearing -–––––––––––––––––––––––––––––––––––––––––––––––––– */ - -/* Self Clearing Goodness */ -.container:after, -.row:after, -.u-cf { - content: ""; - display: table; - clear: both; } - - -/* Media Queries -–––––––––––––––––––––––––––––––––––––––––––––––––– */ -/* -Note: The best way to structure the use of media queries is to create the queries -near the relevant code. For example, if you wanted to change the styles for buttons -on small devices, paste the mobile query code up in the buttons section and style it -there. -*/ - - -/* Larger than mobile */ -@media (min-width: 400px) {} - -/* Larger than phablet (also point when grid becomes active) */ -@media (min-width: 550px) {} - -/* Larger than tablet */ -@media (min-width: 750px) {} - -/* Larger than desktop */ -@media (min-width: 1000px) {} - -/* Larger than Desktop HD */ -@media (min-width: 1200px) {} diff --git a/server/static/js/list.min.js b/server/static/js/list.min.js deleted file mode 100644 index ff939b4..0000000 --- a/server/static/js/list.min.js +++ /dev/null @@ -1 +0,0 @@ -!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<d.length;g++)e(d[g]);return e}({1:[function(a,b,c){!function(c,d){"use strict";var e=c.document,f=a("./src/utils/get-by-class"),g=a("./src/utils/extend"),h=a("./src/utils/index-of"),i=a("./src/utils/events"),j=a("./src/utils/to-string"),k=a("./src/utils/natural-sort"),l=a("./src/utils/classes"),m=a("./src/utils/get-attribute"),n=a("./src/utils/to-array"),o=function(b,c,p){var q,r=this,s=a("./src/item")(r),t=a("./src/add-async")(r);q={start:function(){r.listClass="list",r.searchClass="search",r.sortClass="sort",r.page=1e4,r.i=1,r.items=[],r.visibleItems=[],r.matchingItems=[],r.searched=!1,r.filtered=!1,r.searchColumns=d,r.handlers={updated:[]},r.plugins={},r.valueNames=[],r.utils={getByClass:f,extend:g,indexOf:h,events:i,toString:j,naturalSort:k,classes:l,getAttribute:m,toArray:n},r.utils.extend(r,c),r.listContainer="string"==typeof b?e.getElementById(b):b,r.listContainer&&(r.list=f(r.listContainer,r.listClass,!0),r.parse=a("./src/parse")(r),r.templater=a("./src/templater")(r),r.search=a("./src/search")(r),r.filter=a("./src/filter")(r),r.sort=a("./src/sort")(r),this.handlers(),this.items(),r.update(),this.plugins())},handlers:function(){for(var a in r.handlers)r[a]&&r.on(a,r[a])},items:function(){r.parse(r.list),p!==d&&r.add(p)},plugins:function(){for(var a=0;a<r.plugins.length;a++){var b=r.plugins[a];r[b.name]=b,b.init(r,o)}}},this.reIndex=function(){r.items=[],r.visibleItems=[],r.matchingItems=[],r.searched=!1,r.filtered=!1,r.parse(r.list)},this.toJSON=function(){for(var a=[],b=0,c=r.items.length;c>b;b++)a.push(r.items[b].values());return a},this.add=function(a,b){if(0!==a.length){if(b)return void t(a,b);var c=[],e=!1;a[0]===d&&(a=[a]);for(var f=0,g=a.length;g>f;f++){var h=null;e=r.items.length>r.page?!0:!1,h=new s(a[f],d,e),r.items.push(h),c.push(h)}return r.update(),c}},this.show=function(a,b){return this.i=a,this.page=b,r.update(),r},this.remove=function(a,b,c){for(var d=0,e=0,f=r.items.length;f>e;e++)r.items[e].values()[a]==b&&(r.templater.remove(r.items[e],c),r.items.splice(e,1),f--,e--,d++);return r.update(),d},this.get=function(a,b){for(var c=[],d=0,e=r.items.length;e>d;d++){var f=r.items[d];f.values()[a]==b&&c.push(f)}return c},this.size=function(){return r.items.length},this.clear=function(){return r.templater.clear(),r.items=[],r},this.on=function(a,b){return r.handlers[a].push(b),r},this.off=function(a,b){var c=r.handlers[a],d=h(c,b);return d>-1&&c.splice(d,1),r},this.trigger=function(a){for(var b=r.handlers[a].length;b--;)r.handlers[a][b](r);return r},this.reset={filter:function(){for(var a=r.items,b=a.length;b--;)a[b].filtered=!1;return r},search:function(){for(var a=r.items,b=a.length;b--;)a[b].found=!1;return r}},this.update=function(){var a=r.items,b=a.length;r.visibleItems=[],r.matchingItems=[],r.templater.clear();for(var c=0;b>c;c++)a[c].matching()&&r.matchingItems.length+1>=r.i&&r.visibleItems.length<r.page?(a[c].show(),r.visibleItems.push(a[c]),r.matchingItems.push(a[c])):a[c].matching()?(r.matchingItems.push(a[c]),a[c].hide()):a[c].hide();return r.trigger("updated"),r},q.start()};"function"==typeof define&&define.amd&&define(function(){return o}),b.exports=o,c.List=o}(window)},{"./src/add-async":2,"./src/filter":3,"./src/item":4,"./src/parse":5,"./src/search":6,"./src/sort":7,"./src/templater":8,"./src/utils/classes":9,"./src/utils/events":10,"./src/utils/extend":11,"./src/utils/get-attribute":12,"./src/utils/get-by-class":13,"./src/utils/index-of":14,"./src/utils/natural-sort":15,"./src/utils/to-array":16,"./src/utils/to-string":17}],2:[function(a,b,c){b.exports=function(a){var b=function(c,d,e){var f=c.splice(0,50);e=e||[],e=e.concat(a.add(f)),c.length>0?setTimeout(function(){b(c,d,e)},1):(a.update(),d(e))};return b}},{}],3:[function(a,b,c){b.exports=function(a){return a.handlers.filterStart=a.handlers.filterStart||[],a.handlers.filterComplete=a.handlers.filterComplete||[],function(b){if(a.trigger("filterStart"),a.i=1,a.reset.filter(),void 0===b)a.filtered=!1;else{a.filtered=!0;for(var c=a.items,d=0,e=c.length;e>d;d++){var f=c[d];b(f)?f.filtered=!0:f.filtered=!1}}return a.update(),a.trigger("filterComplete"),a.visibleItems}}},{}],4:[function(a,b,c){b.exports=function(a){return function(b,c,d){var e=this;this._values={},this.found=!1,this.filtered=!1;var f=function(b,c,d){if(void 0===c)d?e.values(b,d):e.values(b);else{e.elm=c;var f=a.templater.get(e,b);e.values(f)}};this.values=function(b,c){if(void 0===b)return e._values;for(var d in b)e._values[d]=b[d];c!==!0&&a.templater.set(e,e.values())},this.show=function(){a.templater.show(e)},this.hide=function(){a.templater.hide(e)},this.matching=function(){return a.filtered&&a.searched&&e.found&&e.filtered||a.filtered&&!a.searched&&e.filtered||!a.filtered&&a.searched&&e.found||!a.filtered&&!a.searched},this.visible=function(){return e.elm&&e.elm.parentNode==a.list?!0:!1},f(b,c,d)}}},{}],5:[function(a,b,c){b.exports=function(b){var c=a("./item")(b),d=function(a){for(var b=a.childNodes,c=[],d=0,e=b.length;e>d;d++)void 0===b[d].data&&c.push(b[d]);return c},e=function(a,d){for(var e=0,f=a.length;f>e;e++)b.items.push(new c(d,a[e]))},f=function(a,c){var d=a.splice(0,50);e(d,c),a.length>0?setTimeout(function(){f(a,c)},1):(b.update(),b.trigger("parseComplete"))};return b.handlers.parseComplete=b.handlers.parseComplete||[],function(){var a=d(b.list),c=b.valueNames;b.indexAsync?f(a,c):e(a,c)}}},{"./item":4}],6:[function(a,b,c){b.exports=function(a){var b,c,d,e,f={resetList:function(){a.i=1,a.templater.clear(),e=void 0},setOptions:function(a){2==a.length&&a[1]instanceof Array?c=a[1]:2==a.length&&"function"==typeof a[1]?e=a[1]:3==a.length&&(c=a[1],e=a[2])},setColumns:function(){0!==a.items.length&&void 0===c&&(c=void 0===a.searchColumns?f.toArray(a.items[0].values()):a.searchColumns)},setSearchString:function(b){b=a.utils.toString(b).toLowerCase(),b=b.replace(/[-[\]{}()*+?.,\\^$|#]/g,"\\$&"),d=b},toArray:function(a){var b=[];for(var c in a)b.push(c);return b}},g={list:function(){for(var b=0,c=a.items.length;c>b;b++)g.item(a.items[b])},item:function(a){a.found=!1;for(var b=0,d=c.length;d>b;b++)if(g.values(a.values(),c[b]))return void(a.found=!0)},values:function(c,e){return c.hasOwnProperty(e)&&(b=a.utils.toString(c[e]).toLowerCase(),""!==d&&b.search(d)>-1)?!0:!1},reset:function(){a.reset.search(),a.searched=!1}},h=function(b){return a.trigger("searchStart"),f.resetList(),f.setSearchString(b),f.setOptions(arguments),f.setColumns(),""===d?g.reset():(a.searched=!0,e?e(d,c):g.list()),a.update(),a.trigger("searchComplete"),a.visibleItems};return a.handlers.searchStart=a.handlers.searchStart||[],a.handlers.searchComplete=a.handlers.searchComplete||[],a.utils.events.bind(a.utils.getByClass(a.listContainer,a.searchClass),"keyup",function(b){var c=b.target||b.srcElement,d=""===c.value&&!a.searched;d||h(c.value)}),a.utils.events.bind(a.utils.getByClass(a.listContainer,a.searchClass),"input",function(a){var b=a.target||a.srcElement;""===b.value&&h("")}),h}},{}],7:[function(a,b,c){b.exports=function(a){a.sortFunction=a.sortFunction||function(b,c,d){return d.desc="desc"==d.order?!0:!1,a.utils.naturalSort(b.values()[d.valueName],c.values()[d.valueName],d)};var b={els:void 0,clear:function(){for(var c=0,d=b.els.length;d>c;c++)a.utils.classes(b.els[c]).remove("asc"),a.utils.classes(b.els[c]).remove("desc")},getOrder:function(b){var c=a.utils.getAttribute(b,"data-order");return"asc"==c||"desc"==c?c:a.utils.classes(b).has("desc")?"asc":a.utils.classes(b).has("asc")?"desc":"asc"},getInSensitive:function(b,c){var d=a.utils.getAttribute(b,"data-insensitive");"false"===d?c.insensitive=!1:c.insensitive=!0},setOrder:function(c){for(var d=0,e=b.els.length;e>d;d++){var f=b.els[d];if(a.utils.getAttribute(f,"data-sort")===c.valueName){var g=a.utils.getAttribute(f,"data-order");"asc"==g||"desc"==g?g==c.order&&a.utils.classes(f).add(c.order):a.utils.classes(f).add(c.order)}}}},c=function(){a.trigger("sortStart");var c={},d=arguments[0].currentTarget||arguments[0].srcElement||void 0;d?(c.valueName=a.utils.getAttribute(d,"data-sort"),b.getInSensitive(d,c),c.order=b.getOrder(d)):(c=arguments[1]||c,c.valueName=arguments[0],c.order=c.order||"asc",c.insensitive="undefined"==typeof c.insensitive?!0:c.insensitive),b.clear(),b.setOrder(c),c.sortFunction=c.sortFunction||a.sortFunction,a.items.sort(function(a,b){var d="desc"===c.order?-1:1;return c.sortFunction(a,b,c)*d}),a.update(),a.trigger("sortComplete")};return a.handlers.sortStart=a.handlers.sortStart||[],a.handlers.sortComplete=a.handlers.sortComplete||[],b.els=a.utils.getByClass(a.listContainer,a.sortClass),a.utils.events.bind(b.els,"click",c),a.on("searchStart",b.clear),a.on("filterStart",b.clear),c}},{}],8:[function(a,b,c){var d=function(a){var b,c=this,d=function(){b=c.getItemSource(a.item),b=c.clearSourceItem(b,a.valueNames)};this.clearSourceItem=function(b,c){for(var d=0,e=c.length;e>d;d++){var f;if(c[d].data)for(var g=0,h=c[d].data.length;h>g;g++)b.setAttribute("data-"+c[d].data[g],"");else c[d].attr&&c[d].name?(f=a.utils.getByClass(b,c[d].name,!0),f&&f.setAttribute(c[d].attr,"")):(f=a.utils.getByClass(b,c[d],!0),f&&(f.innerHTML=""));f=void 0}return b},this.getItemSource=function(b){if(void 0===b){for(var c=a.list.childNodes,d=0,e=c.length;e>d;d++)if(void 0===c[d].data)return c[d].cloneNode(!0)}else{if(/^tr[\s>]/.exec(b)){var f=document.createElement("table");return f.innerHTML=b,f.firstChild}if(-1!==b.indexOf("<")){var g=document.createElement("div");return g.innerHTML=b,g.firstChild}var h=document.getElementById(a.item);if(h)return h}throw new Error("The list need to have at list one item on init otherwise you'll have to add a template.")},this.get=function(b,d){c.create(b);for(var e={},f=0,g=d.length;g>f;f++){var h;if(d[f].data)for(var i=0,j=d[f].data.length;j>i;i++)e[d[f].data[i]]=a.utils.getAttribute(b.elm,"data-"+d[f].data[i]);else d[f].attr&&d[f].name?(h=a.utils.getByClass(b.elm,d[f].name,!0),e[d[f].name]=h?a.utils.getAttribute(h,d[f].attr):""):(h=a.utils.getByClass(b.elm,d[f],!0),e[d[f]]=h?h.innerHTML:"");h=void 0}return e},this.set=function(b,d){var e=function(b){for(var c=0,d=a.valueNames.length;d>c;c++)if(a.valueNames[c].data){for(var e=a.valueNames[c].data,f=0,g=e.length;g>f;f++)if(e[f]===b)return{data:b}}else{if(a.valueNames[c].attr&&a.valueNames[c].name&&a.valueNames[c].name==b)return a.valueNames[c];if(a.valueNames[c]===b)return b}},f=function(c,d){var f,g=e(c);g&&(g.data?b.elm.setAttribute("data-"+g.data,d):g.attr&&g.name?(f=a.utils.getByClass(b.elm,g.name,!0),f&&f.setAttribute(g.attr,d)):(f=a.utils.getByClass(b.elm,g,!0),f&&(f.innerHTML=d)),f=void 0)};if(!c.create(b))for(var g in d)d.hasOwnProperty(g)&&f(g,d[g])},this.create=function(a){if(void 0!==a.elm)return!1;var d=b.cloneNode(!0);return d.removeAttribute("id"),a.elm=d,c.set(a,a.values()),!0},this.remove=function(b){b.elm.parentNode===a.list&&a.list.removeChild(b.elm)},this.show=function(b){c.create(b),a.list.appendChild(b.elm)},this.hide=function(b){void 0!==b.elm&&b.elm.parentNode===a.list&&a.list.removeChild(b.elm)},this.clear=function(){if(a.list.hasChildNodes())for(;a.list.childNodes.length>=1;)a.list.removeChild(a.list.firstChild)},d()};b.exports=function(a){return new d(a)}},{}],9:[function(a,b,c){function d(a){if(!a||!a.nodeType)throw new Error("A DOM element reference is required");this.el=a,this.list=a.classList}var e=a("./index-of"),f=/\s+/,g=Object.prototype.toString;b.exports=function(a){return new d(a)},d.prototype.add=function(a){if(this.list)return this.list.add(a),this;var b=this.array(),c=e(b,a);return~c||b.push(a),this.el.className=b.join(" "),this},d.prototype.remove=function(a){if("[object RegExp]"==g.call(a))return this.removeMatching(a);if(this.list)return this.list.remove(a),this;var b=this.array(),c=e(b,a);return~c&&b.splice(c,1),this.el.className=b.join(" "),this},d.prototype.removeMatching=function(a){for(var b=this.array(),c=0;c<b.length;c++)a.test(b[c])&&this.remove(b[c]);return this},d.prototype.toggle=function(a,b){return this.list?("undefined"!=typeof b?b!==this.list.toggle(a,b)&&this.list.toggle(a):this.list.toggle(a),this):("undefined"!=typeof b?b?this.add(a):this.remove(a):this.has(a)?this.remove(a):this.add(a),this)},d.prototype.array=function(){var a=this.el.getAttribute("class")||"",b=a.replace(/^\s+|\s+$/g,""),c=b.split(f);return""===c[0]&&c.shift(),c},d.prototype.has=d.prototype.contains=function(a){return this.list?this.list.contains(a):!!~e(this.array(),a)}},{"./index-of":14}],10:[function(a,b,c){var d=window.addEventListener?"addEventListener":"attachEvent",e=window.removeEventListener?"removeEventListener":"detachEvent",f="addEventListener"!==d?"on":"",g=a("./to-array");c.bind=function(a,b,c,e){a=g(a);for(var h=0;h<a.length;h++)a[h][d](f+b,c,e||!1)},c.unbind=function(a,b,c,d){a=g(a);for(var h=0;h<a.length;h++)a[h][e](f+b,c,d||!1)}},{"./to-array":16}],11:[function(a,b,c){b.exports=function(a){for(var b,c=Array.prototype.slice.call(arguments,1),d=0;b=c[d];d++)if(b)for(var e in b)a[e]=b[e];return a}},{}],12:[function(a,b,c){b.exports=function(a,b){var c=a.getAttribute&&a.getAttribute(b)||null;if(!c)for(var d=a.attributes,e=d.length,f=0;e>f;f++)void 0!==b[f]&&b[f].nodeName===b&&(c=b[f].nodeValue);return c}},{}],13:[function(a,b,c){b.exports=function(){return document.getElementsByClassName?function(a,b,c){return c?a.getElementsByClassName(b)[0]:a.getElementsByClassName(b)}:document.querySelector?function(a,b,c){return b="."+b,c?a.querySelector(b):a.querySelectorAll(b)}:function(a,b,c){var d=[],e="*";null===a&&(a=document);for(var f=a.getElementsByTagName(e),g=f.length,h=new RegExp("(^|\\s)"+b+"(\\s|$)"),i=0,j=0;g>i;i++)if(h.test(f[i].className)){if(c)return f[i];d[j]=f[i],j++}return d}}()},{}],14:[function(a,b,c){var d=[].indexOf;b.exports=function(a,b){if(d)return a.indexOf(b);for(var c=0;c<a.length;++c)if(a[c]===b)return c;return-1}},{}],15:[function(a,b,c){b.exports=function(a,b,c){var d,e,f=/(^([+\-]?(?:\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[\da-fA-F]+$|\d+)/g,g=/^\s+|\s+$/g,h=/\s+/g,i=/(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,j=/^0x[0-9a-f]+$/i,k=/^0/,l=c||{},m=function(a){return l.insensitive&&(""+a).toLowerCase()||""+a},n=m(a)||"",o=m(b)||"",p=n.replace(f,"\x00$1\x00").replace(/\0$/,"").replace(/^\0/,"").split("\x00"),q=o.replace(f,"\x00$1\x00").replace(/\0$/,"").replace(/^\0/,"").split("\x00"),r=parseInt(n.match(j),16)||1!==p.length&&Date.parse(n),s=parseInt(o.match(j),16)||r&&o.match(i)&&Date.parse(o)||null,t=function(a,b){return(!a.match(k)||1==b)&&parseFloat(a)||a.replace(h," ").replace(g,"")||0};if(s){if(s>r)return-1;if(r>s)return 1}for(var u=0,v=p.length,w=q.length,x=Math.max(v,w);x>u;u++){if(d=t(p[u],v),e=t(q[u],w),isNaN(d)!==isNaN(e))return isNaN(d)?1:-1;if(typeof d!=typeof e&&(d+="",e+=""),e>d)return-1;if(d>e)return 1}return 0}},{}],16:[function(a,b,c){function d(a){return"[object Array]"===Object.prototype.toString.call(a)}b.exports=function(a){if("undefined"==typeof a)return[];if(null===a)return[null];if(a===window)return[window];if("string"==typeof a)return[a];if(d(a))return a;if("number"!=typeof a.length)return[a];if("function"==typeof a&&a instanceof Function)return[a];for(var b=[],c=0;c<a.length;c++)(Object.prototype.hasOwnProperty.call(a,c)||c in a)&&b.push(a[c]);return b.length?b:[]}},{}],17:[function(a,b,c){b.exports=function(a){return a=void 0===a?"":a,a=null===a?"":a,a=a.toString()}},{}]},{},[1]);
\ No newline at end of file diff --git a/server/static/js/table.js b/server/static/js/table.js deleted file mode 100644 index a2c0e20..0000000 --- a/server/static/js/table.js +++ /dev/null @@ -1,51 +0,0 @@ -function reqListener() { - var recs = JSON.parse(this.responseText); - var table = document.querySelector('#cert-table'); - var tbody = table.querySelector("#list"); - while (tbody.rows.length > 0) { - tbody.deleteRow(0); - } - issuedList.clear(); - recs.forEach(function makeTable(el, i, arr) { - var row = tbody.insertRow(-1); - row.insertCell(0).innerHTML = el.key_id; - row.insertCell(1).innerHTML = el.created_at; - row.insertCell(2).innerHTML = el.expires; - row.insertCell(3).innerHTML = el.principals; - row.insertCell(4).innerHTML = el.message; - row.insertCell(5).innerHTML = el.revoked; - // Index keyid and principals. - row.cells[0].classList = ["keyid"]; - row.cells[3].classList = ["principals"]; - row.insertCell(6) - if (!el.revoked) { - row.cells[6].innerHTML = '<input style="margin:0;" type="checkbox" value="'+ el.key_id + '" name="cert_id" id="cert_id" />'; - } - tbody.appendChild(row); - }); - issuedList.reIndex(); -} - -function loadCerts(all) { - var r = new XMLHttpRequest(); - var endpoint = '/admin/certs.json'; - if (all) { - endpoint += '?all=true'; - } - r.open('GET', endpoint); - r.addEventListener('load', reqListener); - r.send() -} - -var SHOW_ALL = false; - -function toggleExpired() { - var button = document.querySelector("#toggle-certs"); - SHOW_ALL = !SHOW_ALL; - loadCerts(SHOW_ALL); - if (SHOW_ALL == false) { - button.innerHTML = "Show Expired"; - } else { - button.innerHTML = "Hide Expired"; - } -} diff --git a/server/store/a_store-packr.go b/server/store/a_store-packr.go deleted file mode 100644 index 7b63e4b..0000000 --- a/server/store/a_store-packr.go +++ /dev/null @@ -1,19 +0,0 @@ -// Code generated by github.com/gobuffalo/packr. DO NOT EDIT. - -package store - -import "github.com/gobuffalo/packr" - -// You can use the "packr clean" command to clean up this, -// and any other packr generated files. -func init() { - packr.PackJSONBytes("migrations", "migrations_test.go", "\"H4sIAAAAAAAA/6xW32/bNhB+lv6KA4ENUqfSBbanDH5IY7frlh9t5BQbgsClpZNNRCIVknJqFPnfhyNlW07TdAX2kkjU/fjuu++ObkVxK5YIjVwa4aRWNo5l02rjIIkjVgonFsLiyN7VLI5Y1Tj6J/VI6s5Jf9YItxoZoUp60Tb8HXUWDT22wq22/0eVrHF7YLCqsfDhHFon1ZLFccSW0q26BS90M1rql/auflkauUYzajYBwxyGNo1wTvWW0uGvLI5CKXhgZroFqjVV8bL/zA5zWWfQFSsz8liqzUhYi8Z9z8rgXScNsjiN46pTBczQuvzDqXR4tmM0cfCiL5HPUvgSR7ZblNLA0RjYHne5yACNP7V3Nb9oUSXBMAN21GCjzeaIpXHUJ+XnemqMNonzfhmwKyUWNYLToFtUEEJDoZXCgpB4504NkWVAaUOalDDwk1pbTNL4YVDR2Sb/cPpcQbICbflbdKjWCTv7J/9wOp9N8xlLYTwGxsgmcjy/lW3CzjXsLQDVWhqtGlQO1sJIKoGAPsSR52JXLd4nLj3gbiuJ0ip6969kd6JVJZdJ6r/wc3QwBuaKNpjy98JYnMkGYQzOdBhOj8vSwPjpMuZ/XFAtW3dr78tvmr4/znMy7TKYEyoaBH7SGYPKESRZAXF3ZdFkoG/JRFt+qvVt104fxbrKp5cs/Z3MiEDKTn6Euw8RRw+AtcXH3zv/oESDnsnntEVeb7RphJvk50lKGtlJ60ldeZ4PZRVHRpW+B+IWk+ubxcZhBr9RLKFKfomiTIwqffuqSn4m06pxPG+NVK5K2PynzywD64xUS2+ZxtE8YB5DueDTz1gk7ORyejybwuR4dvz6OJ8OttacCGHwC4T4j4t4MtrVD0b47uiEsJPLi/d7iO/ewPTvd/ksfzbVE3P3KNtg4igxvKA+Tl5nUEqDhdNm07Pn51EQwWGF7QbnyWlq6PDnfifyN7Le761cd6ZA0tVEmqN9nswrila53Ylqu9j521ovEv/0p5Yq2TsBe8FpWqmxoxF8RCOrDbiVcOBWaBCkBeGgRmEdaDW4kUh3plNfy/JcDzjtrVjfNzdtWrdJPMqvM/ZuagldC/fa3No4Urtitmz4dlKPB1U02e7zVRumWRzIxA+i4Keokp4iFZbZIwiClGPxrqO9ZzoFK2FBDSvaQfp/EE3vOlEnr76Fx+AajSVKBpweUvOfcUz0vfoRbh7ieDTyF03PTV0P2r/NIdHS0nFCKpIMWNEgWHSgqyFmH5kP7i659F8mW6gnWjlU7olbjNalDVusvQ7TdHN9Ex6+PMRR0XvulBJ+BPn1NpEmYZzktx27g9s5jaNKG5hnIP0YCrX0N7OP53mRFUj+zlKcQFTUX3P7aZL8XDSYpPthIrOeyvnXcxgWUxQ5I5sG/X4elhNtIVV7SD5YSL9zG4NoW1Rl0h9k+zSvhcWkSn0WH9FTeL0FeuNvWO8Ue4OguyuLfsRRObOByujGN/STd/5E7IOwIMBghQZVgUBASRVkpmlfWE4ryFhH0EPS/gcl/yjqDi+qxJ+m/Ey0f+HGJun1qxuehwsmvdm1Y72vPbTfazS00I+MD+fXvc+XwbrX7L8BAAD//4C0Bmk7CwAA\"") - packr.PackJSONBytes("migrations", "mysql/20180626224600_create_issued_certs.sql", "\"H4sIAAAAAAAA/5SR0UrDMBSG7/MUh92swxVSYYjuqtoMirUbXQsbIk1oDhrqupLGrX17aV21TkGEXIXv//nOObYNFzv1rIVBSEpyFzE3ZhC7twEDfwHhMga28dfxGriqqjeUaYbaVBwsAsBzbFIlORyEzl6Eti5ns0mXCZMgmLZEqVWRqVK8VmeUxxZuEsQwenwadWSmURiUqTAcpDBo1A4/qbFzfUVt6tjUAUpv2ueMuxjWpdJY/Tem8bDPUXIwqmhUYSznS4l+EOKY5thwMFib9mcV+Q9utIV7tgWrH31CJvN+a37osQ1wJet0aLUMf+xuaP1r/qT3Z08/xhTOKsnwsN7+WBAvWq5Oh/1eMyfvAQAA//+OXEmHBQIAAA==\"") - packr.PackJSONBytes("migrations", "mysql/20180807223808_idx_revoked_expires_at.sql", "\"H4sIAAAAAAAA/9LVVdDOzUwvSixJVQgt4HL0CXENUghxdPJxVUjILC4uTU2JT04tKilOUHAJ8g9Q8PRzcY1QSMhMqYgvSi3Lz05NiU+tKMgsSi2OTyxJsObiQjbPJb88D5+Jji4uBAxU0EiAiiboJCCJa1pzAQIAAP//O0rcq7kAAAA=\"") - packr.PackJSONBytes("migrations", "mysql/20180807224200_new_primary_key.sql", "\"H4sIAAAAAAAA/5TOzarCMBAF4H2e4izvRfsErmIzQrBNa0zArhKxQYr4Q1NR314qFrJ1Nwxz5nxZhtm5O/b7IcDeGC8MaRi+LAi+i/EeWncI/RA9A4SuatRallw3WFMzZwAXAnlV2FLBd62HVCY9AbemclLlmkpSBiupt2bKWSU3liCVoN2YfrpTeLnxy5//Tv8LxlKiuD4uvyM/u0Q5AVJoWvkOAAD//1KTCm8VAQAA\"") - packr.PackJSONBytes("migrations", "mysql/20180822204521_add_reason.sql", "\"H4sIAAAAAAAA/9LVVdDOzUwvSixJVQgt4HL0CXENUghxdPJxVUjILC4uTU2JT04tKilOUHB0cVFw9vcJ9fVTSMhNLS5OTE9NUAhxjQhR8PMPUfAL9fGx5uJCNs4lvzwPn4EuQf4BGCZaAwIAAP//am0hrZEAAAA=\"") - packr.PackJSONBytes("migrations", "sqlite3/20180626224600_create_issued_certs.sql", "\"H4sIAAAAAAAA/5SR0UrDMBSG7/MUh92swxVSYYjuqtoMirUbXQsbIk1oDhrqupLGrX17aV21TkGEXIXv//nOObYNFzv1rIVBSEpyFzE3ZhC7twEDfwHhMga28dfxGriqqjeUaYbaVBwsAsBzbFIlORyEzl6Eti5ns0mXCZMgmLZEqVWRqVK8VmeUxxZuEsQwenwadWSmURiUqTAcpDBo1A4/qbFzfUVt6tjUAUpv2ueMuxjWpdJY/Tem8bDPUXIwqmhUYSznS4l+EOKY5thwMFib9mcV+Q9utIV7tgWrH31CJvN+a37osQ1wJet0aLUMf+xuaP1r/qT3Z08/xhTOKsnwsN7+WBAvWq5Oh/1eMyfvAQAA//+OXEmHBQIAAA==\"") - packr.PackJSONBytes("migrations", "sqlite3/20180807223808_idx_revoked_expires_at.sql", "\"H4sIAAAAAAAA/9LVVdDOzUwvSixJVQgt4HIJ8g9Q8PRzcY1QSMhMqYgvSi3Lz05NiU+tKMgsSi2OTyxJsObiQtbkkl+ex+Uc5OoY4kpAo4K/n0JCZnFxaWpKfHJqUUlxgoJGAlRdgo5CApJSTWsuQAAAAP//Yo/PZJkAAAA=\"") - packr.PackJSONBytes("migrations", "sqlite3/20180807224200_new_primary_key.sql", "\"H4sIAAAAAAAA/9yTUWucQBSF3+dXXPISpS5oIZTWJ5u9W6TumI4jJITgDDq0wyaujNPs7r8vugaN3RaWUigFn5xz9Zxzv1ks4M2T/mqkVZA35JphxBF49DFBELptv6uqKJWxbVGrnQCHAAhdCYgpx0/I4IbF64jdwWe887qzjToU3fmzNOU3aZy3V1cu5DT+kiPQlAPNk6QXNkbXpW7kYzsTL3EV5QmHy/uHy15ZGiWtqgppBVTSKquf1KgK3r/zF36w8APw/Q/dExzH1L7RRrXnjhn1vN2oSoDV9UHX1gkmlvxBI3fFRh0EWLW3xA1JTDNkvCslPdnasRUPxtAejLE8GL16MBjwYPiLSwAyTPCaw598BlYsXb82J0KyZOnNqW2LkEQJR/ZrEhjSaI0wDyzCF4ZiusTbDpZ9MV1FSmcD4ExX5YaETJlcbnf1ayrjVQ8S3sYZz2bOto/VwOhJDs8D8OL+4eIfA9D/Gb/uzeQSgvMS3e3A/A2Zx67+LzL7TH+NzB8BAAD//5JBr+QsBQAA\"") - packr.PackJSONBytes("migrations", "sqlite3/20180822204521_add_reason.sql", "\"H4sIAAAAAAAA/6SSQWvbQBCF7/srHrnEpjJIhVBanVRrUkTlVbpZQUIp2sUa0sW1Ylbb2P73RbFdG+MeSmBv82b2vZlvMsG7pXvyNjDqlchKTQo6+1wSjOv739w2c/ahN8jyHNOqrGcSZsl9b5/YQNODhqw0ZF2WyOk2q0uNq6tUiNPJ+fO6E1NFmaZLw5uO1wYjARjXGhRS0xdSuFPFLFOP+EqP0VBb8LYZ6i/Wz39aP3p/czNGLYtvNf318CpcedfN3cr+6s/EB4PX339cvyrnnm3gtrHBoLWBg1vyUZV8/BBP4mQSJ4jjT8NLdm28WTnP/f+2eX55XnBrEFy3dV0YJSeW4r3GrpsFbw0Cb4IYp6KQ96T0sJTq4tZ2W4lwDB3hGCvC0WuEvYEI+1/GArinkqYabxmDW1XNznhJRa6qu4sopf/GbJdJkcxmhPPAJj0wVMicHgZYNs3pKSp5Du3o9FTj9E8AAAD//xeZkE/uAgAA\"") -} diff --git a/server/store/mem.go b/server/store/mem.go deleted file mode 100644 index 8f27854..0000000 --- a/server/store/mem.go +++ /dev/null @@ -1,93 +0,0 @@ -package store - -import ( - "fmt" - "sync" - "time" -) - -var _ CertStorer = (*memoryStore)(nil) - -// memoryStore is an in-memory CertStorer -type memoryStore struct { - sync.Mutex - certs map[string]*CertRecord -} - -// Get a single *CertRecord -func (ms *memoryStore) Get(id string) (*CertRecord, error) { - ms.Lock() - defer ms.Unlock() - r, ok := ms.certs[id] - if !ok { - return nil, fmt.Errorf("unknown cert %s", id) - } - return r, nil -} - -// SetRecord records a *CertRecord -func (ms *memoryStore) SetRecord(record *CertRecord) error { - ms.Lock() - defer ms.Unlock() - ms.certs[record.KeyID] = record - return nil -} - -// List returns all recorded certs. -// By default only active certs are returned. -func (ms *memoryStore) List(includeExpired bool) ([]*CertRecord, error) { - var records []*CertRecord - ms.Lock() - defer ms.Unlock() - - for _, value := range ms.certs { - if !includeExpired && value.Expires.Before(time.Now().UTC()) { - continue - } - records = append(records, value) - } - return records, nil -} - -// Revoke an issued cert by id. -func (ms *memoryStore) Revoke(ids []string) error { - ms.Lock() - defer ms.Unlock() - for _, id := range ids { - ms.certs[id].Revoked = true - } - return nil -} - -// GetRevoked returns all revoked certs -func (ms *memoryStore) GetRevoked() ([]*CertRecord, error) { - var revoked []*CertRecord - all, _ := ms.List(false) - for _, r := range all { - if r.Revoked { - revoked = append(revoked, r) - } - } - return revoked, nil -} - -// Close the store. This will clear the contents. -func (ms *memoryStore) Close() error { - ms.Lock() - defer ms.Unlock() - ms.certs = nil - return nil -} - -func (ms *memoryStore) clear() { - for k := range ms.certs { - delete(ms.certs, k) - } -} - -// newMemoryStore returns an in-memory CertStorer. -func newMemoryStore() *memoryStore { - return &memoryStore{ - certs: make(map[string]*CertRecord), - } -} diff --git a/server/store/migrations/migrations_test.go b/server/store/migrations/migrations_test.go deleted file mode 100644 index 482450b..0000000 --- a/server/store/migrations/migrations_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package migrations - -import ( - "database/sql" - "fmt" - "io/ioutil" - "math/rand" - "os" - "os/user" - "path" - "path/filepath" - "reflect" - "testing" - - "github.com/go-sql-driver/mysql" - _ "github.com/mattn/go-sqlite3" - migrate "github.com/rubenv/sql-migrate" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSQLiteMigrations(t *testing.T) { - subdir := "sqlite3" - db, err := sql.Open(subdir, ":memory:") - require.NoError(t, err, "Unable to open sqlite connection") - runMigrations(t, db, subdir) - db.Close() -} - -func TestMySQLMigrations(t *testing.T) { - if os.Getenv("MYSQL_TEST") == "" { - t.Skip("No MYSQL_TEST environment variable") - } - r := require.New(t) - subdir := "mysql" - dsn := mysql.NewConfig() - dsn.Net = "tcp" - dsn.ParseTime = true - dsn.Addr = os.Getenv("MYSQL_TEST_HOST") - dsn.Passwd = os.Getenv("MYSQL_TEST_PASS") - u, _ := user.Current() - if testUser, ok := os.LookupEnv("MYSQL_TEST_USER"); ok { - dsn.User = testUser - } else { - dsn.User = u.Username - } - db, err := sql.Open(subdir, dsn.FormatDSN()) - r.NoError(err, "Unable to open mysql connection") - - rnd := make([]byte, 4) - rand.Read(rnd) - suffix := fmt.Sprintf("_%x", string(rnd)) - _, err = db.Exec("CREATE DATABASE migrations_test" + suffix) - r.NoError(err) - _, err = db.Exec("USE migrations_test" + suffix) - r.NoError(err) - runMigrations(t, db, subdir) - db.Exec("DROP DATABASE IF EXISTS migrations_test" + suffix) - db.Close() -} - -func runMigrations(t *testing.T, db *sql.DB, directory string) { - a := assert.New(t) - r := require.New(t) - m := &migrate.FileMigrationSource{ - Dir: directory, - } - files, err := filepath.Glob(path.Join(directory, "*.sql")) - // Verify that there is at least one migration to run - r.NoError(err, "No migrations to run") - r.NotEmpty(files) - // Verify that migrating up works - n, err := migrate.Exec(db, directory, m, migrate.Up) - if a.NoError(err) { - a.Len(files, n) - } - // Verify that a subsequent run has no migrations - n, err = migrate.Exec(db, directory, m, migrate.Up) - if a.NoError(err) { - a.Equal(0, n) - } - // Verify that reversing migrations works - n, err = migrate.Exec(db, directory, m, migrate.Down) - if a.NoError(err) { - a.Len(files, n) - } -} - -// Test that all migration directories contain the same set of migrations files. -func TestMigationDirectoryContents(t *testing.T) { - names := map[string][]string{} - contents, err := ioutil.ReadDir(".") - assert.NoError(t, err) - for _, i := range contents { - if i.IsDir() { - dir := path.Join(i.Name(), "*.sql") - files, _ := filepath.Glob(dir) - trimmed := []string{} - for _, f := range files { - trimmed = append(trimmed, filepath.Base(f)) - } - names[i.Name()] = trimmed - } - } - // Use one entry from the `names` map as a reference for all the others. - first := names[reflect.ValueOf(names).MapKeys()[0].String()] - for _, v := range names { - assert.EqualValues(t, first, v) - } -} diff --git a/server/store/migrations/mysql/20180626224600_create_issued_certs.sql b/server/store/migrations/mysql/20180626224600_create_issued_certs.sql deleted file mode 100644 index c5b80e0..0000000 --- a/server/store/migrations/mysql/20180626224600_create_issued_certs.sql +++ /dev/null @@ -1,15 +0,0 @@ --- +migrate Up -CREATE TABLE IF NOT EXISTS `issued_certs` ( - `key_id` varchar(255) NOT NULL, - `principals` varchar(255) DEFAULT "[]", - `created_at` datetime DEFAULT '1970-01-01 00:00:01', - `expires_at` datetime DEFAULT '1970-01-01 00:00:01', - `revoked` tinyint(1) DEFAULT 0, - `raw_key` text, - PRIMARY KEY (`key_id`) -); -CREATE INDEX `idx_expires_at` ON `issued_certs` (`expires_at`); -CREATE INDEX `idx_revoked_expires_at` ON `issued_certs` (`revoked`, `expires_at`); - --- +migrate Down -DROP TABLE `issued_certs`; diff --git a/server/store/migrations/mysql/20180807223808_idx_revoked_expires_at.sql b/server/store/migrations/mysql/20180807223808_idx_revoked_expires_at.sql deleted file mode 100644 index fe82dd5..0000000 --- a/server/store/migrations/mysql/20180807223808_idx_revoked_expires_at.sql +++ /dev/null @@ -1,5 +0,0 @@ --- +migrate Up -ALTER TABLE `issued_certs` DROP INDEX `idx_revoked_expires_at`; - --- +migrate Down -ALTER TABLE `issued_certs` ADD INDEX `idx_revoked_expires_at` (`revoked`,`expires_at`); diff --git a/server/store/migrations/mysql/20180807224200_new_primary_key.sql b/server/store/migrations/mysql/20180807224200_new_primary_key.sql deleted file mode 100644 index ed6a3c2..0000000 --- a/server/store/migrations/mysql/20180807224200_new_primary_key.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +migrate Up -ALTER TABLE `issued_certs` - DROP PRIMARY KEY, - ADD COLUMN `id` INT PRIMARY KEY AUTO_INCREMENT FIRST, - ADD UNIQUE INDEX `idx_key_id` (`key_id`); - --- +migrate Down -ALTER TABLE `issued_certs` - DROP PRIMARY KEY, - DROP COLUMN `id`, - ADD PRIMARY KEY (`key_id`); diff --git a/server/store/migrations/mysql/20180822204521_add_reason.sql b/server/store/migrations/mysql/20180822204521_add_reason.sql deleted file mode 100644 index 85fdd4d..0000000 --- a/server/store/migrations/mysql/20180822204521_add_reason.sql +++ /dev/null @@ -1,5 +0,0 @@ --- +migrate Up -ALTER TABLE `issued_certs` ADD COLUMN `message` TEXT NOT NULL; - --- +migrate Down -ALTER TABLE `issued_certs` DROP COLUMN `message`;
\ No newline at end of file diff --git a/server/store/migrations/sqlite3/20180626224600_create_issued_certs.sql b/server/store/migrations/sqlite3/20180626224600_create_issued_certs.sql deleted file mode 100644 index c5b80e0..0000000 --- a/server/store/migrations/sqlite3/20180626224600_create_issued_certs.sql +++ /dev/null @@ -1,15 +0,0 @@ --- +migrate Up -CREATE TABLE IF NOT EXISTS `issued_certs` ( - `key_id` varchar(255) NOT NULL, - `principals` varchar(255) DEFAULT "[]", - `created_at` datetime DEFAULT '1970-01-01 00:00:01', - `expires_at` datetime DEFAULT '1970-01-01 00:00:01', - `revoked` tinyint(1) DEFAULT 0, - `raw_key` text, - PRIMARY KEY (`key_id`) -); -CREATE INDEX `idx_expires_at` ON `issued_certs` (`expires_at`); -CREATE INDEX `idx_revoked_expires_at` ON `issued_certs` (`revoked`, `expires_at`); - --- +migrate Down -DROP TABLE `issued_certs`; diff --git a/server/store/migrations/sqlite3/20180807223808_idx_revoked_expires_at.sql b/server/store/migrations/sqlite3/20180807223808_idx_revoked_expires_at.sql deleted file mode 100644 index ae9ca3d..0000000 --- a/server/store/migrations/sqlite3/20180807223808_idx_revoked_expires_at.sql +++ /dev/null @@ -1,5 +0,0 @@ --- +migrate Up -DROP INDEX `idx_revoked_expires_at`; - --- +migrate Down -CREATE INDEX `idx_revoked_expires_at` ON `issued_certs` (`revoked`, `expires_at`); diff --git a/server/store/migrations/sqlite3/20180807224200_new_primary_key.sql b/server/store/migrations/sqlite3/20180807224200_new_primary_key.sql deleted file mode 100644 index 40f333c..0000000 --- a/server/store/migrations/sqlite3/20180807224200_new_primary_key.sql +++ /dev/null @@ -1,32 +0,0 @@ --- +migrate Up -CREATE TABLE `issued_certs_new` ( - `id` INTEGER PRIMARY KEY, - `key_id` varchar(255) UNIQUE NOT NULL, - `principals` varchar(255) DEFAULT '[]', - `created_at` datetime DEFAULT '1970-01-01 00:00:01', - `expires_at` datetime DEFAULT '1970-01-01 00:00:01', - `revoked` tinyint(1) DEFAULT '0', - `raw_key` text -); -INSERT INTO `issued_certs_new` (key_id, principals, created_at, expires_at, revoked, raw_key) - SELECT key_id, principals, created_at, expires_at, revoked, raw_key FROM `issued_certs`; -DROP TABLE `issued_certs`; -ALTER TABLE `issued_certs_new` RENAME TO `issued_certs`; -CREATE INDEX `idx_expires_at` ON `issued_certs` (`expires_at`); - --- +migrate Down -CREATE TABLE IF NOT EXISTS `issued_certs_old` ( - `key_id` varchar(255) NOT NULL, - `principals` varchar(255) DEFAULT "[]", - `created_at` datetime DEFAULT '1970-01-01 00:00:01', - `expires_at` datetime DEFAULT '1970-01-01 00:00:01', - `revoked` tinyint(1) DEFAULT 0, - `raw_key` text, - PRIMARY KEY (`key_id`) -); - -INSERT INTO `issued_certs_old` (key_id, principals, created_at, expires_at, revoked, raw_key) - SELECT key_id, principals, created_at, expires_at, revoked, raw_key FROM `issued_certs`; -DROP TABLE `issued_certs`; -ALTER TABLE `issued_certs_old` RENAME TO `issued_certs`; -CREATE INDEX `idx_expires_at` ON `issued_certs` (`expires_at`);
\ No newline at end of file diff --git a/server/store/migrations/sqlite3/20180822204521_add_reason.sql b/server/store/migrations/sqlite3/20180822204521_add_reason.sql deleted file mode 100644 index 07e9d49..0000000 --- a/server/store/migrations/sqlite3/20180822204521_add_reason.sql +++ /dev/null @@ -1,18 +0,0 @@ --- +migrate Up -ALTER TABLE `issued_certs` ADD COLUMN `message` TEXT NOT NULL DEFAULT ""; - --- +migrate Down -CREATE TABLE `issued_certs_new` ( - `id` INTEGER PRIMARY KEY, - `key_id` varchar(255) UNIQUE NOT NULL, - `principals` varchar(255) DEFAULT '[]', - `created_at` datetime DEFAULT '1970-01-01 00:00:01', - `expires_at` datetime DEFAULT '1970-01-01 00:00:01', - `revoked` tinyint(1) DEFAULT '0', - `raw_key` text -); -INSERT INTO `issued_certs_new` (key_id, principals, created_at, expires_at, revoked, raw_key) - SELECT key_id, principals, created_at, expires_at, revoked, raw_key FROM `issued_certs`; -DROP TABLE `issued_certs`; -ALTER TABLE `issued_certs_new` RENAME TO `issued_certs`; -CREATE INDEX `idx_expires_at` ON `issued_certs` (`expires_at`);
\ No newline at end of file diff --git a/server/store/sqldb.go b/server/store/sqldb.go deleted file mode 100644 index 8ca34a9..0000000 --- a/server/store/sqldb.go +++ /dev/null @@ -1,176 +0,0 @@ -package store - -import ( - "fmt" - "log" - "net" - "time" - - "github.com/go-sql-driver/mysql" - "github.com/gobuffalo/packr" - multierror "github.com/hashicorp/go-multierror" - "github.com/jmoiron/sqlx" - "github.com/nsheridan/cashier/server/config" - "github.com/pkg/errors" - migrate "github.com/rubenv/sql-migrate" -) - -var _ CertStorer = (*sqlStore)(nil) - -// sqlStore is an sql-based CertStorer -type sqlStore struct { - conn *sqlx.DB - - get *sqlx.Stmt - set *sqlx.Stmt - listAll *sqlx.Stmt - listCurrent *sqlx.Stmt - revoked *sqlx.Stmt -} - -// newSQLStore returns a *sql.DB CertStorer. -func newSQLStore(c config.Database) (*sqlStore, error) { - var driver string - var dsn string - switch c["type"] { - case "mysql": - driver = "mysql" - address := c["address"] - _, _, err := net.SplitHostPort(address) - if err != nil { - address = address + ":3306" - } - m := mysql.NewConfig() - m.User = c["username"] - m.Passwd = c["password"] - m.Addr = address - m.Net = "tcp" - m.DBName = c["dbname"] - if m.DBName == "" { - m.DBName = "certs" // Legacy database name - } - m.ParseTime = true - dsn = m.FormatDSN() - case "sqlite": - driver = "sqlite3" - dsn = c["filename"] - } - - conn, err := sqlx.Connect(driver, dsn) - if err != nil { - return nil, fmt.Errorf("sqlStore: could not get a connection: %v", err) - } - if err := autoMigrate(driver, conn); err != nil { - return nil, fmt.Errorf("sqlStore: could not update schema: %v", err) - } - - db := &sqlStore{ - conn: conn, - } - - if db.set, err = conn.Preparex("INSERT INTO issued_certs (key_id, principals, created_at, expires_at, raw_key, message) VALUES (?, ?, ?, ?, ?, ?)"); err != nil { - return nil, fmt.Errorf("sqlStore: prepare set: %v", err) - } - if db.get, err = conn.Preparex("SELECT * FROM issued_certs WHERE key_id = ?"); err != nil { - return nil, fmt.Errorf("sqlStore: prepare get: %v", err) - } - if db.listAll, err = conn.Preparex("SELECT * FROM issued_certs"); err != nil { - return nil, fmt.Errorf("sqlStore: prepare listAll: %v", err) - } - if db.listCurrent, err = conn.Preparex("SELECT * FROM issued_certs WHERE expires_at >= ?"); err != nil { - return nil, fmt.Errorf("sqlStore: prepare listCurrent: %v", err) - } - if db.revoked, err = conn.Preparex("SELECT * FROM issued_certs WHERE revoked = 1 AND ? <= expires_at"); err != nil { - return nil, fmt.Errorf("sqlStore: prepare revoked: %v", err) - } - return db, nil -} - -func autoMigrate(driver string, conn *sqlx.DB) error { - log.Print("Executing any pending schema migrations") - var err error - migrate.SetTable("schema_migrations") - srcs := &migrate.PackrMigrationSource{ - Box: packr.NewBox("migrations"), - Dir: driver, - } - n, err := migrate.Exec(conn.DB, driver, srcs, migrate.Up) - if err != nil { - err = multierror.Append(err) - return err - } - log.Printf("Executed %d migrations", n) - if err != nil { - log.Fatalf("Errors were found running migrations: %v", err) - } - return nil -} - -// Get a single *CertRecord -func (db *sqlStore) Get(id string) (*CertRecord, error) { - if err := db.conn.Ping(); err != nil { - return nil, errors.Wrap(err, "unable to connect to database") - } - r := &CertRecord{} - return r, db.get.Get(r, id) -} - -// SetRecord records a *CertRecord -func (db *sqlStore) SetRecord(rec *CertRecord) error { - if err := db.conn.Ping(); err != nil { - return errors.Wrap(err, "unable to connect to database") - } - _, err := db.set.Exec(rec.KeyID, rec.Principals, rec.CreatedAt, rec.Expires, rec.Raw, rec.Message) - return err -} - -// List returns all recorded certs. -// By default only active certs are returned. -func (db *sqlStore) List(includeExpired bool) ([]*CertRecord, error) { - if err := db.conn.Ping(); err != nil { - return nil, errors.Wrap(err, "unable to connect to database") - } - recs := []*CertRecord{} - if includeExpired { - if err := db.listAll.Select(&recs); err != nil { - return nil, err - } - } else { - if err := db.listCurrent.Select(&recs, time.Now()); err != nil { - return nil, err - } - } - return recs, nil -} - -// Revoke an issued cert by id. -func (db *sqlStore) Revoke(ids []string) error { - var err error - if err = db.conn.Ping(); err != nil { - return errors.Wrap(err, "unable to connect to database") - } - q, args, err := sqlx.In("UPDATE issued_certs SET revoked = 1 WHERE key_id IN (?)", ids) - if err != nil { - return err - } - q = db.conn.Rebind(q) - _, err = db.conn.Exec(q, args...) - return err -} - -// GetRevoked returns all revoked certs -func (db *sqlStore) GetRevoked() ([]*CertRecord, error) { - if err := db.conn.Ping(); err != nil { - return nil, errors.Wrap(err, "unable to connect to database") - } - var recs []*CertRecord - if err := db.revoked.Select(&recs, time.Now().UTC()); err != nil { - return nil, err - } - return recs, nil -} - -// Close the connection to the database -func (db *sqlStore) Close() error { - return db.conn.Close() -} diff --git a/server/store/sqlite.go b/server/store/sqlite.go deleted file mode 100644 index 8f38bd2..0000000 --- a/server/store/sqlite.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build cgo - -package store - -import ( - _ "github.com/mattn/go-sqlite3" // required by sql driver -) diff --git a/server/store/store.go b/server/store/store.go deleted file mode 100644 index 88ec7ce..0000000 --- a/server/store/store.go +++ /dev/null @@ -1,77 +0,0 @@ -package store - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/nsheridan/cashier/lib" - "github.com/nsheridan/cashier/server/config" - "golang.org/x/crypto/ssh" -) - -// New returns a new configured database. -func New(c config.Database) (CertStorer, error) { - switch c["type"] { - case "mysql", "sqlite": - return newSQLStore(c) - case "mem": - return newMemoryStore(), nil - } - return nil, fmt.Errorf("unable to create store with driver %s", c["type"]) -} - -// CertStorer records issued certs in a persistent store for audit and -// revocation purposes. -type CertStorer interface { - Get(id string) (*CertRecord, error) - SetRecord(record *CertRecord) error - List(includeExpired bool) ([]*CertRecord, error) - Revoke(id []string) error - GetRevoked() ([]*CertRecord, error) - Close() error -} - -// A CertRecord is a representation of a ssh certificate used by a CertStorer. -type CertRecord struct { - ID int `json:"-" db:"id"` - KeyID string `json:"key_id" db:"key_id"` - Principals StringSlice `json:"principals" db:"principals"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - Expires time.Time `json:"expires" db:"expires_at"` - Revoked bool `json:"revoked" db:"revoked"` - Raw string `json:"-" db:"raw_key"` - Message string `json:"message" db:"message"` -} - -// MarshalJSON implements the json.Marshaler interface for the CreatedAt and -// Expires fields. -// The resulting string looks like "2017-04-11 10:00:00 +0000" -func (c *CertRecord) MarshalJSON() ([]byte, error) { - type Alias CertRecord - f := "2006-01-02 15:04:05 -0700" - return json.Marshal(&struct { - *Alias - CreatedAt string `json:"created_at"` - Expires string `json:"expires"` - }{ - Alias: (*Alias)(c), - CreatedAt: c.CreatedAt.Format(f), - Expires: c.Expires.Format(f), - }) -} - -func parseTime(t uint64) time.Time { - return time.Unix(int64(t), 0) -} - -// MakeRecord converts a Certificate to a CertRecord -func MakeRecord(cert *ssh.Certificate) *CertRecord { - return &CertRecord{ - KeyID: cert.KeyId, - Principals: StringSlice(cert.ValidPrincipals), - CreatedAt: parseTime(cert.ValidAfter), - Expires: parseTime(cert.ValidBefore), - Raw: string(lib.GetPublicKey(cert)), - } -} diff --git a/server/store/store_test.go b/server/store/store_test.go deleted file mode 100644 index 90a494e..0000000 --- a/server/store/store_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package store - -import ( - "crypto/rand" - "crypto/rsa" - "encoding/json" - "io/ioutil" - "os" - "os/user" - "testing" - "time" - - "github.com/nsheridan/cashier/testdata" - "github.com/stretchr/testify/assert" - - "golang.org/x/crypto/ssh" -) - -func TestParseCertificate(t *testing.T) { - a := assert.New(t) - now := uint64(time.Now().Unix()) - r, _ := rsa.GenerateKey(rand.Reader, 1024) - pub, _ := ssh.NewPublicKey(r.Public()) - c := &ssh.Certificate{ - KeyId: "id", - ValidPrincipals: StringSlice{"principal"}, - ValidBefore: now, - CertType: ssh.UserCert, - Key: pub, - } - s, _ := ssh.NewSignerFromKey(r) - c.SignCert(rand.Reader, s) - rec := MakeRecord(c) - - a.Equal(c.KeyId, rec.KeyID) - a.Equal(c.ValidPrincipals, []string(rec.Principals)) - a.Equal(c.ValidBefore, uint64(rec.Expires.Unix())) - a.Equal(c.ValidAfter, uint64(rec.CreatedAt.Unix())) -} - -func testStore(t *testing.T, db CertStorer) { - defer db.Close() - - r := &CertRecord{ - KeyID: "a", - Principals: []string{"b"}, - CreatedAt: time.Now().UTC(), - Expires: time.Now().UTC().Add(-1 * time.Second), - Raw: "AAAAAA", - } - if err := db.SetRecord(r); err != nil { - t.Error(err) - } - - // includeExpired = false should return 0 results - recs, err := db.List(false) - if err != nil { - t.Error(err) - } - if len(recs) > 0 { - t.Errorf("Expected 0 results, got %d", len(recs)) - } - // includeExpired = false should return 1 result - recs, err = db.List(true) - if err != nil { - t.Error(err) - } - if recs[0].KeyID != r.KeyID { - t.Error("key mismatch") - } - - c, _, _, _, _ := ssh.ParseAuthorizedKey(testdata.Cert) - cert := c.(*ssh.Certificate) - cert.ValidBefore = uint64(time.Now().Add(1 * time.Hour).UTC().Unix()) - cert.ValidAfter = uint64(time.Now().Add(-5 * time.Minute).UTC().Unix()) - rec := MakeRecord(cert) - if err := db.SetRecord(rec); err != nil { - t.Error(err) - } - - ret, err := db.Get("key") - if err != nil { - t.Error(err) - } - if ret.KeyID != cert.KeyId { - t.Error("key mismatch") - } - if err := db.Revoke([]string{"key"}); err != nil { - t.Error(err) - } - - revoked, err := db.GetRevoked() - if err != nil { - t.Error(err) - } - if len(revoked) != 1 { - t.Errorf("Expected 1 revoked key, got %d", len(revoked)) - } - for _, k := range revoked { - if k.KeyID != "key" { - t.Errorf("Unexpected key: %s", k.KeyID) - } - } -} - -func TestMemoryStore(t *testing.T) { - db := newMemoryStore() - testStore(t, db) -} - -func TestMySQLStore(t *testing.T) { - if os.Getenv("MYSQL_TEST") == "" { - t.Skip("No MYSQL_TEST environment variable") - } - u, _ := user.Current() - sqlConfig := map[string]string{ - "type": "mysql", - "password": os.Getenv("MYSQL_TEST_PASS"), - "address": os.Getenv("MYSQL_TEST_HOST"), - } - if testUser, ok := os.LookupEnv("MYSQL_TEST_USER"); ok { - sqlConfig["username"] = testUser - } else { - sqlConfig["username"] = u.Username - } - db, err := newSQLStore(sqlConfig) - if err != nil { - t.Error(err) - } - testStore(t, db) -} - -func TestSQLiteStore(t *testing.T) { - f, err := ioutil.TempFile("", "sqlite_test_db") - if err != nil { - t.Error(err) - } - defer os.Remove(f.Name()) - config := map[string]string{"type": "sqlite", "filename": f.Name()} - db, err := newSQLStore(config) - if err != nil { - t.Error(err) - } - testStore(t, db) -} - -func TestMarshalCert(t *testing.T) { - a := assert.New(t) - c := &CertRecord{ - KeyID: "id", - Principals: []string{"user"}, - CreatedAt: time.Date(2017, time.April, 10, 13, 0, 0, 0, time.UTC), - Expires: time.Date(2017, time.April, 11, 10, 0, 0, 0, time.UTC), - Raw: "ABCDEF", - } - b, err := json.Marshal(c) - if err != nil { - t.Error(err) - } - want := `{"key_id":"id","principals":["user"],"revoked":false,"created_at":"2017-04-10 13:00:00 +0000","expires":"2017-04-11 10:00:00 +0000","message":""}` - a.JSONEq(want, string(b)) -} diff --git a/server/store/string_slice.go b/server/store/string_slice.go deleted file mode 100644 index a443cdd..0000000 --- a/server/store/string_slice.go +++ /dev/null @@ -1,38 +0,0 @@ -package store - -import ( - "database/sql/driver" - "encoding/json" -) - -// StringSlice is a []string which will be stored in a database as a JSON array. -type StringSlice []string - -var _ driver.Valuer = (*StringSlice)(nil) - -// Value implements the driver.Valuer interface, marshalling the raw value to -// a JSON array. -func (s StringSlice) Value() (driver.Value, error) { - v, err := json.Marshal(s) - if err != nil { - return nil, err - } - return string(v), err -} - -// Scan implements the sql.Scanner interface, unmarshalling the value coming -// off the wire and storing the result in the StringSlice. -func (s *StringSlice) Scan(value interface{}) error { - if value == nil { - s = &StringSlice{} - return nil - } - var err error - v, err := driver.String.ConvertValue(value) - if err == nil { - if v, ok := v.([]byte); ok { - err = json.Unmarshal(v, s) - } - } - return err -} diff --git a/server/templates/token.go b/server/templates/token.go index 7eb0b47..05f5a82 100644 --- a/server/templates/token.go +++ b/server/templates/token.go @@ -3,49 +3,17 @@ package templates // Token is the page users see when authenticated. const Token = ` <!DOCTYPE html> -<html lang="en"> +<html> <head> <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Token</title> - - <link rel="stylesheet" href="/static/css/normalize.css"> - <link rel="stylesheet" href="/static/css/skeleton.css"> - <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet"> - <link href="https://fonts.googleapis.com/css?family=Source+Code+Pro" rel="stylesheet"> <style> - <!-- - .code { - background-color: #eee; - border: solid 1px #ccc; - font-family: 'Source Code Pro', monospace; - font-weight: bold; - height: 120px; - margin: 12px 12px 12px 12px; - padding: 24px 12px 12px 12px; - resize: none; - text-align: center; - } - --> </style> </head> <body> - <div class="container"> - <div class="page-header"> - <h2>Access Token</h2> - </div> - <div> - <textarea style="font-size: 12pt" class="u-full-width code" readonly spellcheck="false" onclick="this.focus();this.select();">{{.Token}}</textarea> - <h3> - The token will expire in < 1 hour. - </h3> - </div> - <div> - <h4> - <a href="/admin/certs">Previously Issued Certificates</a> - </h4> - </div> - </div> + <h1>Access Token</h1> + <p>Paste the following token at your <code>cashier</code> prompt:</p> + <pre><code>{{.Token}}</code></pre> </body> </html> ` diff --git a/server/wkfs/vaultfs/vault.go b/server/wkfs/vaultfs/vault.go deleted file mode 100644 index dcefd54..0000000 --- a/server/wkfs/vaultfs/vault.go +++ /dev/null @@ -1,99 +0,0 @@ -package vaultfs - -import ( - "bytes" - "errors" - "os" - "path" - "time" - - "github.com/nsheridan/cashier/server/config" - "github.com/nsheridan/cashier/server/helpers/vault" - "go4.org/wkfs" -) - -// Register the /vault/ filesystem as a well-known filesystem. -func Register(vc *config.Vault) { - if vc == nil { - registerBrokenFS(errors.New("no vault configuration found")) - return - } - client, err := vault.NewClient(vc.Address, vc.Token) - if err != nil { - registerBrokenFS(err) - return - } - wkfs.RegisterFS("/vault/", &vaultFS{ - client: client, - }) -} - -func registerBrokenFS(err error) { - wkfs.RegisterFS("/vault/", &vaultFS{ - err: err, - }) -} - -type vaultFS struct { - err error - client *vault.Client -} - -// Open opens the named file for reading. -func (fs *vaultFS) Open(name string) (wkfs.File, error) { - secret, err := fs.client.Read(name) - if err != nil { - return nil, err - } - return &file{ - name: name, - Reader: bytes.NewReader([]byte(secret)), - }, nil -} - -func (fs *vaultFS) Stat(name string) (os.FileInfo, error) { return fs.Lstat(name) } -func (fs *vaultFS) Lstat(name string) (os.FileInfo, error) { - secret, err := fs.client.Read(name) - if err != nil { - return nil, err - } - return &statInfo{ - name: path.Base(name), - size: int64(len(secret)), - }, nil -} - -func (fs *vaultFS) MkdirAll(path string, perm os.FileMode) error { return nil } - -func (fs *vaultFS) OpenFile(name string, flag int, perm os.FileMode) (wkfs.FileWriter, error) { - return nil, errors.New("not implemented") -} - -func (fs *vaultFS) Remove(path string) error { - return fs.client.Delete(path) -} - -type statInfo struct { - name string - size int64 - isDir bool - modtime time.Time -} - -func (si *statInfo) IsDir() bool { return si.isDir } -func (si *statInfo) ModTime() time.Time { return si.modtime } -func (si *statInfo) Mode() os.FileMode { return 0644 } -func (si *statInfo) Name() string { return path.Base(si.name) } -func (si *statInfo) Size() int64 { return si.size } -func (si *statInfo) Sys() interface{} { return nil } - -type file struct { - name string - *bytes.Reader -} - -func (*file) Close() error { return nil } -func (f *file) Name() string { return path.Base(f.name) } -func (f *file) Stat() (os.FileInfo, error) { - return nil, errors.New("Stat not implemented on /vault/ files") -} |