Commit db67c6a8 authored by ale's avatar ale

Support partitioned keystore service

Uses the clientutil.Backend abstraction for the keystore client API.
parent 4feb3be4
Pipeline #722 passed with stages
in 1 minute and 5 seconds
......@@ -19,7 +19,7 @@ import (
"github.com/crewjam/saml"
"github.com/crewjam/saml/logger"
"github.com/gorilla/mux"
yaml "gopkg.in/yaml.v2"
"gopkg.in/yaml.v2"
"git.autistici.org/id/go-sso/httpsso"
)
......
......@@ -6,9 +6,9 @@ import (
"regexp"
"time"
ksclient "git.autistici.org/id/keystore/client"
"github.com/gorilla/securecookie"
"git.autistici.org/ai3/go-common/clientutil"
"git.autistici.org/id/go-sso/server/device"
)
......@@ -29,12 +29,12 @@ type Config struct {
TTLSeconds int `yaml:"ttl"`
rx *regexp.Regexp
} `yaml:"service_ttls"`
AuthSessionLifetimeSeconds int `yaml:"auth_session_lifetime"`
SessionSecrets []string `yaml:"session_secrets"`
CSRFSecret string `yaml:"csrf_secret"`
AuthService string `yaml:"auth_service"`
DeviceManager *device.Config `yaml:"device_manager"`
KeyStore *ksclient.Config `yaml:"keystore"`
AuthSessionLifetimeSeconds int `yaml:"auth_session_lifetime"`
SessionSecrets []string `yaml:"session_secrets"`
CSRFSecret string `yaml:"csrf_secret"`
AuthService string `yaml:"auth_service"`
DeviceManager *device.Config `yaml:"device_manager"`
KeyStore *clientutil.BackendConfig `yaml:"keystore"`
allowedServicesRx []*regexp.Regexp
}
......
......@@ -86,7 +86,7 @@ type Server struct {
authSessionLifetime time.Duration
loginHandler *loginHandler
loginService *LoginService
keystore *ksclient.Client
keystore ksclient.Client
csrfSecret []byte
tpl *template.Template
}
......@@ -147,7 +147,11 @@ func (h *Server) loginCallback(w http.ResponseWriter, req *http.Request, usernam
// authenticate. Set the TTL to the duration of the
// authenticated session.
if h.keystore != nil {
if err := h.keystore.Open(req.Context(), username, password, int(h.authSessionLifetime.Seconds())); err != nil {
var shard string
if userinfo != nil {
shard = userinfo.Shard
}
if err := h.keystore.Open(req.Context(), shard, username, password, int(h.authSessionLifetime.Seconds())); err != nil {
log.Printf("failed to unlock keystore for user %s: %v", username, err)
return err
}
......@@ -257,7 +261,11 @@ func (h *Server) handleLogout(w http.ResponseWriter, req *http.Request, session
// Close the keystore.
if h.keystore != nil {
if err := h.keystore.Close(req.Context(), session.Username); err != nil {
var shard string
if session.UserInfo != nil {
shard = session.UserInfo.Shard
}
if err := h.keystore.Close(req.Context(), shard, session.Username); err != nil {
log.Printf("failed to wipe keystore for user %s: %v", session.Username, err)
}
}
......
......@@ -46,7 +46,7 @@ auth_service: login
if keystoreURL != "" {
cfgstr += fmt.Sprintf(`
keystore:
backend_url: "%s"
url: "%s"
`, keystoreURL)
}
ioutil.WriteFile(filepath.Join(tmpdir, "config"), []byte(cfgstr), 0600)
......
package clientutil
import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"sync"
"time"
)
// BackendConfig specifies the configuration to access a service.
//
// Services with multiple backends can be replicated or partitioned,
// depending on a configuration switch, making it a deployment-time
// decision. Clients are expected to compute their own sharding
// function (either by database lookup or other methods), and expose a
// 'shard' parameter on their APIs.
type BackendConfig struct {
URL string `yaml:"url"`
Sharded bool `yaml:"sharded"`
TLSConfig *TLSClientConfig `yaml:"tls_config"`
}
// Backend is a runtime class that provides http Clients for use with
// a specific service backend. If the service can't be partitioned,
// pass an empty string to the Client method.
type Backend interface {
// URL for the service for a specific shard.
URL(string) string
// Client that can be used to make a request to the service.
Client(string) *http.Client
}
// NewBackend returns a new Backend with the given config.
func NewBackend(config *BackendConfig) (Backend, error) {
u, err := url.Parse(config.URL)
if err != nil {
return nil, err
}
var tlsConfig *tls.Config
if config.TLSConfig != nil {
tlsConfig, err = config.TLSConfig.TLSConfig()
if err != nil {
return nil, err
}
}
if config.Sharded {
return &replicatedClient{
u: u,
c: newHTTPClient(u, tlsConfig),
}, nil
}
return &shardedClient{
baseURL: u,
tlsConfig: tlsConfig,
urls: make(map[string]*url.URL),
shards: make(map[string]*http.Client),
}, nil
}
type replicatedClient struct {
c *http.Client
u *url.URL
}
func (r *replicatedClient) Client(_ string) *http.Client { return r.c }
func (r *replicatedClient) URL(_ string) string { return r.u.String() }
type shardedClient struct {
baseURL *url.URL
tlsConfig *tls.Config
mx sync.Mutex
urls map[string]*url.URL
shards map[string]*http.Client
}
func (s *shardedClient) getShardURL(shard string) *url.URL {
if shard == "" {
return s.baseURL
}
u, ok := s.urls[shard]
if !ok {
var tmp = *s.baseURL
tmp.Host = fmt.Sprintf("%s.%s", shard, tmp.Host)
u = &tmp
s.urls[shard] = u
}
return u
}
func (s *shardedClient) URL(shard string) string {
s.mx.Lock()
defer s.mx.Unlock()
return s.getShardURL(shard).String()
}
func (s *shardedClient) Client(shard string) *http.Client {
s.mx.Lock()
defer s.mx.Unlock()
client, ok := s.shards[shard]
if !ok {
u := s.getShardURL(shard)
client = newHTTPClient(u, s.tlsConfig)
s.shards[shard] = client
}
return client
}
func newHTTPClient(u *url.URL, tlsConfig *tls.Config) *http.Client {
return &http.Client{
Transport: NewTransport([]string{u.Host}, tlsConfig, nil),
Timeout: 30 * time.Second,
}
}
......@@ -2,10 +2,6 @@ package client
import (
"context"
"crypto/tls"
"net/http"
"net/url"
"time"
"git.autistici.org/ai3/go-common/clientutil"
......@@ -13,66 +9,50 @@ import (
)
// Client for the keystore API.
type Client struct {
*http.Client
backendURL string
type Client interface {
Open(context.Context, string, string, string, int) error
Get(context.Context, string, string, string) ([]byte, error)
Close(context.Context, string, string) error
}
// Config for a keystore client.
type Config struct {
BackendURL string `yaml:"backend_url"`
TLSConfig *clientutil.TLSClientConfig `yaml:"tls_config"`
type ksClient struct {
be clientutil.Backend
}
// New returns a new Client with the given Config.
func New(config *Config) (*Client, error) {
u, err := url.Parse(config.BackendURL)
// New returns a new Client with the given Config. Use this when the
// keystore service runs on a single global instance.
func New(config *clientutil.BackendConfig) (*ksClient, error) {
be, err := clientutil.NewBackend(config)
if err != nil {
return nil, err
}
var tlsConfig *tls.Config
if config.TLSConfig != nil {
tlsConfig, err = config.TLSConfig.TLSConfig()
if err != nil {
return nil, err
}
}
c := &http.Client{
Transport: clientutil.NewTransport([]string{u.Host}, tlsConfig, nil),
Timeout: 20 * time.Second,
}
return &Client{
Client: c,
backendURL: config.BackendURL,
}, nil
return &ksClient{be}, nil
}
func (c *Client) Open(ctx context.Context, username, password string, ttl int) error {
func (c *ksClient) Open(ctx context.Context, shard, username, password string, ttl int) error {
req := keystore.OpenRequest{
Username: username,
Password: password,
TTL: ttl,
}
var resp keystore.OpenResponse
return clientutil.DoJSONHTTPRequest(ctx, c.Client, c.backendURL+"/api/open", &req, &resp)
return clientutil.DoJSONHTTPRequest(ctx, c.be.Client(shard), c.be.URL(shard)+"/api/open", &req, &resp)
}
func (c *Client) Get(ctx context.Context, username, ssoTicket string) ([]byte, error) {
func (c *ksClient) Get(ctx context.Context, shard, username, ssoTicket string) ([]byte, error) {
req := keystore.GetRequest{
Username: username,
SSOTicket: ssoTicket,
}
var resp keystore.GetResponse
err := clientutil.DoJSONHTTPRequest(ctx, c.Client, c.backendURL+"/api/get", &req, &resp)
err := clientutil.DoJSONHTTPRequest(ctx, c.be.Client(shard), c.be.URL(shard)+"/api/get", &req, &resp)
return resp.Key, err
}
func (c *Client) Close(ctx context.Context, username string) error {
func (c *ksClient) Close(ctx context.Context, shard, username string) error {
req := keystore.CloseRequest{
Username: username,
}
var resp keystore.CloseResponse
return clientutil.DoJSONHTTPRequest(ctx, c.Client, c.backendURL+"/api/close", &req, &resp)
return clientutil.DoJSONHTTPRequest(ctx, c.be.Client(shard), c.be.URL(shard)+"/api/close", &req, &resp)
}
......@@ -9,10 +9,10 @@
"revisionTime": "2017-12-16T15:39:23Z"
},
{
"checksumSHA1": "2X2UMundICtpGTb8pTdBk7PCKss=",
"checksumSHA1": "o+rWKVQIDy79ZwrItwa5/whAL6g=",
"path": "git.autistici.org/ai3/go-common/clientutil",
"revision": "8cedcb1d73128f5566216cb3e39ad1ccea318213",
"revisionTime": "2017-12-16T15:39:23Z"
"revision": "9b20acad90c411c48f7ddc837a35ef3d0d6f98d4",
"revisionTime": "2017-12-17T20:32:41Z"
},
{
"checksumSHA1": "wY0SM35qAhX3P2IZzDnYa068cPw=",
......@@ -39,10 +39,10 @@
"revisionTime": "2017-12-15T13:50:57Z"
},
{
"checksumSHA1": "HGK52MX+2CEKVzb9I5y1BfgDkWQ=",
"checksumSHA1": "MgtHklQMI/3fNcZZzkg+fmQUrCQ=",
"path": "git.autistici.org/id/keystore/client",
"revision": "b09f1210471f6a60402e8ced4783be3889a4074f",
"revisionTime": "2017-12-15T13:50:57Z"
"revision": "ae260514708a9eb3e81b554e1b7b2c65ef802584",
"revisionTime": "2017-12-17T21:51:41Z"
},
{
"checksumSHA1": "usT4LCSQItkFvFOQT7cBlkCuGaE=",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment