Commit a430981a authored by ale's avatar ale

Support partitioned user-meta-server

Uses the clientutil.Backend abstraction for the user-meta-server
client API.
parent 1ac213f9
Pipeline #723 passed with stages
in 58 seconds
......@@ -55,7 +55,7 @@ The YAML file should contain a dictionary with the following attributes:
see the *LDAP Backend* section below
* `user_meta_server` holds the configuration for the user-meta-server
backend:
* `backend_url` is the URL of the service
* `url` is the URL of the service
* `tls_config` configures TLS for the client:
* `cert` is the path to the client certificate
* `key` is the path to the client private key
......
......@@ -15,8 +15,7 @@ import (
"github.com/tstranex/u2f"
"gopkg.in/yaml.v2"
usermetadbclient "git.autistici.org/id/usermetadb/client"
"git.autistici.org/ai3/go-common/clientutil"
"git.autistici.org/id/auth"
)
......@@ -214,7 +213,7 @@ type Config struct {
RateLimiters map[string]*authRatelimiterConfig `yaml:"rate_limits"`
// Configuration for the user-meta-server backend.
UserMetaDBConfig *usermetadbclient.Config `yaml:"user_meta_server"`
UserMetaDBConfig *clientutil.BackendConfig `yaml:"user_meta_server"`
// Runtime versions of the above. These objects are shared by
// all services, as they contain the actual map data.
......
......@@ -5,6 +5,7 @@ import (
"log"
"time"
"git.autistici.org/ai3/go-common/clientutil"
"git.autistici.org/id/usermetadb/client"
"git.autistici.org/id/auth"
......@@ -12,7 +13,7 @@ import (
// Inject an interface for testing purposes.
type checkDeviceClient interface {
CheckDevice(context.Context, string, *auth.DeviceInfo) (bool, error)
CheckDevice(context.Context, string, string, *auth.DeviceInfo) (bool, error)
}
type deviceFilter struct {
......@@ -21,7 +22,7 @@ type deviceFilter struct {
var usermetadbTimeout = 3 * time.Second
func newDeviceFilter(config *client.Config) (*deviceFilter, error) {
func newDeviceFilter(config *clientutil.BackendConfig) (*deviceFilter, error) {
c, err := client.New(config)
if err != nil {
return nil, err
......@@ -39,7 +40,7 @@ func (f *deviceFilter) Filter(user *User, req *auth.Request, resp *auth.Response
// OK and don't need to do anything else.
ctx, cancel := context.WithTimeout(context.Background(), usermetadbTimeout)
defer cancel()
seen, err := f.client.CheckDevice(ctx, user.Name, req.DeviceInfo)
seen, err := f.client.CheckDevice(ctx, user.Shard, user.Name, req.DeviceInfo)
if err != nil {
log.Printf("usermetadb error for %s: %v", user.Name, err)
return resp
......
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,
}
}
......@@ -125,6 +125,9 @@ func (b *balancer) dial(ctx context.Context, network, addr string) (net.Conn, er
if err == nil {
return conn, nil
} else if err == context.Canceled {
// A timeout might be bad, set the error bit
// on the connection.
b.notify(addr, false)
return nil, err
}
b.notify(addr, false)
......
......@@ -2,10 +2,6 @@ package client
import (
"context"
"crypto/tls"
"net/http"
"net/url"
"time"
"git.autistici.org/ai3/go-common/clientutil"
"git.autistici.org/id/auth"
......@@ -14,73 +10,56 @@ import (
)
// Client for the user-meta-server API.
type Client struct {
*http.Client
backendURL string
type Client interface {
CheckDevice(context.Context, string, string, *auth.DeviceInfo) (bool, error)
AddLog(context.Context, string, *usermetadb.LogEntry) error
GetUserDevices(context.Context, string, string) ([]*usermetadb.MetaDeviceInfo, error)
GetUserLogs(context.Context, string, string, int, int) ([]*usermetadb.LogEntry, error)
}
// Config for a user-meta-server API client.
type Config struct {
BackendURL string `yaml:"backend_url"`
TLSConfig *clientutil.TLSClientConfig `yaml:"tls_config"`
type udbClient 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)
func New(config *clientutil.BackendConfig) (Client, 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: 10 * time.Second,
}
return &Client{
Client: c,
backendURL: config.BackendURL,
}, nil
return &udbClient{be}, nil
}
func (c *Client) CheckDevice(ctx context.Context, username string, dev *auth.DeviceInfo) (bool, error) {
func (c *udbClient) CheckDevice(ctx context.Context, shard, username string, dev *auth.DeviceInfo) (bool, error) {
req := usermetadb.CheckDeviceRequest{
Username: username,
DeviceInfo: dev,
}
var resp usermetadb.CheckDeviceResponse
err := clientutil.DoJSONHTTPRequest(ctx, c.Client, c.backendURL+"/api/check_device", &req, &resp)
err := clientutil.DoJSONHTTPRequest(ctx, c.be.Client(shard), c.be.URL(shard)+"/api/check_device", &req, &resp)
return resp.Seen, err
}
func (c *Client) AddLog(ctx context.Context, entry *usermetadb.LogEntry) error {
func (c *udbClient) AddLog(ctx context.Context, shard string, entry *usermetadb.LogEntry) error {
req := usermetadb.AddLogRequest{Log: entry}
return clientutil.DoJSONHTTPRequest(ctx, c.Client, c.backendURL+"/api/add_log", &req, nil)
return clientutil.DoJSONHTTPRequest(ctx, c.be.Client(shard), c.be.URL(shard)+"/api/add_log", &req, nil)
}
func (c *Client) GetUserDevices(ctx context.Context, username string) ([]*usermetadb.MetaDeviceInfo, error) {
func (c *udbClient) GetUserDevices(ctx context.Context, shard, username string) ([]*usermetadb.MetaDeviceInfo, error) {
req := usermetadb.GetUserDevicesRequest{Username: username}
var resp usermetadb.GetUserDevicesResponse
err := clientutil.DoJSONHTTPRequest(ctx, c.Client, c.backendURL+"/api/get_user_devices", &req, &resp)
err := clientutil.DoJSONHTTPRequest(ctx, c.be.Client(shard), c.be.URL(shard)+"/api/get_user_devices", &req, &resp)
return resp.Devices, err
}
func (c *Client) GetUserLogs(ctx context.Context, username string, maxDays, limit int) ([]*usermetadb.LogEntry, error) {
func (c *udbClient) GetUserLogs(ctx context.Context, shard, username string, maxDays, limit int) ([]*usermetadb.LogEntry, error) {
req := usermetadb.GetUserLogsRequest{
Username: username,
MaxDays: maxDays,
Limit: limit,
}
var resp usermetadb.GetUserLogsResponse
err := clientutil.DoJSONHTTPRequest(ctx, c.Client, c.backendURL+"/api/get_user_logs", &req, &resp)
err := clientutil.DoJSONHTTPRequest(ctx, c.be.Client(shard), c.be.URL(shard)+"/api/get_user_logs", &req, &resp)
return resp.Results, err
}
......@@ -9,10 +9,10 @@
"revisionTime": "2017-12-16T15:39:23Z"
},
{
"checksumSHA1": "jFlhSIit/5+VAIUu1cc7EVVlw0M=",
"checksumSHA1": "o+rWKVQIDy79ZwrItwa5/whAL6g=",
"path": "git.autistici.org/ai3/go-common/clientutil",
"revision": "86a36cf5da88919ee7d9ec12d1a92043b16fcc9c",
"revisionTime": "2017-12-10T11:04:55Z"
"revision": "9b20acad90c411c48f7ddc837a35ef3d0d6f98d4",
"revisionTime": "2017-12-17T20:32:41Z"
},
{
"checksumSHA1": "mEnXMNziH82HFtGngHU19VHTVHs=",
......@@ -29,14 +29,14 @@
{
"checksumSHA1": "7Kbb9vTjqcQhhxtSGpmp9rk6PUk=",
"path": "git.autistici.org/id/usermetadb",
"revision": "870324b771df3ab35be9b881a1c4981260e18b5d",
"revisionTime": "2017-12-09T12:27:05Z"
"revision": "438e07d22e6891ae0c371c31003c66756c1f0ffa",
"revisionTime": "2017-12-17T21:36:12Z"
},
{
"checksumSHA1": "WkbTf01lQyXPIjdrmQLNQSCGauE=",
"checksumSHA1": "eEUTBX7C9TjnWcY2O9kT/88SClQ=",
"path": "git.autistici.org/id/usermetadb/client",
"revision": "870324b771df3ab35be9b881a1c4981260e18b5d",
"revisionTime": "2017-12-09T12:27:05Z"
"revision": "438e07d22e6891ae0c371c31003c66756c1f0ffa",
"revisionTime": "2017-12-17T21:36:12Z"
},
{
"checksumSHA1": "spyv5/YFBjYyZLZa1U2LBfDR8PM=",
......
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