Commit e06ac2a9 authored by shammash's avatar shammash

Merge branch 'last_login' into 'master'

Last login

See merge request !3
parents b0d2931a 12462a89
Pipeline #2216 passed with stages
in 1 minute and 33 seconds
......@@ -116,6 +116,8 @@ Each service definition is a dictionary with the following attributes:
only for interactive services)
* `enforce_2fa` is a boolean flag that, when true, will disable
non-2FA logins for this service
* `enable_last_login_reporting` is a boolean flag that enables last login
reporting to usermetadb
* `enable_device_tracking` is a boolean flag that enables device
tracking for this service (assuming the client provides device
information)
......
......@@ -203,6 +203,17 @@ func createService(config *Config, sc *ServiceConfig, backends map[string]userBa
}
}
if sc.EnableLastLoginReporting {
if config.UserMetaDBConfig == nil {
return nil, errors.New("usermetadb config is missing")
}
llr, err := newLastLoginFilter(config.UserMetaDBConfig)
if err != nil {
return nil, err
}
s.filters = append(s.filters, llr)
}
// Enabling device tracking also enables user activity
// logging.
if sc.EnableDeviceTracking {
......
......@@ -19,11 +19,12 @@ type BackendSpec struct {
// ServiceConfig defines the authentication backends for a service.
type ServiceConfig struct {
BackendSpecs []*BackendSpec `yaml:"backends"`
ChallengeResponse bool `yaml:"challenge_response"`
Enforce2FA bool `yaml:"enforce_2fa"`
EnableDeviceTracking bool `yaml:"enable_device_tracking"`
Ratelimits []string `yaml:"rate_limits"`
BackendSpecs []*BackendSpec `yaml:"backends"`
ChallengeResponse bool `yaml:"challenge_response"`
Enforce2FA bool `yaml:"enforce_2fa"`
EnableLastLoginReporting bool `yaml:"enable_last_login_reporting"`
EnableDeviceTracking bool `yaml:"enable_device_tracking"`
Ratelimits []string `yaml:"rate_limits"`
}
// Config for the authentication server.
......
package server
import (
"context"
"log"
"time"
"git.autistici.org/ai3/go-common/clientutil"
"git.autistici.org/id/auth"
"git.autistici.org/id/usermetadb"
"git.autistici.org/id/usermetadb/client"
)
type setLastLoginClient interface {
SetLastLogin(context.Context, string, *usermetadb.LastLoginEntry) error
}
type lastloginFilter struct {
client setLastLoginClient
}
func newLastLoginFilter(config *clientutil.BackendConfig) (*lastloginFilter, error) {
c, err := client.New(config)
if err != nil {
return nil, err
}
return &lastloginFilter{c}, nil
}
var lastloginTimeout = 30 * time.Second
func (f *lastloginFilter) Filter(user *User, req *auth.Request, resp *auth.Response) *auth.Response {
if resp.Status != auth.StatusOK {
return resp
}
entry := usermetadb.LastLoginEntry{
Timestamp: time.Now(),
Username: user.Name,
Service: req.Service,
}
// Make the log RPC in the background, no need to wait for it to complete.
go func() {
ctx, cancel := context.WithTimeout(context.Background(), lastloginTimeout)
defer cancel()
if err := f.client.SetLastLogin(ctx, user.Shard, &entry); err != nil {
log.Printf("usermetadb.SetLastLogin error for %s: %v", user.Name, err)
}
}()
return resp
}
......@@ -26,6 +26,11 @@ a specific device and an account if one is in possession of the
server-side log database (only partially mitigated by the fact that
the cookie is encrypted).
`usermetadb` also stores last-login information for internal infrastructure
maintenance. The idea is to retain the minimal amount of information to perform
tasks such as "disable accounts that have not been active for more than N
years".
# API
The server exports an API over HTTP/HTTPS, all requests should be made
......@@ -39,14 +44,14 @@ and the *analysis* API.
## Log API
`/api/add_log` (*AddLogRequest*)
Stores a new log entry for a user in the database. The request must be
a `LogEntry` object. The method returns an empty response. If the log
entry contains device information, the list of devices for the
specified user is updated with that information.
`/api/get_user_logs` (*GetUserLogsRequest*) -> *GetUserLogsResponse*
Returns recent logs for a specific user.
`/api/get_user_devices` (*GetUserDevicesRequest*) -> *GetUserDevicesResponse*
......@@ -60,6 +65,20 @@ Returns the list of known devices for a user.
Returns information about a device, whether we have seen it before, if
the localization information matches the historical trend, etc.
## Last-login API
`/api/set_last_login` (*SetLastLoginRequest*)
Stores the last login of a user in the database. The request must be a
`LastLoginEntry` object. The method returns an empty response. The service name
must be specified in the last login entry.
`/api/get_last_login` (*GetLastLoginRequest*) -> *GetLastLoginResponse*
Returns the last login of a given user. If the service name is specified it
returns the last login for that specific service, otherwise return last login
for all services.
# Configuration
......
......@@ -15,6 +15,8 @@ type Client interface {
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)
SetLastLogin(context.Context, string, *usermetadb.LastLoginEntry) error
GetLastLogin(context.Context, string, string, string) ([]*usermetadb.LastLoginEntry, error)
}
type udbClient struct {
......@@ -63,3 +65,19 @@ func (c *udbClient) GetUserLogs(ctx context.Context, shard, username string, max
err := c.be.Call(ctx, shard, "/api/get_user_logs", &req, &resp)
return resp.Results, err
}
func (c *udbClient) SetLastLogin(ctx context.Context, shard string, entry *usermetadb.LastLoginEntry) error {
req := usermetadb.SetLastLoginRequest{LastLogin: entry}
return c.be.Call(ctx, shard, "/api/set_last_login", &req, nil)
}
func (c *udbClient) GetLastLogin(ctx context.Context, shard string, username string, service string) ([]*usermetadb.LastLoginEntry, error) {
req := usermetadb.GetLastLoginRequest{
Username: username,
Service: service,
}
var resp usermetadb.GetLastLoginResponse
err := c.be.Call(ctx, shard, "/api/get_last_login", &req, &resp)
return resp.Results, err
}
......@@ -92,3 +92,35 @@ type GetUserLogsRequest struct {
type GetUserLogsResponse struct {
Results []*LogEntry `json:"result"`
}
type LastLoginEntry struct {
Timestamp time.Time `json:"timestamp"`
Username string `json:"username"`
Service string `json:"service"`
}
func (e *LastLoginEntry) Validate() error {
if e.Username == "" {
return errors.New("invalid last login entry: missing username")
}
if e.Service == "" {
return errors.New("invalid last login entry: missing service")
}
return nil
}
type SetLastLoginRequest struct {
LastLogin *LastLoginEntry `json:"last_login"`
}
type SetLastLoginResponse struct{}
type GetLastLoginRequest struct {
Username string `json:"username"`
Service string `json:"service,omitempty"`
}
type GetLastLoginResponse struct {
Results []*LastLoginEntry `json:"result"`
}
......@@ -39,16 +39,16 @@
"revisionTime": "2019-01-29T12:17:45Z"
},
{
"checksumSHA1": "NtTOmajRf6xh0mkVSm18L70ncl0=",
"checksumSHA1": "J0QeD9LVccFOejgPKa0td8JD0rY=",
"path": "git.autistici.org/id/usermetadb",
"revision": "fa081ac5509ba4ed97ab6a447d611c1adcf9e764",
"revisionTime": "2018-11-03T07:16:06Z"
"revision": "61e5a7b24130b36be4964ffadae1b4a9ef803a7a",
"revisionTime": "2019-02-09T10:52:39Z"
},
{
"checksumSHA1": "kwosXxbzygo9ZUZSYvD+WiAbM3Q=",
"checksumSHA1": "HnFofi1vg8of8d1uVSRUX7LIAxI=",
"path": "git.autistici.org/id/usermetadb/client",
"revision": "fa081ac5509ba4ed97ab6a447d611c1adcf9e764",
"revisionTime": "2018-11-03T07:16:06Z"
"revision": "61e5a7b24130b36be4964ffadae1b4a9ef803a7a",
"revisionTime": "2019-02-09T10:52:39Z"
},
{
"checksumSHA1": "yReqUM4tQkY+1YEI+L2d0SOzFWs=",
......
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