Commit ea03535f authored by ale's avatar ale

Add a method to find unused accounts

The server-side part to implementing unused account expiration.
parent 6ffd1a37
Pipeline #6270 passed with stages
in 9 minutes and 36 seconds
......@@ -79,6 +79,9 @@ 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.
`/api/get_unused_accounts` (*GetUnusedAccountsRequest*) -> *GetUnusedAccountsResponse*
Returns accounts that have not been used in a specified amount of time.
# Configuration
......
......@@ -124,3 +124,12 @@ type GetLastLoginRequest struct {
type GetLastLoginResponse struct {
Results []*LastLoginEntry `json:"result"`
}
type GetUnusedAccountsRequest struct {
Usernames []string `json:"usernames"`
Days int `json:"days"`
}
type GetUnusedAccountsResponse struct {
UnusedUsernames []string `json:"unused_usernames"`
}
......@@ -3,6 +3,9 @@ package server
import (
"database/sql"
"errors"
"fmt"
"strings"
"time"
"git.autistici.org/id/usermetadb"
)
......@@ -82,7 +85,6 @@ func (l *lastloginDB) AddLastLogin(entry *usermetadb.LastLoginEntry) error {
}
func (l *lastloginDB) GetLastLogin(username string, service string) ([]*usermetadb.LastLoginEntry, error) {
tx, err := l.db.Begin()
if err != nil {
return nil, err
......@@ -124,3 +126,49 @@ func (l *lastloginDB) GetLastLogin(username string, service string) ([]*usermeta
return entries, rows.Err()
}
func (l *lastloginDB) GetUnusedAccounts(usernames []string, days int) ([]string, error) {
if len(usernames) < 1 {
return nil, errors.New("no usernames given")
}
tx, err := l.db.Begin()
if err != nil {
return nil, err
}
defer tx.Rollback() // nolint
// To find users that have *never* logged in, we run the
// reverse query in SQL (find users that have logged in within
// the time frame), and return the set remainder.
cutoff := time.Now().AddDate(0, 0, -days)
q := fmt.Sprintf(
"SELECT username FROM lastlogin WHERE username IN (?%s) AND timestamp > ?",
strings.Repeat(",?", len(usernames)-1),
)
var args []interface{}
usermap := make(map[string]struct{})
for _, u := range usernames {
args = append(args, u)
usermap[u] = struct{}{}
}
args = append(args, cutoff)
rows, err := tx.Query(q, args...)
if err != nil {
return nil, err
}
for rows.Next() {
var username string
if err := rows.Scan(&username); err != nil {
return nil, err
}
delete(usermap, username)
}
var unused []string
for username := range usermap {
unused = append(unused, username)
}
return unused, rows.Err()
}
......@@ -2,6 +2,7 @@ package server
import (
"os"
"sort"
"testing"
"time"
......@@ -131,3 +132,52 @@ func TestLastLogin_NoUser(t *testing.T) {
t.Fatalf("Expected no entries, found %v", out)
}
}
func TestLastLogin_UnusedAccounts(t *testing.T) {
defer os.Remove("test.db")
db, err := openDB("test.db")
if err != nil {
t.Fatal(err)
}
defer db.Close()
ll, err := newLastloginDB(db)
if err != nil {
t.Fatal(err)
}
defer ll.Close()
entries := []*usermetadb.LastLoginEntry{
&usermetadb.LastLoginEntry{
Timestamp: time.Now().AddDate(0, 0, -30),
Username: "user1",
Service: "service1",
},
&usermetadb.LastLoginEntry{
Timestamp: time.Now().AddDate(0, 0, -1),
Username: "user1",
Service: "service2",
},
&usermetadb.LastLoginEntry{
Timestamp: time.Now().AddDate(0, 0, -30),
Username: "user2",
Service: "service1",
},
}
for _, e := range entries {
if err := ll.AddLastLogin(e); err != nil {
t.Fatal(err)
}
}
// Now look for user1, user2 and user3.
unused, err := ll.GetUnusedAccounts([]string{"user1", "user2", "user3"}, 7)
if err != nil {
t.Fatalf("GetUnusedAccounts: %v", err)
}
sort.Strings(unused)
if diffs := cmp.Diff([]string{"user2", "user3"}, unused); diffs != "" {
t.Fatalf("GetUnusedAccounts returned unexpected result: %s", diffs)
}
}
......@@ -163,6 +163,22 @@ func (s *UserMetaServer) handleGetLastLogin(w http.ResponseWriter, r *http.Reque
serverutil.EncodeJSONResponse(w, &usermetadb.GetLastLoginResponse{Results: entries})
}
func (s *UserMetaServer) handleGetUnusedAccounts(w http.ResponseWriter, r *http.Request) {
var req usermetadb.GetUnusedAccountsRequest
if !serverutil.DecodeJSONRequest(w, r, &req) {
return
}
unused, err := s.lastlogin.GetUnusedAccounts(req.Usernames, req.Days)
if err != nil {
log.Printf("GetUnusedAccounts error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
serverutil.EncodeJSONResponse(w, &usermetadb.GetUnusedAccountsResponse{UnusedUsernames: unused})
}
// Handler returns a http.Handler for the HTTP API.
func (s *UserMetaServer) Handler() http.Handler {
h := http.NewServeMux()
......@@ -172,5 +188,6 @@ func (s *UserMetaServer) Handler() http.Handler {
h.HandleFunc("/api/get_user_logs", s.handleGetUserLogs)
h.HandleFunc("/api/set_last_login", s.handleSetLastLogin)
h.HandleFunc("/api/get_last_login", s.handleGetLastLogin)
h.HandleFunc("/api/get_unused_accounts", s.handleGetUnusedAccounts)
return h
}
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