25 changed files with 850 additions and 196 deletions
@ -0,0 +1,35 @@
|
||||
package db |
||||
|
||||
import ( |
||||
"errors" |
||||
"time" |
||||
) |
||||
|
||||
var ( |
||||
openpgpNotificationsBucket = []byte("openpgp-notifications") |
||||
) |
||||
|
||||
type openpgpNotification struct { |
||||
Fingerprint string |
||||
CreationDate time.Time |
||||
} |
||||
|
||||
// AddOpenpgpNotification stores in the db the dn being notified for their key being expired
|
||||
func (db *DB) AddOpenpgpNotification(dn string, fingerprint string) error { |
||||
return db.put(openpgpNotificationsBucket, dn, openpgpNotification{fingerprint, time.Now()}) |
||||
} |
||||
|
||||
// GetOpenpgpNotification gets the fingerprint latest notification for the dn
|
||||
func (db *DB) GetOpenpgpNotification(dn string) (string, error) { |
||||
var notif openpgpNotification |
||||
err := db.get(openpgpNotificationsBucket, dn, ¬if) |
||||
if errors.Is(err, notFoundError{}) { |
||||
err = nil |
||||
} |
||||
return notif.Fingerprint, err |
||||
} |
||||
|
||||
// ExpireOpenpgpNotifications older than duration
|
||||
func (db *DB) ExpireOpenpgpNotifications(duration time.Duration) error { |
||||
return db.expire(openpgpNotificationsBucket, duration) |
||||
} |
@ -0,0 +1,68 @@
|
||||
package db |
||||
|
||||
import ( |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
const ( |
||||
user = "user" |
||||
fingerprint = "AABBCCDDEEFF1122334455" |
||||
) |
||||
|
||||
func TestAddOpenPGPNotification(t *testing.T) { |
||||
db := initTestDB(t) |
||||
defer delTestDB(db) |
||||
|
||||
fp, err := db.GetOpenpgpNotification(user) |
||||
if err != nil { |
||||
t.Fatalf("Got an error getting openpgp notification: %v", err) |
||||
} |
||||
if fp != "" { |
||||
t.Errorf("Got an unexpected fingerprint: %s", fp) |
||||
} |
||||
|
||||
err = db.AddOpenpgpNotification(user, fingerprint) |
||||
if err != nil { |
||||
t.Fatalf("Got an error adding a openpgp notification: %v", err) |
||||
} |
||||
|
||||
fp, err = db.GetOpenpgpNotification(user) |
||||
if err != nil { |
||||
t.Fatalf("Got an error getting openpgp notification: %v", err) |
||||
} |
||||
if fp != fingerprint { |
||||
t.Errorf("Got an unexpected fingerprint: %s", fp) |
||||
} |
||||
} |
||||
|
||||
func TestExpireOpenpgpNofications(t *testing.T) { |
||||
db := initTestDB(t) |
||||
defer delTestDB(db) |
||||
|
||||
err := db.AddOpenpgpNotification(user, fingerprint) |
||||
if err != nil { |
||||
t.Fatalf("Got an error adding a openpgp notification: %v", err) |
||||
} |
||||
|
||||
fp, err := db.GetOpenpgpNotification(user) |
||||
if err != nil { |
||||
t.Fatalf("Got an error getting openpgp notification: %v", err) |
||||
} |
||||
if fp != fingerprint { |
||||
t.Errorf("Got an unexpected fingerprint: %s", fp) |
||||
} |
||||
|
||||
err = db.ExpireOpenpgpNotifications(time.Microsecond) |
||||
if err != nil { |
||||
t.Fatalf("Got an error expiring openpgp notifications: %v", err) |
||||
} |
||||
|
||||
fp, err = db.GetOpenpgpNotification(user) |
||||
if err != nil { |
||||
t.Fatalf("Got an error getting openpgp notification: %v", err) |
||||
} |
||||
if fp != "" { |
||||
t.Errorf("Got an unexpected fingerprint: %s", fp) |
||||
} |
||||
} |
@ -0,0 +1,70 @@
|
||||
package ldap |
||||
|
||||
import ( |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/go-ldap/ldap/v3" |
||||
) |
||||
|
||||
var openPGPAttributes = []string{"openPGPKey", "openPGPId", "openPGPExpiry", "openPGPKeyHash"} |
||||
|
||||
type OpenPGPkey struct { |
||||
Fingerprint string |
||||
Expiry time.Time |
||||
Key []byte |
||||
WkdHash string |
||||
} |
||||
|
||||
func (l Ldap) changeOpenPGPkey(dn string, fingerprint string, expiry time.Time, key []byte, wkdHash string, email string) error { |
||||
conn, err := l.connect() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer conn.Close() |
||||
|
||||
modifyRequest := ldap.NewModifyRequest(dn, nil) |
||||
modifyRequest.Add("objectClass", []string{"openPGP"}) |
||||
modifyRequest.Replace("openPGPId", []string{fingerprint}) |
||||
modifyRequest.Replace("openPGPExpiry", []string{expiry.Format(dateFormat)}) |
||||
modifyRequest.Replace("openPGPKey", []string{string(key)}) |
||||
modifyRequest.Replace("openPGPKeyHash", []string{wkdHash}) |
||||
if email != "" { |
||||
modifyRequest.Replace("mail", []string{email}) |
||||
} |
||||
return conn.Modify(modifyRequest) |
||||
} |
||||
|
||||
func (l Ldap) DeleteOpenPGPkey(dn string) error { |
||||
conn, err := l.connect() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer conn.Close() |
||||
|
||||
modifyRequest := ldap.NewModifyRequest(dn, nil) |
||||
modifyRequest.Delete("objectClass", []string{"openPGP"}) |
||||
modifyRequest.Delete("openPGPId", []string{}) |
||||
modifyRequest.Delete("openPGPExpiry", []string{}) |
||||
modifyRequest.Delete("openPGPKey", []string{}) |
||||
modifyRequest.Delete("openPGPKeyHash", []string{}) |
||||
if strings.Contains(strings.ToLower(dn), "ou=group") { |
||||
modifyRequest.Delete("mail", []string{}) |
||||
} |
||||
return conn.Modify(modifyRequest) |
||||
} |
||||
|
||||
func openPGPkey(entry *ldap.Entry) *OpenPGPkey { |
||||
openPGPexpiry, _ := time.Parse(dateFormat, entry.GetAttributeValue("openPGPExpiry")) |
||||
key := OpenPGPkey{ |
||||
Fingerprint: entry.GetAttributeValue("openPGPId"), |
||||
Expiry: openPGPexpiry, |
||||
Key: []byte(entry.GetAttributeValue("openPGPKey")), |
||||
WkdHash: entry.GetAttributeValue("openPGPKeyHash"), |
||||
} |
||||
|
||||
if len(key.Key) == 0 || key.Fingerprint == "" || key.WkdHash == "" { |
||||
return nil |
||||
} |
||||
return &key |
||||
} |
@ -0,0 +1,115 @@
|
||||
package ldap |
||||
|
||||
import ( |
||||
"bytes" |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
func TestOpenPGPuser(t *testing.T) { |
||||
key := []byte("openpgpkey") |
||||
fingerprint := "AABBCCDDEEFF1122334455" |
||||
wkdHash := "hashhash" |
||||
|
||||
l := testLdap(t) |
||||
u, err := l.GetUser(user) |
||||
if err != nil { |
||||
t.Errorf("GetUser() failed: %v", err) |
||||
} |
||||
if u.OpenPGPkey != nil { |
||||
t.Errorf("user already has a key") |
||||
} |
||||
|
||||
dn := l.userDN(user) |
||||
err = l.changeOpenPGPkey(dn, fingerprint, time.Time{}, key, wkdHash, "") |
||||
if err != nil { |
||||
t.Errorf("ChangeOpenPGPkey() failed: %v", err) |
||||
} |
||||
|
||||
u, err = l.GetUser(user) |
||||
if err != nil { |
||||
t.Errorf("GetUser() failed: %v", err) |
||||
} |
||||
if u.OpenPGPkey == nil { |
||||
t.Fatal("user doesn't have a key") |
||||
} |
||||
if u.OpenPGPkey.Fingerprint != fingerprint { |
||||
t.Errorf("fingeprint doesn't match: %s", u.OpenPGPkey.Fingerprint) |
||||
} |
||||
if !bytes.Equal(u.OpenPGPkey.Key, key) { |
||||
t.Errorf("key doesn't match: %s", u.OpenPGPkey.Key) |
||||
} |
||||
if u.OpenPGPkey.WkdHash != wkdHash { |
||||
t.Errorf("wkdHash doesn't match: %s", u.OpenPGPkey.WkdHash) |
||||
} |
||||
if !u.OpenPGPkey.Expiry.IsZero() { |
||||
t.Errorf("expiry is not zero: %v", u.OpenPGPkey.Expiry) |
||||
} |
||||
|
||||
err = l.DeleteOpenPGPkey(dn) |
||||
if err != nil { |
||||
t.Errorf("DeleteOpenPGPkey() failed: %v", err) |
||||
} |
||||
|
||||
u, err = l.GetUser(user) |
||||
if err != nil { |
||||
t.Errorf("GetUser() failed: %v", err) |
||||
} |
||||
if u.OpenPGPkey != nil { |
||||
t.Errorf("user already has a key") |
||||
} |
||||
} |
||||
|
||||
func TestOpenPGPgroup(t *testing.T) { |
||||
key := []byte("openpgpkey") |
||||
fingerprint := "AABBCCDDEEFF1122334455" |
||||
wkdHash := "hashhash" |
||||
|
||||
l := testLdap(t) |
||||
g, err := l.GetGroup(group) |
||||
if err != nil { |
||||
t.Errorf("GetGRoup() failed: %v", err) |
||||
} |
||||
if g.OpenPGPkey != nil { |
||||
t.Errorf("user already has a key") |
||||
} |
||||
|
||||
dn := l.groupDN(group) |
||||
err = l.changeOpenPGPkey(dn, fingerprint, time.Time{}, key, wkdHash, group+"@nodomain") |
||||
if err != nil { |
||||
t.Errorf("ChangeOpenPGPkey() failed: %v", err) |
||||
} |
||||
|
||||
g, err = l.GetGroup(group) |
||||
if err != nil { |
||||
t.Errorf("GetGRoup() failed: %v", err) |
||||
} |
||||
if g.OpenPGPkey == nil { |
||||
t.Fatal("user doesn't have a key") |
||||
} |
||||
if g.OpenPGPkey.Fingerprint != fingerprint { |
||||
t.Errorf("fingeprint doesn't match: %s", g.OpenPGPkey.Fingerprint) |
||||
} |
||||
if !bytes.Equal(g.OpenPGPkey.Key, key) { |
||||
t.Errorf("key doesn't match: %s", g.OpenPGPkey.Key) |
||||
} |
||||
if g.OpenPGPkey.WkdHash != wkdHash { |
||||
t.Errorf("wkdHash doesn't match: %s", g.OpenPGPkey.WkdHash) |
||||
} |
||||
if !g.OpenPGPkey.Expiry.IsZero() { |
||||
t.Errorf("expiry is not zero: %v", g.OpenPGPkey.Expiry) |
||||
} |
||||
|
||||
err = l.DeleteOpenPGPkey(dn) |
||||
if err != nil { |
||||
t.Errorf("DeleteOpenPGPkey() failed: %v", err) |
||||
} |
||||
|
||||
g, err = l.GetGroup(group) |
||||
if err != nil { |
||||
t.Errorf("GetGRoup() failed: %v", err) |
||||
} |
||||
if g.OpenPGPkey != nil { |
||||
t.Errorf("user already has a key") |
||||
} |
||||
} |
@ -0,0 +1,123 @@
|
||||
package server |
||||
|
||||
import ( |
||||
"log" |
||||
"math" |
||||
"time" |
||||
|
||||
"git.sindominio.net/sindominio/lowry/ldap" |
||||
) |
||||
|
||||
var ( |
||||
inviteExpireDuration = time.Hour * 24 * 30 // 30 days
|
||||
collectiveExpireDuration = time.Hour * 24 * 90 // 90 days
|
||||
accountExpireDuration = time.Hour * 24 * 90 // 90 days
|
||||
accountBlockDuration = time.Hour * 24 * 6 * 30 // ~ 6 months
|
||||
accountDeleteDuration = time.Hour * 24 * 365 // ~ 1 year
|
||||
notifyKeyExpiredDuration = time.Hour * 24 * 30 // 30 days
|
||||
) |
||||
|
||||
// Cleanup runs periodic clean up tasks
|
||||
func (s *Server) Cleanup(noLockUsers bool) { |
||||
for { |
||||
users, err := s.ldap.ListUsers() |
||||
if err != nil { |
||||
log.Printf("Error listing users for updating: %v", err) |
||||
} else { |
||||
for _, u := range users { |
||||
if !noLockUsers { |
||||
s.updateUserLock(u) |
||||
} |
||||
s.checkKeyExpiration(u.DN, u.OpenPGPkey, u.Name, u.Mail) |
||||
} |
||||
} |
||||
|
||||
collectives, err := s.ldap.ListGroups() |
||||
if err != nil { |
||||
log.Printf("Error listing collectives for updating: %v", err) |
||||
} else { |
||||
for _, c := range collectives { |
||||
mail := c.Name + "@" + s.domain |
||||
s.checkKeyExpiration(c.DN, c.OpenPGPkey, c.Name, mail) |
||||
} |
||||
} |
||||
|
||||
s.expireDBEntries() |
||||
time.Sleep(time.Minute * 61) |
||||
} |
||||
} |
||||
|
||||
func (s *Server) updateUserLock(u ldap.User) { |
||||
if u.Shell == "/bin/false" && u.Role == ldap.Sindominante { |
||||
err := s.ldap.ChangeShell(u.Name, "/bin/bash") |
||||
if err != nil { |
||||
log.Println("An error ocurred changing shell of '", u.Name, "': ", err) |
||||
} |
||||
} |
||||
|
||||
newLocked := ldap.Unknown |
||||
sinceLastLogin := time.Now().Sub(u.LastLogin) |
||||
if u.Locked != ldap.Deleted && sinceLastLogin > accountDeleteDuration { |
||||
newLocked = ldap.Deleted |
||||
} else if u.Locked != ldap.Blocked && sinceLastLogin > accountBlockDuration && sinceLastLogin < accountDeleteDuration { |
||||
newLocked = ldap.Blocked |
||||
} else { |
||||
return |
||||
} |
||||
|
||||
err := s.ldap.ChangeLocked(u.Name, newLocked) |
||||
if err != nil { |
||||
log.Printf("Error changing locked to %s for user %s: %v", newLocked.String(), u.Name, err) |
||||
} |
||||
if u.Role == ldap.Sindominante { |
||||
err = s.ldap.ChangeRole(u.Name, ldap.Amiga) |
||||
if err != nil { |
||||
log.Printf("Error changing role for blocked user %s: %v", u.Name, err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
type NotificationData struct { |
||||
Name string |
||||
Fingerprint string |
||||
Days int |
||||
} |
||||
|
||||
func (s *Server) checkKeyExpiration(dn string, key *ldap.OpenPGPkey, name string, mail string) { |
||||
if key == nil { |
||||
return |
||||
} |
||||
|
||||
if key.Expiry.IsZero() { |
||||
return |
||||
} |
||||
|
||||
if key.Expiry.Before(time.Now()) { |
||||
s.ldap.DeleteOpenPGPkey(dn) |
||||
} |
||||
|
||||
notifiedFingerprint, err := s.db.GetOpenpgpNotification(dn) |
||||
if err != nil { |
||||
log.Printf("An error has occurred accessing the user %s last notification: %v", name, err) |
||||
} |
||||
if key.Expiry.Add(-notifyKeyExpiredDuration).Before(time.Now()) && notifiedFingerprint != key.Fingerprint { |
||||
data := NotificationData{ |
||||
Name: name, |
||||
Fingerprint: key.Fingerprint, |
||||
Days: int(math.Round(time.Now().Sub(key.Expiry).Hours())), |
||||
} |
||||
s.mail.Send([]string{mail}, "openpgp_expire", data) |
||||
|
||||
err = s.db.AddOpenpgpNotification(name, key.Fingerprint) |
||||
if err != nil { |
||||
log.Printf("An error has occurred storing user %s last notification: %v", name, err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Server) expireDBEntries() { |
||||
s.db.ExpireInvites(inviteExpireDuration) |
||||
s.db.ExpireAccounts(accountExpireDuration) |
||||
s.db.ExpireCollectives(collectiveExpireDuration) |
||||
s.db.ExpireOpenpgpNotifications(notifyKeyExpiredDuration) |
||||
} |
@ -0,0 +1,180 @@
|
||||
package server |
||||
|
||||
import ( |
||||
"crypto/sha1" |
||||
"errors" |
||||
"fmt" |
||||
"log" |
||||
"net/http" |
||||
"strings" |
||||
"time" |
||||
|
||||
"git.sindominio.net/sindominio/lowry/ldap" |
||||
"github.com/ProtonMail/gopenpgp/v2/crypto" |
||||
"github.com/gorilla/mux" |
||||
"github.com/tv42/zbase32" |
||||
) |
||||
|
||||
var ( |
||||
ErrInvalidKey = errors.New("Key is not valid") |
||||
ErrNoKeyId = errors.New("The key doesn't contain a valid user id") |
||||
ErrExpiredKey = errors.New("The key is expired") |
||||
) |
||||
|
||||
type openPGPdata struct { |
||||
Error string |
||||
Success bool |
||||
Key string |
||||
Fingerprint string |
||||
Expiry string |
||||
Collective string |
||||
} |
||||
|
||||
func (s *Server) openPGPkeyHandler(w http.ResponseWriter, r *http.Request) { |
||||
response := s.newResponse("openpgp", w, r) |
||||
if response.User == "" { |
||||
http.Redirect(w, r, "/", http.StatusFound) |
||||
return |
||||
} |
||||
|
||||
vars := mux.Vars(r) |
||||
collective := vars["name"] |
||||
|
||||
var data openPGPdata |
||||
if r.Method == "POST" { |
||||
formKey := r.FormValue("key") |
||||
name := response.User |
||||
if collective != "" { |
||||
if isInCollective, _ := s.isUserinCollective(response.User, collective); !isInCollective { |
||||
log.Println("User", response.User, "doesn't have permissions to modify the openPGP key of", collective) |
||||
s.errorHandler(w, r) |
||||
return |
||||
} |
||||
name = collective |
||||
} |
||||
err := s.changeOpenPGPkey(name, formKey, collective != "") |
||||
if err != nil { |
||||
switch err { |
||||
case ErrInvalidKey: |
||||
data.Error = "invalid" |
||||
case ErrNoKeyId: |
||||
data.Error = "no-identity" |
||||
case ErrExpiredKey: |
||||
data.Error = "expired" |
||||
default: |
||||
s.errorHandler(w, r) |
||||
return |
||||
} |
||||
data.Key = formKey |
||||
} else { |
||||
data.Success = true |
||||
} |
||||
} |
||||
|
||||
var key *ldap.OpenPGPkey |
||||
if collective != "" { |
||||
collectiveData, err := s.ldap.GetGroup(collective) |
||||
if err != nil { |
||||
log.Println("Error loading", collective, "profile:", err) |
||||
s.errorHandler(w, r) |
||||
return |
||||
} |
||||
key = collectiveData.OpenPGPkey |
||||
data.Collective = collective |
||||
} else { |
||||
userData, err := s.ldap.GetUser(response.User) |
||||
if err != nil { |
||||
log.Println("Error loading", response.User, "profile:", err) |
||||
s.errorHandler(w, r) |
||||
return |
||||
} |
||||
key = userData.OpenPGPkey |
||||
} |
||||
|
||||
if key != nil { |
||||
if data.Key == "" { |
||||
var err error |
||||
data.Key, err = getArmoredKey(key.Key) |
||||
if err != nil { |
||||
log.Println("Error getting", response.User, "armored key:", err) |
||||
s.errorHandler(w, r) |
||||
return |
||||
} |
||||
} |
||||
|
||||
data.Fingerprint = key.Fingerprint |
||||
if !key.Expiry.IsZero() { |
||||
data.Expiry = key.Expiry.Format("01/02/2006") |
||||
} |
||||
} |
||||
|
||||
response.execute(data) |
||||
} |
||||
|
||||
func (s *Server) changeOpenPGPkey(name, formKey string, collective bool) error { |
||||
key, err := crypto.NewKeyFromArmored(formKey) |
||||
if err != nil { |
||||
log.Println("Can't parse key for", name, ":", err) |
||||
return ErrInvalidKey |
||||
} |
||||
fingerprint := strings.ToUpper(key.GetFingerprint()) |
||||
|
||||
email := name + "@" + s.domain |
||||
found := false |
||||
for _, identity := range key.GetEntity().Identities { |
||||
if identity.UserId.Email == email { |
||||
found = true |
||||
break |
||||
} |
||||
} |
||||
if !found { |
||||
log.Println("User email", email, "not in key", fingerprint) |
||||
return ErrNoKeyId |
||||
} |
||||
|
||||
pubKey, err := key.GetPublicKey() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
expiry := getEntityExpiry(key) |
||||
if !expiry.IsZero() && expiry.Before(time.Now()) { |
||||
return ErrExpiredKey |
||||
} |
||||
wkdHash := fmt.Sprintf("%s@%s", calculateWKDhash(name), s.domain) |
||||
|
||||
if collective { |
||||
err = s.ldap.ChangeGroupOpenPGPkey(name, fingerprint, expiry, pubKey, wkdHash) |
||||
} else { |
||||
err = s.ldap.ChangeUserOpenPGPkey(name, fingerprint, expiry, pubKey, wkdHash) |
||||
} |
||||
return err |
||||
} |
||||
|
||||
func getArmoredKey(key []byte) (string, error) { |
||||
okey, err := crypto.NewKey(key) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
return okey.GetArmoredPublicKey() |
||||
|
||||
} |
||||
|
||||