aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--client/client.go24
-rw-r--r--cmd/cashierd/main.go15
-rw-r--r--example-server.conf16
-rw-r--r--generate/migration/migration.go53
-rw-r--r--generate/static/static.go24
-rw-r--r--lib/proto.go1
-rw-r--r--server/a_server-packr.go14
-rw-r--r--server/auth/github/github.go70
-rw-r--r--server/auth/gitlab/gitlab.go225
-rw-r--r--server/auth/gitlab/gitlab_test.go98
-rw-r--r--server/auth/google/google.go135
-rw-r--r--server/auth/google/google_test.go75
-rw-r--r--server/auth/microsoft/microsoft.go201
-rw-r--r--server/auth/microsoft/microsoft_test.go72
-rw-r--r--server/auth/provider.go13
-rw-r--r--server/auth/testprovider/testprovider.go56
-rw-r--r--server/config/config.go100
-rw-r--r--server/handlers.go66
-rw-r--r--server/helpers/vault/vault.go62
-rw-r--r--server/server.go101
-rw-r--r--server/signer/signer.go19
-rw-r--r--server/static/css/normalize.css427
-rw-r--r--server/static/css/skeleton.css418
-rw-r--r--server/static/js/list.min.js1
-rw-r--r--server/static/js/table.js51
-rw-r--r--server/store/a_store-packr.go19
-rw-r--r--server/store/mem.go93
-rw-r--r--server/store/migrations/migrations_test.go110
-rw-r--r--server/store/migrations/mysql/20180626224600_create_issued_certs.sql15
-rw-r--r--server/store/migrations/mysql/20180807223808_idx_revoked_expires_at.sql5
-rw-r--r--server/store/migrations/mysql/20180807224200_new_primary_key.sql11
-rw-r--r--server/store/migrations/mysql/20180822204521_add_reason.sql5
-rw-r--r--server/store/migrations/sqlite3/20180626224600_create_issued_certs.sql15
-rw-r--r--server/store/migrations/sqlite3/20180807223808_idx_revoked_expires_at.sql5
-rw-r--r--server/store/migrations/sqlite3/20180807224200_new_primary_key.sql32
-rw-r--r--server/store/migrations/sqlite3/20180822204521_add_reason.sql18
-rw-r--r--server/store/sqldb.go176
-rw-r--r--server/store/sqlite.go7
-rw-r--r--server/store/store.go77
-rw-r--r--server/store/store_test.go162
-rw-r--r--server/store/string_slice.go38
-rw-r--r--server/templates/token.go40
-rw-r--r--server/wkfs/vaultfs/vault.go99
43 files changed, 76 insertions, 3188 deletions
diff --git a/client/client.go b/client/client.go
index b84e09a..22194d2 100644
--- a/client/client.go
+++ b/client/client.go
@@ -1,7 +1,6 @@
package client
import (
- "bufio"
"bytes"
"crypto/tls"
"encoding/base64"
@@ -11,9 +10,7 @@ import (
"io/ioutil"
"net/http"
"net/url"
- "os"
"path"
- "strings"
"time"
"github.com/nsheridan/cashier/lib"
@@ -22,10 +19,6 @@ import (
"golang.org/x/crypto/ssh/agent"
)
-var (
- errNeedsReason = errors.New("reason required")
-)
-
// SavePublicFiles installs the public part of the cert and key.
func SavePublicFiles(prefix string, cert *ssh.Certificate, pub ssh.PublicKey) error {
if prefix == "" {
@@ -114,9 +107,6 @@ func send(sr *lib.SignRequest, token, ca string, ValidateTLSCertificate bool) (*
defer resp.Body.Close()
signResponse := &lib.SignResponse{}
if resp.StatusCode != http.StatusOK {
- if resp.StatusCode == http.StatusForbidden && strings.HasPrefix(resp.Header.Get("X-Need-Reason"), "required") {
- return signResponse, errNeedsReason
- }
return signResponse, fmt.Errorf("bad response from server: %s", resp.Status)
}
if err := json.NewDecoder(resp.Body).Decode(signResponse); err != nil {
@@ -125,15 +115,6 @@ func send(sr *lib.SignRequest, token, ca string, ValidateTLSCertificate bool) (*
return signResponse, nil
}
-func promptForReason() (message string) {
- fmt.Print("Enter message: ")
- scanner := bufio.NewScanner(os.Stdin)
- if scanner.Scan() {
- message = scanner.Text()
- }
- return message
-}
-
// Sign sends the public key to the CA to be signed.
func Sign(pub ssh.PublicKey, token string, conf *Config) (*ssh.Certificate, error) {
var err error
@@ -152,10 +133,7 @@ func Sign(pub ssh.PublicKey, token string, conf *Config) (*ssh.Certificate, erro
if err == nil {
break
}
- if err != nil && err == errNeedsReason {
- s.Message = promptForReason()
- continue
- } else if err != nil {
+ if err != nil {
return nil, errors.Wrap(err, "error sending request to CA")
}
}
diff --git a/cmd/cashierd/main.go b/cmd/cashierd/main.go
index b4f1fe7..5690d7c 100644
--- a/cmd/cashierd/main.go
+++ b/cmd/cashierd/main.go
@@ -9,8 +9,6 @@ import (
"github.com/nsheridan/cashier/lib"
"github.com/nsheridan/cashier/server"
"github.com/nsheridan/cashier/server/config"
- "github.com/nsheridan/cashier/server/wkfs/vaultfs"
- "github.com/nsheridan/wkfs/s3"
)
var (
@@ -28,18 +26,5 @@ func main() {
if err != nil {
log.Fatal(err)
}
-
- // Register well-known filesystems.
- if conf.AWS == nil {
- conf.AWS = &config.AWS{}
- }
- s3.Register(&s3.Options{
- Region: conf.AWS.Region,
- AccessKey: conf.AWS.AccessKey,
- SecretKey: conf.AWS.SecretKey,
- })
- vaultfs.Register(conf.Vault)
-
- // Start the server
server.Run(conf)
}
diff --git a/example-server.conf b/example-server.conf
index 477ae15..795acc5 100644
--- a/example-server.conf
+++ b/example-server.conf
@@ -1,22 +1,12 @@
# Server config
server {
- use_tls = true # Optional. If this is set then `tls_key` and `tls_cert` must be set
- tls_key = "server.key" # Path to TLS key
- tls_cert = "server.crt" # Path to TLS certificate
- address = "127.0.0.1" # Optional. IP address to listen on
- port = 443 # Port to listen on
+ address = "0.0.0.0" # Optional. IP address to listen on
+ port = 80 # Port to listen on
user = "www" # Optional. User to which the server drops privileges to
cookie_secret = "supersecret" # Authentication key for the client cookie
csrf_secret = "supersecret" # Authentication key for the CSRF token
+ secure_cookie = true
http_logfile = "http.log" # Logfile for HTTP requests
- require_reason = false # Optional. Request a reason for the certificate from the client
- database {
- type = "mysql"
- dbname = "cashier_production"
- address = "host:3306"
- username = "user"
- password = "pass"
- }
}
# Oauth2 configuration
diff --git a/generate/migration/migration.go b/generate/migration/migration.go
deleted file mode 100644
index 37515bf..0000000
--- a/generate/migration/migration.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package main
-
-import (
- "flag"
- "fmt"
- "io/ioutil"
- "log"
- "os/exec"
- "path"
- "strings"
- "time"
-)
-
-const (
- dateFormat = "20060102150405"
- migrationsPath = "server/store/migrations"
-)
-
-var (
- contents = []byte(`-- +migrate Up
-
-
--- +migrate Down`)
-)
-
-func main() {
- flag.Usage = func() {
- fmt.Println("Usage: migration <migration name>")
- }
- flag.Parse()
- if len(flag.Args()) != 1 {
- flag.Usage()
- }
- name := fmt.Sprintf("%s_%s.sql", time.Now().UTC().Format(dateFormat), flag.Arg(0))
- gitRoot, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
- if err != nil {
- log.Fatal(err)
- }
- root := strings.TrimSpace(string(gitRoot))
- ents, err := ioutil.ReadDir(path.Join(root, migrationsPath))
- if err != nil {
- log.Fatal(err)
- }
- for _, e := range ents {
- if e.IsDir() {
- filename := path.Join(migrationsPath, e.Name(), name)
- fmt.Printf("Wrote empty migration file: %s\n", filename)
- if err := ioutil.WriteFile(filename, contents, 0644); err != nil {
- log.Fatal(err)
- }
- }
- }
-}
diff --git a/generate/static/static.go b/generate/static/static.go
deleted file mode 100644
index c8e7f43..0000000
--- a/generate/static/static.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package main
-
-//go:generate go run static.go
-
-import (
- "context"
- "log"
- "os/exec"
- "strings"
-
- "github.com/gobuffalo/packr/builder"
-)
-
-func main() {
- root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
- if err != nil {
- log.Fatal(err)
- }
- b := builder.New(context.Background(), strings.TrimSpace(string(root)))
- b.Compress = true
- if err := b.Run(); err != nil {
- log.Fatal(err)
- }
-}
diff --git a/lib/proto.go b/lib/proto.go
index 5d8c67a..4e927af 100644
--- a/lib/proto.go
+++ b/lib/proto.go
@@ -6,7 +6,6 @@ import "time"
type SignRequest struct {
Key string `json:"key"`
ValidUntil time.Time `json:"valid_until"`
- Message string `json:"message"`
Version string `json:"version"`
}
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 &lt; 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")
-}