Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • better-validation
  • master
  • renovate/git.autistici.org-id-auth-digest
  • renovate/github.com-go-ldap-ldap-v3-3.x
  • renovate/github.com-pquerna-otp-1.x
  • renovate/github.com-prometheus-client_golang-1.x
  • renovate/github.com-protonmail-gopenpgp-v2-2.x
  • renovate/github.com-protonmail-gopenpgp-v3-3.x
  • renovate/go-1.x
  • renovate/golang.org-x-crypto-0.x
  • renovate/golang.org-x-net-0.x
  • renovate/golang.org-x-sync-0.x
12 results

Target

Select target project
  • ai3/accountserver
  • svp-bot/accountserver
2 results
Select Git revision
  • better-validation
  • lintian-fixes
  • master
  • renovate/github.com-patrickmn-go-cache-2.x
  • renovate/golang.org-x-crypto-digest
  • renovate/golang.org-x-net-digest
6 results
Show changes
Commits on Source (248)
Showing
with 521 additions and 176 deletions
include: "https://git.autistici.org/ai3/build-deb/raw/master/ci-buster-backports.yml" include:
- "https://git.autistici.org/pipelines/debian/raw/master/common.yml"
- "https://git.autistici.org/pipelines/images/test/golang/raw/master/ci.yml"
variables:
GO_TEST_ARGS: "--tags integration"
GO_TEST_PACKAGES: "default-jre-headless"
...@@ -251,7 +251,10 @@ func randomAppSpecificPasswordID() string { ...@@ -251,7 +251,10 @@ func randomAppSpecificPasswordID() string {
} }
func generateTOTPSecret() (string, error) { func generateTOTPSecret() (string, error) {
key, err := totp.Generate(totp.GenerateOpts{}) key, err := totp.Generate(totp.GenerateOpts{
Issuer: "accountserver",
AccountName: "placeholder",
})
if err != nil { if err != nil {
return "", err return "", err
} }
......
...@@ -95,7 +95,7 @@ func (r *CreateResourcesRequest) Validate(rctx *RequestContext) error { ...@@ -95,7 +95,7 @@ func (r *CreateResourcesRequest) Validate(rctx *RequestContext) error {
// resources do not violate user invariants). // resources do not violate user invariants).
if err := checkUserInvariants(tplUser); err != nil { if err := checkUserInvariants(tplUser); err != nil {
log.Printf("validation error while creating resources: %v", err) log.Printf("validation error while creating resources: %v", err)
return newValidationError(nil, "user", err.Error()) return newValidationError("user", err)
} }
} }
......
package accountserver package accountserver
import ( import (
"errors"
"fmt" "fmt"
"log" "log"
"strings"
) )
// GetResourceRequest requests a specific resource. // GetResourceRequest requests a specific resource.
...@@ -39,7 +39,7 @@ type SearchResourceRequest struct { ...@@ -39,7 +39,7 @@ type SearchResourceRequest struct {
// Validate the request. // Validate the request.
func (r *SearchResourceRequest) Validate(rctx *RequestContext) error { func (r *SearchResourceRequest) Validate(rctx *RequestContext) error {
if r.Pattern == "" { if r.Pattern == "" {
return newValidationError(nil, "pattern", "empty pattern") return newValidationErrorStr("pattern", "empty search pattern")
} }
return nil return nil
} }
...@@ -73,15 +73,26 @@ func setResourceStatus(rctx *RequestContext, status string) error { ...@@ -73,15 +73,26 @@ func setResourceStatus(rctx *RequestContext, status string) error {
// SetResourceStatusRequest modifies the status of a resource // SetResourceStatusRequest modifies the status of a resource
// belonging to the user (admin-only). // belonging to the user (admin-only).
type SetResourceStatusRequest struct { type SetResourceStatusRequest struct {
AdminResourceRequestBase ResourceRequestBase
Status string `json:"status"` Status string `json:"status"`
} }
// Authorize self only when status == 'inactive'.
func (r *SetResourceStatusRequest) Authorize(rctx *RequestContext) error {
if err := r.ResourceRequestBase.Authorize(rctx); err != nil {
return err
}
if !rctx.Auth.IsAdmin && r.Status != ResourceStatusInactive {
return fmt.Errorf("setting status to %s is not allowed", r.Status)
}
return nil
}
// Validate the request. // Validate the request.
func (r *SetResourceStatusRequest) Validate(rctx *RequestContext) error { func (r *SetResourceStatusRequest) Validate(rctx *RequestContext) error {
if !isValidStatusByResourceType(rctx.Resource.Type, r.Status) { if !isValidStatusByResourceType(rctx.Resource.Type, r.Status) {
return newValidationError(nil, "status", "invalid or unknown status") return newValidationErrorStr("status", "invalid or unknown status")
} }
return nil return nil
} }
...@@ -115,13 +126,13 @@ func (r *AdminUpdateResourceRequest) Validate(rctx *RequestContext) error { ...@@ -115,13 +126,13 @@ func (r *AdminUpdateResourceRequest) Validate(rctx *RequestContext) error {
// can't be changed, etc). Prevent updates that would require // can't be changed, etc). Prevent updates that would require
// re-templating, since we don't really support derived fields. // re-templating, since we don't really support derived fields.
if r.Resource.ID != rctx.Resource.ID { if r.Resource.ID != rctx.Resource.ID {
return newValidationError(nil, "id", "can't update resource ID") return newValidationErrorStr("id", "can't update resource ID")
} }
if r.Resource.Type != rctx.Resource.Type { if r.Resource.Type != rctx.Resource.Type {
return newValidationError(nil, "type", "can't update resource type") return newValidationErrorStr("type", "can't update resource type")
} }
if r.Resource.ParentID != rctx.Resource.ParentID { if r.Resource.ParentID != rctx.Resource.ParentID {
return newValidationError(nil, "parent_id", "can't update resource parent ID") return newValidationErrorStr("parent_id", "can't update resource parent ID")
} }
// The logic here mirrors somewhat that in // The logic here mirrors somewhat that in
...@@ -136,7 +147,7 @@ func (r *AdminUpdateResourceRequest) Validate(rctx *RequestContext) error { ...@@ -136,7 +147,7 @@ func (r *AdminUpdateResourceRequest) Validate(rctx *RequestContext) error {
} }
// Validate the resource. // Validate the resource.
if err := rctx.resourceValidator.validateResource(rctx, r.Resource, tplUser, false); err != nil { if err := rctx.resourceValidator.validateResource(rctx, r.Resource, tplUser, false); err != nil && isCriticalErr(rctx, err) {
return err return err
} }
...@@ -144,9 +155,9 @@ func (r *AdminUpdateResourceRequest) Validate(rctx *RequestContext) error { ...@@ -144,9 +155,9 @@ func (r *AdminUpdateResourceRequest) Validate(rctx *RequestContext) error {
if tplUser != nil { if tplUser != nil {
// If the resource has an owner, validate it (checks that the new // If the resource has an owner, validate it (checks that the new
// resources do not violate user invariants). // resources do not violate user invariants).
if err := checkUserInvariants(tplUser); err != nil { if err := checkUserInvariants(tplUser); err != nil && isCriticalErr(rctx, err) {
log.Printf("validation error while updating resources: %v", err) log.Printf("validation error while updating resources: %v", err)
return newValidationError(nil, "global", err.Error()) return newValidationError("", err)
} }
} }
...@@ -187,7 +198,7 @@ func (r *CheckResourceAvailabilityRequest) PopulateContext(rctx *RequestContext) ...@@ -187,7 +198,7 @@ func (r *CheckResourceAvailabilityRequest) PopulateContext(rctx *RequestContext)
// Validate the request. // Validate the request.
func (r *CheckResourceAvailabilityRequest) Validate(rctx *RequestContext) error { func (r *CheckResourceAvailabilityRequest) Validate(rctx *RequestContext) error {
if r.Name == "" { if r.Name == "" {
return newValidationError(nil, "name", "name is unset") return newValidationErrorStr("name", "name is unset")
} }
return nil return nil
} }
...@@ -196,7 +207,7 @@ func (r *CheckResourceAvailabilityRequest) Validate(rctx *RequestContext) error ...@@ -196,7 +207,7 @@ func (r *CheckResourceAvailabilityRequest) Validate(rctx *RequestContext) error
func (r *CheckResourceAvailabilityRequest) Serve(rctx *RequestContext) (interface{}, error) { func (r *CheckResourceAvailabilityRequest) Serve(rctx *RequestContext) (interface{}, error) {
var check ValidatorFunc var check ValidatorFunc
switch r.Type { switch r.Type {
case ResourceTypeEmail, ResourceTypeMailingList: case ResourceTypeEmail, ResourceTypeMailingList, ResourceTypeNewsletter:
check = rctx.validationCtx.isAvailableEmailAddr() check = rctx.validationCtx.isAvailableEmailAddr()
case ResourceTypeDomain: case ResourceTypeDomain:
check = rctx.validationCtx.isAvailableDomain() check = rctx.validationCtx.isAvailableDomain()
...@@ -207,7 +218,7 @@ func (r *CheckResourceAvailabilityRequest) Serve(rctx *RequestContext) (interfac ...@@ -207,7 +218,7 @@ func (r *CheckResourceAvailabilityRequest) Serve(rctx *RequestContext) (interfac
case ResourceTypeDatabase: case ResourceTypeDatabase:
check = rctx.validationCtx.isAvailableDatabase() check = rctx.validationCtx.isAvailableDatabase()
default: default:
return nil, errors.New("unknown resource type") return nil, newValidationErrorStr("type", "unknown resource type")
} }
var resp CheckResourceAvailabilityResponse var resp CheckResourceAvailabilityResponse
...@@ -235,9 +246,9 @@ func (r *ResetResourcePasswordRequest) Validate(rctx *RequestContext) error { ...@@ -235,9 +246,9 @@ func (r *ResetResourcePasswordRequest) Validate(rctx *RequestContext) error {
switch rctx.Resource.Type { switch rctx.Resource.Type {
case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList: case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList:
case ResourceTypeEmail: case ResourceTypeEmail:
return newValidationError(nil, "type", "can't reset email passwords with this API") return newValidationErrorStr("type", "can't reset email passwords with this API")
default: default:
return newValidationError(nil, "type", "can't reset password on this resource type") return newValidationErrorStr("type", "can't reset password on this resource type")
} }
return nil return nil
} }
...@@ -317,16 +328,16 @@ type AddEmailAliasRequest struct { ...@@ -317,16 +328,16 @@ type AddEmailAliasRequest struct {
// Validate the request. // Validate the request.
func (r *AddEmailAliasRequest) Validate(rctx *RequestContext) error { func (r *AddEmailAliasRequest) Validate(rctx *RequestContext) error {
if rctx.Resource.Type != ResourceTypeEmail { if rctx.Resource.Type != ResourceTypeEmail {
return newValidationError(nil, "type", "this operation only works on email resources") return newValidationErrorStr("type", "this operation only works on email resources")
} }
// Allow at most 5 aliases. // Allow at most 5 aliases.
if len(rctx.Resource.Email.Aliases) >= maxEmailAliases { if len(rctx.Resource.Email.Aliases) >= maxEmailAliases {
return newValidationError(nil, "addr", "too many aliases") return newValidationErrorStr("addr", "too many aliases")
} }
if err := rctx.fieldValidators.newEmail(rctx, r.Addr); err != nil { if err := rctx.fieldValidators.newEmail(rctx, r.Addr); err != nil {
return newValidationError(nil, "addr", err.Error()) return newValidationError("addr", err)
} }
return nil return nil
} }
...@@ -353,7 +364,7 @@ type DeleteEmailAliasRequest struct { ...@@ -353,7 +364,7 @@ type DeleteEmailAliasRequest struct {
// Validate the request. // Validate the request.
func (r *DeleteEmailAliasRequest) Validate(rctx *RequestContext) error { func (r *DeleteEmailAliasRequest) Validate(rctx *RequestContext) error {
if rctx.Resource.Type != ResourceTypeEmail { if rctx.Resource.Type != ResourceTypeEmail {
return newValidationError(nil, "type", "this operation only works on email resources") return newValidationErrorStr("type", "this operation only works on email resources")
} }
return nil return nil
} }
...@@ -374,3 +385,47 @@ func (r *DeleteEmailAliasRequest) Serve(rctx *RequestContext) (interface{}, erro ...@@ -374,3 +385,47 @@ func (r *DeleteEmailAliasRequest) Serve(rctx *RequestContext) (interface{}, erro
rctx.audit.Log(rctx, rctx.Resource, fmt.Sprintf("removed alias %s", r.Addr)) rctx.audit.Log(rctx, rctx.Resource, fmt.Sprintf("removed alias %s", r.Addr))
return nil, nil return nil, nil
} }
// WebSetPHPVersion sets the PHP version for a website.
type WebSetPHPVersionRequest struct {
ResourceRequestBase
PHPVersion string `json:"php_version"`
}
// Validate the request.
func (r *WebSetPHPVersionRequest) Validate(rctx *RequestContext) error {
if rctx.Resource.Type != ResourceTypeWebsite && rctx.Resource.Type != ResourceTypeDomain {
return newValidationErrorStr("type", "this operation only works on web resources")
}
if r.PHPVersion == "" {
return newValidationErrorStr("php_version", "version can't be empty")
}
// TODO: stricter validation? Right now we allow setting any
// version, but this check is highly spescific.
if !strings.HasPrefix(r.PHPVersion, "php") {
return newValidationErrorStr("php_version", "invalid PHP version")
}
return nil
}
// Serve the request.
func (r *WebSetPHPVersionRequest) Serve(rctx *RequestContext) (interface{}, error) {
var opts []string
for _, opt := range rctx.Resource.Website.Options {
if !strings.HasPrefix(opt, "php") {
opts = append(opts, opt)
}
}
opts = append(opts, r.PHPVersion)
rctx.Resource.Website.Options = opts
if err := rctx.TX.UpdateResource(rctx.Context, rctx.Resource); err != nil {
return nil, err
}
rctx.audit.Log(rctx, rctx.Resource, fmt.Sprintf("set PHP version to %s", r.PHPVersion))
return nil, nil
}
package accountserver
import (
"bytes"
"crypto/sha1"
"errors"
"fmt"
"strings"
"time"
"github.com/ProtonMail/gopenpgp/v3/crypto"
"github.com/tv42/zbase32"
)
// SetOpenPGPKeyRequest allows users to set their own OpenPGP keys.
type SetOpenPGPKeyRequest struct {
ResourceRequestBase
// Set to empty value to delete key.
OpenPGPKey []byte `json:"openpgp_key"`
key *crypto.Key
matchedIdentities []string
}
var pgpArmor = []byte("-----BEGIN PGP PUBLIC KEY BLOCK-----")
func splitAddress(addr string) (local, domain string, err error) {
parts := strings.Split(addr, "@")
if len(parts) != 2 {
return "", "", errors.New("wkd: invalid email address")
}
return parts[0], parts[1], nil
}
// hashLocal returns the WKD hash for the local part of a given email address.
func hashLocal(local string) string {
local = strings.ToLower(local)
hashedLocal := sha1.Sum([]byte(local))
return zbase32.EncodeToString(hashedLocal[:])
}
// wkdHashAddress combines the WKD hash for the local part of a given
// email address, with the domain, to generate a site-wide unique
// identifier for quick WKD lookup.
func wkdHashAddress(addr string) (string, error) {
local, domain, err := splitAddress(addr)
if err != nil {
return "", err
}
return fmt.Sprintf("%s@%s", hashLocal(local), domain), nil
}
// Bag of strings, unordered.
type addrSet map[string]struct{}
func newAddrSet(l []string) addrSet {
s := make(addrSet)
for _, elem := range l {
s.add(elem)
}
return s
}
func (s addrSet) add(elem string) {
s[elem] = struct{}{}
}
func (s addrSet) toList() []string {
out := make([]string, 0, len(s))
for elem := range s {
out = append(out, elem)
}
return out
}
func (s addrSet) intersect(b addrSet) addrSet {
out := make(addrSet)
for elem := range s {
if _, ok := b[elem]; ok {
out.add(elem)
}
}
return out
}
// Find unique identities (email addresses) associated with the key
// that match email addresses (including aliases) of the Resource.
func findMatchingKeyIdentities(rsrc *Resource, key *crypto.Key) []string {
keyIdentities := newAddrSet(nil)
for _, identity := range key.GetEntity().Identities {
// Some keys can have an empty Email field, but will
// contain an email address in the Id field.
idEmail := identity.UserId.Email
if idEmail == "" && strings.Contains(identity.UserId.Id, "@") {
idEmail = identity.UserId.Id
}
keyIdentities.add(idEmail)
}
emails := newAddrSet(rsrc.Email.Aliases)
emails.add(rsrc.Name)
return emails.intersect(keyIdentities).toList()
}
func parseOpenPGPKey(data []byte) (key *crypto.Key, err error) {
// Accept either armored or raw inputs.
if bytes.HasPrefix(data, pgpArmor) {
key, err = crypto.NewKeyFromArmored(string(data))
} else {
key, err = crypto.NewKey(data)
}
if err != nil {
return
}
// Detect uploads of private keys!
if key.IsPrivate() {
err = errors.New("input is a private key")
}
return
}
func getPGPKeyExpiry(key *crypto.Key) int64 {
ent := key.GetEntity()
if sig, _ := ent.PrimaryIdentity(time.Now(), nil); sig != nil && sig.KeyLifetimeSecs != nil {
return ent.PrimaryKey.CreationTime.Add(
time.Duration(*sig.KeyLifetimeSecs) * time.Second).Unix()
}
// Key does not expire.
return 0
}
func newOpenPGPKey(identities []string, key *crypto.Key) (*OpenPGPKey, error) {
data, err := key.GetPublicKey()
if err != nil {
return nil, err
}
var hashes []string
for _, identity := range identities {
wkdHash, err := wkdHashAddress(identity)
if err != nil {
return nil, err
}
hashes = append(hashes, wkdHash)
}
return &OpenPGPKey{
Key: data,
ID: key.GetHexKeyID(),
Hashes: hashes,
Expiry: getPGPKeyExpiry(key),
}, nil
}
func (r *SetOpenPGPKeyRequest) Validate(rctx *RequestContext) error {
if rctx.Resource.Type != ResourceTypeEmail {
return newValidationErrorStr("type", "this operation only works on email resources")
}
// If a key is present, validate it.
if len(r.OpenPGPKey) > 0 {
key, err := parseOpenPGPKey(r.OpenPGPKey)
if err != nil {
return newValidationError("openpgp_key", err)
}
// Find all the key identities that match the resource
// email address or one of its aliases. We will later
// define wkd hash lookup keys for each one of them.
matchedIdentities := findMatchingKeyIdentities(rctx.Resource, key)
if len(matchedIdentities) == 0 {
return newValidationError("openpgp_key", fmt.Errorf(
"no matching key identity for user %s",
rctx.Resource.Name))
}
r.key = key
r.matchedIdentities = matchedIdentities
}
return nil
}
func (r *SetOpenPGPKeyRequest) Serve(rctx *RequestContext) (interface{}, error) {
var auditMsg string
rsrc := rctx.Resource
if r.key == nil {
rsrc.Email.OpenPGPKey = nil
auditMsg = "deleted GPG key"
} else {
pgpKey, err := newOpenPGPKey(r.matchedIdentities, r.key)
if err != nil {
return nil, err
}
rsrc.Email.OpenPGPKey = pgpKey
auditMsg = fmt.Sprintf("updated GPG key %s", pgpKey.ID)
}
if err := rctx.TX.UpdateResource(rctx.Context, rsrc); err != nil {
return nil, err
}
rctx.audit.Log(rctx, rctx.Resource, auditMsg)
return nil, nil
}
...@@ -22,7 +22,7 @@ type fakeBackend struct { ...@@ -22,7 +22,7 @@ type fakeBackend struct {
passwords map[string]string passwords map[string]string
recoveryPasswords map[string]string recoveryPasswords map[string]string
resourcePasswords map[string]string resourcePasswords map[string]string
appSpecificPasswords map[string][]*AppSpecificPasswordInfo appSpecificPasswords map[string][]*ct.AppSpecificPassword
encryptionKeys map[string][]*ct.EncryptedKey encryptionKeys map[string][]*ct.EncryptedKey
} }
...@@ -175,7 +175,7 @@ func (b *fakeBackend) SetUserEncryptionPublicKey(_ context.Context, user *User, ...@@ -175,7 +175,7 @@ func (b *fakeBackend) SetUserEncryptionPublicKey(_ context.Context, user *User,
return nil return nil
} }
func (b *fakeBackend) SetApplicationSpecificPassword(_ context.Context, user *User, info *AppSpecificPasswordInfo, _ string) error { func (b *fakeBackend) SetApplicationSpecificPassword(_ context.Context, user *User, info *ct.AppSpecificPassword, _ string) error {
b.appSpecificPasswords[user.Name] = append(b.appSpecificPasswords[user.Name], info) b.appSpecificPasswords[user.Name] = append(b.appSpecificPasswords[user.Name], info)
return nil return nil
} }
...@@ -268,7 +268,7 @@ func createFakeBackend() *fakeBackend { ...@@ -268,7 +268,7 @@ func createFakeBackend() *fakeBackend {
passwords: make(map[string]string), passwords: make(map[string]string),
recoveryPasswords: make(map[string]string), recoveryPasswords: make(map[string]string),
resourcePasswords: make(map[string]string), resourcePasswords: make(map[string]string),
appSpecificPasswords: make(map[string][]*AppSpecificPasswordInfo), appSpecificPasswords: make(map[string][]*ct.AppSpecificPassword),
encryptionKeys: make(map[string][]*ct.EncryptedKey), encryptionKeys: make(map[string][]*ct.EncryptedKey),
} }
fb.addUser(&User{ fb.addUser(&User{
......
...@@ -2,7 +2,9 @@ package accountserver ...@@ -2,7 +2,9 @@ package accountserver
import ( import (
"errors" "errors"
"strings"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
umdb "git.autistici.org/id/usermetadb" umdb "git.autistici.org/id/usermetadb"
) )
...@@ -37,7 +39,7 @@ type SearchUserRequest struct { ...@@ -37,7 +39,7 @@ type SearchUserRequest struct {
// Validate the request. // Validate the request.
func (r *SearchUserRequest) Validate(rctx *RequestContext) error { func (r *SearchUserRequest) Validate(rctx *RequestContext) error {
if r.Pattern == "" { if r.Pattern == "" {
return newValidationError(nil, "pattern", "empty pattern") return newValidationErrorStr("pattern", "empty search pattern")
} }
return nil return nil
} }
...@@ -74,10 +76,10 @@ func (r *ChangeUserPasswordRequest) Sanitize() { ...@@ -74,10 +76,10 @@ func (r *ChangeUserPasswordRequest) Sanitize() {
// Validate the request. // Validate the request.
func (r *ChangeUserPasswordRequest) Validate(rctx *RequestContext) error { func (r *ChangeUserPasswordRequest) Validate(rctx *RequestContext) error {
if err := rctx.fieldValidators.password(rctx, r.Password); err != nil { if err := rctx.fieldValidators.password(rctx, r.Password); err != nil {
return newValidationError(nil, "password", err.Error()) return newValidationError("password", err)
} }
if r.Password == r.CurPassword { if r.Password == r.CurPassword {
return newValidationError(nil, "password", "The new password can't be the same as the old one") return newValidationErrorStr("password", "The new password can't be the same as the old one")
} }
return r.PrivilegedRequestBase.Validate(rctx) return r.PrivilegedRequestBase.Validate(rctx)
} }
...@@ -127,7 +129,7 @@ func (r *AccountRecoveryRequest) Validate(rctx *RequestContext) error { ...@@ -127,7 +129,7 @@ func (r *AccountRecoveryRequest) Validate(rctx *RequestContext) error {
// Only validate the password if attempting recovery. // Only validate the password if attempting recovery.
if r.RecoveryPassword != "" { if r.RecoveryPassword != "" {
if err := rctx.fieldValidators.password(rctx, r.Password); err != nil { if err := rctx.fieldValidators.password(rctx, r.Password); err != nil {
return newValidationError(nil, "password", err.Error()) return newValidationError("password", err)
} }
} }
return nil return nil
...@@ -156,7 +158,6 @@ func (r *AccountRecoveryRequest) Authorize(rctx *RequestContext) error { ...@@ -156,7 +158,6 @@ func (r *AccountRecoveryRequest) Authorize(rctx *RequestContext) error {
return nil return nil
} }
// TODO: call out to auth-server for rate limiting and other features.
// Authenticate the secret recovery password. // Authenticate the secret recovery password.
if err := rctx.authorizeAccountRecovery(rctx.Context, rctx.User.Name, r.RecoveryPassword, r.RemoteAddr); err != nil { if err := rctx.authorizeAccountRecovery(rctx.Context, rctx.User.Name, r.RecoveryPassword, r.RemoteAddr); err != nil {
return err return err
...@@ -245,14 +246,14 @@ func (r *SetAccountRecoveryHintRequest) Sanitize() { ...@@ -245,14 +246,14 @@ func (r *SetAccountRecoveryHintRequest) Sanitize() {
// Validate the request. // Validate the request.
func (r *SetAccountRecoveryHintRequest) Validate(rctx *RequestContext) error { func (r *SetAccountRecoveryHintRequest) Validate(rctx *RequestContext) error {
var err *ValidationError var err error
if r.Hint == "" { if r.Hint == "" {
err = newValidationError(err, "recovery_hint", "mandatory field") err = errors.Join(err, newValidationErrorStr("recovery_hint", "mandatory field"))
} }
if verr := rctx.fieldValidators.password(rctx, r.Response); verr != nil { if verr := rctx.fieldValidators.password(rctx, r.Response); verr != nil {
err = newValidationError(err, "recovery_response", verr.Error()) err = errors.Join(err, newValidationError("recovery_response", verr))
} }
return err.orNil() return err
} }
// Serve the request. // Serve the request.
...@@ -282,11 +283,10 @@ func (r *CreateApplicationSpecificPasswordResponse) Sanitize() { ...@@ -282,11 +283,10 @@ func (r *CreateApplicationSpecificPasswordResponse) Sanitize() {
// Validate the request. // Validate the request.
func (r *CreateApplicationSpecificPasswordRequest) Validate(_ *RequestContext) error { func (r *CreateApplicationSpecificPasswordRequest) Validate(_ *RequestContext) error {
var err *ValidationError
if r.Service == "" { if r.Service == "" {
err = newValidationError(err, "service", "mandatory field") return newValidationErrorStr("service", "mandatory field")
} }
return err.orNil() return nil
} }
// Serve the request. // Serve the request.
...@@ -296,7 +296,7 @@ func (r *CreateApplicationSpecificPasswordRequest) Serve(rctx *RequestContext) ( ...@@ -296,7 +296,7 @@ func (r *CreateApplicationSpecificPasswordRequest) Serve(rctx *RequestContext) (
} }
// Create a new application-specific password metadata. // Create a new application-specific password metadata.
asp := &AppSpecificPasswordInfo{ asp := &ct.AppSpecificPassword{
ID: randomAppSpecificPasswordID(), ID: randomAppSpecificPasswordID(),
Service: r.Service, Service: r.Service,
Comment: r.Notes, Comment: r.Notes,
...@@ -343,12 +343,13 @@ func (r *EnableOTPRequest) Sanitize() { ...@@ -343,12 +343,13 @@ func (r *EnableOTPRequest) Sanitize() {
// Validate the request. // Validate the request.
func (r *EnableOTPRequest) Validate(_ *RequestContext) error { func (r *EnableOTPRequest) Validate(_ *RequestContext) error {
var err *ValidationError // Only check if the client-side secret is set, skip otherwise. We
// Only check if the client-side secret is set, skip otherwise. // don't really expect a bad value coming from the generator, so the
if r.TOTPSecret == "" && len(r.TOTPSecret) != 16 { // length check is just for internal consistency.
err = newValidationError(err, "totp_secret", "bad value") if r.TOTPSecret != "" && len(r.TOTPSecret) < 10 {
return newValidationErrorStr("totp_secret", "bad value")
} }
return err.orNil() return nil
} }
// EnableOTPResponse is the response type for AccountService.EnableOTP(). // EnableOTPResponse is the response type for AccountService.EnableOTP().
...@@ -386,7 +387,7 @@ func (r *EnableOTPRequest) Serve(rctx *RequestContext) (interface{}, error) { ...@@ -386,7 +387,7 @@ func (r *EnableOTPRequest) Serve(rctx *RequestContext) (interface{}, error) {
}, nil }, nil
} }
// DisableOTPRequest disables two-factor authentication for a user. // DisableOTPRequest disables TOTP second-factor authentication for a user.
type DisableOTPRequest struct { type DisableOTPRequest struct {
UserRequestBase UserRequestBase
} }
...@@ -400,26 +401,45 @@ func (r *DisableOTPRequest) Serve(rctx *RequestContext) (interface{}, error) { ...@@ -400,26 +401,45 @@ func (r *DisableOTPRequest) Serve(rctx *RequestContext) (interface{}, error) {
return nil, nil return nil, nil
} }
// Disable2FARequest disables all second-factor authentication for a user.
type Disable2FARequest struct {
UserRequestBase
}
// Serve the request.
func (r *Disable2FARequest) Serve(rctx *RequestContext) (interface{}, error) {
if err := rctx.User.disable2FA(rctx.Context, rctx.TX); err != nil {
return nil, err
}
rctx.audit.Log(rctx, nil, "2FA disabled")
return nil, nil
}
// UpdateUserRequest allows the caller to update a (very limited) selected set // UpdateUserRequest allows the caller to update a (very limited) selected set
// of fields on a User object. It is a catch-all function for very simple // of fields on a User object. It is a catch-all function for very simple
// changes that don't justify their own specialized method. // changes that don't justify their own specialized method. Fields are
// associated with a "set_field" attribute to allow for selective updates.
type UpdateUserRequest struct { type UpdateUserRequest struct {
UserRequestBase UserRequestBase
Lang string `json:"lang,omitempty"` Lang string `json:"lang,omitempty"`
U2FRegistrations []*U2FRegistration `json:"u2f_registrations,omitempty"` SetLang bool `json:"set_lang"`
U2FRegistrations []*ct.U2FRegistration `json:"u2f_registrations,omitempty"`
SetU2FRegistrations bool `json:"set_u2f_registrations"`
} }
const maxU2FRegistrations = 20 const maxU2FRegistrations = 20
// Validate the request. // Validate the request.
func (r *UpdateUserRequest) Validate(rctx *RequestContext) error { func (r *UpdateUserRequest) Validate(rctx *RequestContext) error {
if len(r.U2FRegistrations) > maxU2FRegistrations { if r.SetU2FRegistrations && len(r.U2FRegistrations) > maxU2FRegistrations {
return newValidationError(nil, "u2f_registrations", "too many U2F registrations") return newValidationErrorStr("u2f_registrations", "too many U2F registrations")
} }
// TODO: better validation of the language code! // TODO: better validation of the language code!
if len(r.Lang) > 2 { if r.SetLang && (r.Lang == "" || len(r.Lang) > 2) {
return newValidationError(nil, "lang", "invalid language code") return newValidationErrorStr("lang", "invalid language code")
} }
return nil return nil
...@@ -427,16 +447,22 @@ func (r *UpdateUserRequest) Validate(rctx *RequestContext) error { ...@@ -427,16 +447,22 @@ func (r *UpdateUserRequest) Validate(rctx *RequestContext) error {
// Serve the request. // Serve the request.
func (r *UpdateUserRequest) Serve(rctx *RequestContext) (interface{}, error) { func (r *UpdateUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
if r.Lang != "" { if r.SetLang {
rctx.User.Lang = r.Lang rctx.User.Lang = strings.ToLower(r.Lang)
} }
// TODO: check if setU2FRegistration calls tx.UpdateUser, this is a bug otherwise. if r.SetU2FRegistrations {
return nil, rctx.User.setU2FRegistrations(rctx.Context, rctx.TX, r.U2FRegistrations) rctx.User.U2FRegistrations = r.U2FRegistrations
if err := rctx.User.check2FAState(rctx.Context, rctx.TX); err != nil {
return nil, err
}
}
return nil, rctx.TX.UpdateUser(rctx.Context, &rctx.User.User)
} }
// AdminUpdateUserRequest is the privileged version of UpdateUser and // AdminUpdateUserRequest is the privileged version of UpdateUser and
// allows to update many more attributes. It is a catch-all function // allows to update privileged attributes. It is a catch-all function
// for very simple changes that don't justify their own specialized // for very simple changes that don't justify their own specialized
// method. // method.
type AdminUpdateUserRequest struct { type AdminUpdateUserRequest struct {
...@@ -450,7 +476,7 @@ func (r *AdminUpdateUserRequest) Validate(rctx *RequestContext) error { ...@@ -450,7 +476,7 @@ func (r *AdminUpdateUserRequest) Validate(rctx *RequestContext) error {
switch r.Status { switch r.Status {
case "", ResourceStatusActive, ResourceStatusInactive: case "", ResourceStatusActive, ResourceStatusInactive:
default: default:
return newValidationError(nil, "status", "invalid or unknown status") return newValidationErrorStr("status", "invalid or unknown status")
} }
return nil return nil
} }
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"log" "log"
"strings" "strings"
"sync" "sync"
"time"
as "git.autistici.org/ai3/accountserver" as "git.autistici.org/ai3/accountserver"
"git.autistici.org/ai3/go-common/clientutil" "git.autistici.org/ai3/go-common/clientutil"
...@@ -18,6 +19,10 @@ const auxDbWebappDataType = "cms_info" ...@@ -18,6 +19,10 @@ const auxDbWebappDataType = "cms_info"
// Data type of disk usage resources in aux-db. // Data type of disk usage resources in aux-db.
const auxDbDiskUsageDataType = "disk_usage" const auxDbDiskUsageDataType = "disk_usage"
// Timeout to use for auxdb RPCs. It's fast so we can time out early
// if the backends are unreachable.
var auxdbTimeout = 3 * time.Second
// AuxWebappBackend looks up website information (cms_info data type) in // AuxWebappBackend looks up website information (cms_info data type) in
// the aux-db service. // the aux-db service.
// //
...@@ -148,7 +153,10 @@ func lookupsForResources(resources []*as.Resource) (lookups []*lookupEntry) { ...@@ -148,7 +153,10 @@ func lookupsForResources(resources []*as.Resource) (lookups []*lookupEntry) {
return return
} }
func (tx *wdbTX) lookup(ctx context.Context, lookups []*lookupEntry) { func (tx *wdbTX) lookup(outerCtx context.Context, lookups []*lookupEntry) {
ctx, cancel := context.WithTimeout(outerCtx, auxdbTimeout)
defer cancel()
// Group the keys by shard, and build a reverse index of keys // Group the keys by shard, and build a reverse index of keys
// -> callbacks, that we'll use to process the results. // -> callbacks, that we'll use to process the results.
keysByShard := make(map[string][]auxpb.Key) keysByShard := make(map[string][]auxpb.Key)
......
...@@ -166,7 +166,7 @@ func (c *cacheTX) SetUserEncryptionPublicKey(ctx context.Context, user *as.User, ...@@ -166,7 +166,7 @@ func (c *cacheTX) SetUserEncryptionPublicKey(ctx context.Context, user *as.User,
return err return err
} }
func (c *cacheTX) SetApplicationSpecificPassword(ctx context.Context, user *as.User, asp *as.AppSpecificPasswordInfo, pw string) error { func (c *cacheTX) SetApplicationSpecificPassword(ctx context.Context, user *as.User, asp *ct.AppSpecificPassword, pw string) error {
err := c.TX.SetApplicationSpecificPassword(ctx, user, asp, pw) err := c.TX.SetApplicationSpecificPassword(ctx, user, asp, pw)
if err == nil { if err == nil {
c.invalidateUser(user.Name) c.invalidateUser(user.Name)
......
...@@ -110,7 +110,7 @@ func (tx instrumentedTX) SetUserEncryptionPublicKey(ctx context.Context, u *as.U ...@@ -110,7 +110,7 @@ func (tx instrumentedTX) SetUserEncryptionPublicKey(ctx context.Context, u *as.U
return tx.TX.SetUserEncryptionPublicKey(ctx, u, k) return tx.TX.SetUserEncryptionPublicKey(ctx, u, k)
} }
func (tx instrumentedTX) SetApplicationSpecificPassword(ctx context.Context, u *as.User, a *as.AppSpecificPasswordInfo, s string) error { func (tx instrumentedTX) SetApplicationSpecificPassword(ctx context.Context, u *as.User, a *ct.AppSpecificPassword, s string) error {
counters.WithLabelValues(tx.name, "SetApplicationSpecificPassword").Inc() counters.WithLabelValues(tx.name, "SetApplicationSpecificPassword").Inc()
return tx.TX.SetApplicationSpecificPassword(ctx, u, a, s) return tx.TX.SetApplicationSpecificPassword(ctx, u, a, s)
} }
......
package ldapbackend package ldapbackend
import ( import (
"encoding/base64"
as "git.autistici.org/ai3/accountserver"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes" ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
) )
...@@ -13,15 +10,6 @@ import ( ...@@ -13,15 +10,6 @@ import (
// is an overall better strategy than crashing, but it's a bit gentler // is an overall better strategy than crashing, but it's a bit gentler
// on the user. // on the user.
func newAppSpecificPassword(info as.AppSpecificPasswordInfo, pw string) *ct.AppSpecificPassword {
return &ct.AppSpecificPassword{
ID: info.ID,
Service: info.Service,
Comment: info.Comment,
EncryptedPassword: pw,
}
}
func decodeAppSpecificPasswords(values []string) []*ct.AppSpecificPassword { func decodeAppSpecificPasswords(values []string) []*ct.AppSpecificPassword {
var out []*ct.AppSpecificPassword var out []*ct.AppSpecificPassword
for _, value := range values { for _, value := range values {
...@@ -50,16 +38,11 @@ func encodeAppSpecificPasswords(asps []*ct.AppSpecificPassword) []string { ...@@ -50,16 +38,11 @@ func encodeAppSpecificPasswords(asps []*ct.AppSpecificPassword) []string {
return out return out
} }
func getASPInfo(asps []*ct.AppSpecificPassword) []*as.AppSpecificPasswordInfo { func sanitizeAppSpecificPasswords(asps []*ct.AppSpecificPassword) []*ct.AppSpecificPassword {
var out []*as.AppSpecificPasswordInfo for i := 0; i < len(asps); i++ {
for _, asp := range asps { asps[i].EncryptedPassword = ""
out = append(out, &as.AppSpecificPasswordInfo{
ID: asp.ID,
Service: asp.Service,
Comment: asp.Comment,
})
} }
return out return asps
} }
func decodeUserEncryptionKeys(values []string) []*ct.EncryptedKey { func decodeUserEncryptionKeys(values []string) []*ct.EncryptedKey {
...@@ -82,41 +65,20 @@ func encodeUserEncryptionKeys(keys []*ct.EncryptedKey) []string { ...@@ -82,41 +65,20 @@ func encodeUserEncryptionKeys(keys []*ct.EncryptedKey) []string {
return out return out
} }
func decodeU2FRegistrations(encRegs []string) []*as.U2FRegistration { func decodeU2FRegistrations(encRegs []string) []*ct.U2FRegistration {
var out []*as.U2FRegistration var out []*ct.U2FRegistration
for _, enc := range encRegs { for _, enc := range encRegs {
if r, err := ct.UnmarshalU2FRegistration(enc); err == nil { if r, err := ct.UnmarshalU2FRegistration(enc); err == nil {
// Convert ct.U2FRegistration (internal) -> out = append(out, r)
// as.U2FRegistration (public) by
// base64-encoding the data.
out = append(out, &as.U2FRegistration{
KeyHandle: base64.StdEncoding.EncodeToString(r.KeyHandle),
PublicKey: base64.StdEncoding.EncodeToString(r.PublicKey),
})
} }
} }
return out return out
} }
func encodeU2FRegistrations(regs []*as.U2FRegistration) []string { func encodeU2FRegistrations(regs []*ct.U2FRegistration) []string {
var out []string var out []string
for _, r := range regs { for _, r := range regs {
// Convert as.U2FRegistration (public) -> out = append(out, r.Marshal())
// ct.U2FRegistration (internal) by base64-decoding
// the data.
kh, err := base64.StdEncoding.DecodeString(r.KeyHandle)
if err != nil {
continue
}
pk, err := base64.StdEncoding.DecodeString(r.PublicKey)
if err != nil {
continue
}
ctr := ct.U2FRegistration{
KeyHandle: kh,
PublicKey: pk,
}
out = append(out, ctr.Marshal())
} }
return out return out
} }
...@@ -144,7 +144,7 @@ func newUser(entry *ldap.Entry) (*as.RawUser, error) { ...@@ -144,7 +144,7 @@ func newUser(entry *ldap.Entry) (*as.RawUser, error) {
LastPasswordChangeStamp: decodeShadowTimestamp(entry.GetAttributeValue(passwordLastChangeLDAPAttr)), LastPasswordChangeStamp: decodeShadowTimestamp(entry.GetAttributeValue(passwordLastChangeLDAPAttr)),
AccountRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr), AccountRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr),
U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)), U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)),
AppSpecificPasswords: getASPInfo(decodeAppSpecificPasswords(entry.GetAttributeValues(aspLDAPAttr))), AppSpecificPasswords: sanitizeAppSpecificPasswords(decodeAppSpecificPasswords(entry.GetAttributeValues(aspLDAPAttr))),
HasOTP: entry.GetAttributeValue(totpSecretLDAPAttr) != "", HasOTP: entry.GetAttributeValue(totpSecretLDAPAttr) != "",
}, },
// Remove the legacy LDAP {crypt} prefix on old passwords. // Remove the legacy LDAP {crypt} prefix on old passwords.
...@@ -425,10 +425,20 @@ func (tx *backendTX) SetUserEncryptionPublicKey(ctx context.Context, user *as.Us ...@@ -425,10 +425,20 @@ func (tx *backendTX) SetUserEncryptionPublicKey(ctx context.Context, user *as.Us
return nil return nil
} }
func (tx *backendTX) SetApplicationSpecificPassword(ctx context.Context, user *as.User, info *as.AppSpecificPasswordInfo, encryptedPassword string) error { func (tx *backendTX) SetApplicationSpecificPassword(ctx context.Context, user *as.User, asp *ct.AppSpecificPassword, encryptedPassword string) error {
dn := tx.getUserDN(user) dn := tx.getUserDN(user)
asps := decodeAppSpecificPasswords(tx.readAttributeValues(ctx, dn, aspLDAPAttr)) asps := decodeAppSpecificPasswords(tx.readAttributeValues(ctx, dn, aspLDAPAttr))
asps = append(excludeASPFromList(asps, info.ID), newAppSpecificPassword(*info, encryptedPassword))
// Build a new AppSpecificPassword that includes the encrypted
// pw, without modifying the original object.
newASP := ct.AppSpecificPassword{
ID: asp.ID,
Service: asp.Service,
Comment: asp.Comment,
EncryptedPassword: encryptedPassword,
}
asps = append(excludeASPFromList(asps, asp.ID), &newASP)
outASPs := encodeAppSpecificPasswords(asps) outASPs := encodeAppSpecificPasswords(asps)
tx.setAttr(dn, aspLDAPAttr, outASPs...) tx.setAttr(dn, aspLDAPAttr, outASPs...)
return nil return nil
......
package ldapbackend package ldapbackend
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"testing" "testing"
...@@ -14,8 +15,6 @@ import ( ...@@ -14,8 +15,6 @@ import (
) )
const ( const (
testLDAPPort = 42871
testLDAPAddr = "ldap://127.0.0.1:42871"
testUser1 = "uno@investici.org" testUser1 = "uno@investici.org"
testUser2 = "due@investici.org" // has encryption keys testUser2 = "due@investici.org" // has encryption keys
testUser3 = "tre@investici.org" // has OTP testUser3 = "tre@investici.org" // has OTP
...@@ -23,26 +22,25 @@ const ( ...@@ -23,26 +22,25 @@ const (
testBaseDN = "dc=example,dc=com" testBaseDN = "dc=example,dc=com"
) )
func startServerAndGetUser(t testing.TB) (func(), as.Backend, *as.RawUser) { func startServerAndGetUser(t *testing.T) (*ldaptest.TestLDAPServer, as.Backend, *as.RawUser) {
return startServerAndGetUserWithName(t, testUser1) return startServerAndGetUserWithName(t, testUser1)
} }
func startServerAndGetUser2(t testing.TB) (func(), as.Backend, *as.RawUser) { func startServerAndGetUser2(t *testing.T) (*ldaptest.TestLDAPServer, as.Backend, *as.RawUser) {
return startServerAndGetUserWithName(t, testUser2) return startServerAndGetUserWithName(t, testUser2)
} }
func startServerAndGetUser3(t testing.TB) (func(), as.Backend, *as.RawUser) { func startServerAndGetUser3(t *testing.T) (*ldaptest.TestLDAPServer, as.Backend, *as.RawUser) {
return startServerAndGetUserWithName(t, testUser3) return startServerAndGetUserWithName(t, testUser3)
} }
func startServerAndGetUser4(t testing.TB) (func(), as.Backend, *as.RawUser) { func startServerAndGetUser4(t *testing.T) (*ldaptest.TestLDAPServer, as.Backend, *as.RawUser) {
return startServerAndGetUserWithName(t, testUser4) return startServerAndGetUserWithName(t, testUser4)
} }
func startServer(t testing.TB) (func(), as.Backend) { func startServer(t *testing.T) (*ldaptest.TestLDAPServer, as.Backend) {
stop := ldaptest.StartServer(t, &ldaptest.Config{ srv := ldaptest.StartServer(t, &ldaptest.Config{
Dir: "../../ldaptest", Dir: "../../ldaptest",
Port: testLDAPPort,
Base: "dc=example,dc=com", Base: "dc=example,dc=com",
LDIFs: []string{ LDIFs: []string{
"testdata/base.ldif", "testdata/base.ldif",
...@@ -53,16 +51,16 @@ func startServer(t testing.TB) (func(), as.Backend) { ...@@ -53,16 +51,16 @@ func startServer(t testing.TB) (func(), as.Backend) {
}, },
}) })
b, err := NewLDAPBackend(testLDAPAddr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com") b, err := NewLDAPBackend(srv.Addr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
if err != nil { if err != nil {
t.Fatal("NewLDAPBackend", err) t.Fatal("NewLDAPBackend", err)
} }
return stop, b return srv, b
} }
func startServerAndGetUserWithName(t testing.TB, username string) (func(), as.Backend, *as.RawUser) { func startServerAndGetUserWithName(t *testing.T, username string) (*ldaptest.TestLDAPServer, as.Backend, *as.RawUser) {
stop, b := startServer(t) srv, b := startServer(t)
tx, _ := b.NewTransaction() tx, _ := b.NewTransaction()
user, err := tx.GetUser(context.Background(), username) user, err := tx.GetUser(context.Background(), username)
...@@ -73,12 +71,12 @@ func startServerAndGetUserWithName(t testing.TB, username string) (func(), as.Ba ...@@ -73,12 +71,12 @@ func startServerAndGetUserWithName(t testing.TB, username string) (func(), as.Ba
t.Fatalf("could not find test user %s", username) t.Fatalf("could not find test user %s", username)
} }
return stop, b, user return srv, b, user
} }
func TestModel_GetUser_NotFound(t *testing.T) { func TestModel_GetUser_NotFound(t *testing.T) {
stop, b := startServer(t) srv, b := startServer(t)
defer stop() defer srv.Close()
tx, _ := b.NewTransaction() tx, _ := b.NewTransaction()
user, err := tx.GetUser(context.Background(), "wrong_user") user, err := tx.GetUser(context.Background(), "wrong_user")
...@@ -91,8 +89,8 @@ func TestModel_GetUser_NotFound(t *testing.T) { ...@@ -91,8 +89,8 @@ func TestModel_GetUser_NotFound(t *testing.T) {
} }
func TestModel_GetUser(t *testing.T) { func TestModel_GetUser(t *testing.T) {
stop, _, user := startServerAndGetUser(t) srv, _, user := startServerAndGetUser(t)
defer stop() defer srv.Close()
if user.Name != testUser1 { if user.Name != testUser1 {
t.Errorf("bad username: expected %s, got %s", testUser1, user.Name) t.Errorf("bad username: expected %s, got %s", testUser1, user.Name)
...@@ -126,8 +124,8 @@ func TestModel_GetUser(t *testing.T) { ...@@ -126,8 +124,8 @@ func TestModel_GetUser(t *testing.T) {
} }
func TestModel_GetUser_HasEncryptionKeys(t *testing.T) { func TestModel_GetUser_HasEncryptionKeys(t *testing.T) {
stop, _, user := startServerAndGetUser2(t) srv, _, user := startServerAndGetUser2(t)
defer stop() defer srv.Close()
if !user.HasEncryptionKeys { if !user.HasEncryptionKeys {
t.Errorf("user %s does not appear to have encryption keys", user.Name) t.Errorf("user %s does not appear to have encryption keys", user.Name)
...@@ -135,17 +133,35 @@ func TestModel_GetUser_HasEncryptionKeys(t *testing.T) { ...@@ -135,17 +133,35 @@ func TestModel_GetUser_HasEncryptionKeys(t *testing.T) {
} }
func TestModel_GetUser_Has2FA(t *testing.T) { func TestModel_GetUser_Has2FA(t *testing.T) {
stop, _, user := startServerAndGetUser3(t) srv, _, user := startServerAndGetUser3(t)
defer stop() defer srv.Close()
if !user.Has2FA { if !user.Has2FA {
t.Errorf("user %s does not appear to have 2FA enabled", user.Name) t.Errorf("user %s does not appear to have 2FA enabled", user.Name)
} }
} }
func TestModel_GetUser_HasU2FRegistrations(t *testing.T) {
srv, _, user := startServerAndGetUser4(t)
defer srv.Close()
if n := len(user.U2FRegistrations); n != 2 {
t.Errorf("user %s has %d u2f registrations, expected 2", user.Name, n)
}
expectedKey := []byte{
164, 1, 2, 3, 38, 33, 88, 32, 182, 233, 26, 63, 41, 208, 70, 136, 89, 102, 192, 232, 56, 134, 225, 180, 18,
196, 51, 198, 91, 162, 121, 83, 86, 85, 224, 46, 64, 151, 99, 8, 34, 88, 32, 34, 176, 200, 116, 202, 44, 231,
42, 170, 189, 102, 70, 10, 9, 116, 206, 125, 3, 130, 59, 200, 44, 245, 249, 90, 172, 181, 184, 201, 81, 174, 182,
}
if !bytes.Equal(user.U2FRegistrations[0].PublicKey, expectedKey) {
t.Errorf("user %s has wrong public key for u2f registration: %v", user.Name, user.U2FRegistrations[0].PublicKey)
}
}
func TestModel_GetUser_Resources(t *testing.T) { func TestModel_GetUser_Resources(t *testing.T) {
stop, b, user := startServerAndGetUser(t) srv, b, user := startServerAndGetUser(t)
defer stop() defer srv.Close()
// Ensure that the user *has* resources. // Ensure that the user *has* resources.
if len(user.Resources) < 1 { if len(user.Resources) < 1 {
...@@ -176,8 +192,8 @@ func TestModel_GetUser_Resources(t *testing.T) { ...@@ -176,8 +192,8 @@ func TestModel_GetUser_Resources(t *testing.T) {
} }
func TestModel_GetUser_MailingListsAndNewsletters(t *testing.T) { func TestModel_GetUser_MailingListsAndNewsletters(t *testing.T) {
stop, _, user := startServerAndGetUser4(t) srv, _, user := startServerAndGetUser4(t)
defer stop() defer srv.Close()
// Ensure that the user has the expected number of list resources. // Ensure that the user has the expected number of list resources.
// The backend should find two lists, one of which has an alias as the owner. // The backend should find two lists, one of which has an alias as the owner.
...@@ -194,8 +210,8 @@ func TestModel_GetUser_MailingListsAndNewsletters(t *testing.T) { ...@@ -194,8 +210,8 @@ func TestModel_GetUser_MailingListsAndNewsletters(t *testing.T) {
} }
func TestModel_SearchUser(t *testing.T) { func TestModel_SearchUser(t *testing.T) {
stop, b := startServer(t) srv, b := startServer(t)
defer stop() defer srv.Close()
tx, _ := b.NewTransaction() tx, _ := b.NewTransaction()
users, err := tx.SearchUser(context.Background(), "uno", 0) users, err := tx.SearchUser(context.Background(), "uno", 0)
if err != nil { if err != nil {
...@@ -210,15 +226,14 @@ func TestModel_SearchUser(t *testing.T) { ...@@ -210,15 +226,14 @@ func TestModel_SearchUser(t *testing.T) {
} }
func TestModel_SetResourceStatus(t *testing.T) { func TestModel_SetResourceStatus(t *testing.T) {
stop := ldaptest.StartServer(t, &ldaptest.Config{ srv := ldaptest.StartServer(t, &ldaptest.Config{
Dir: "../../ldaptest", Dir: "../../ldaptest",
Port: testLDAPPort,
Base: "dc=example,dc=com", Base: "dc=example,dc=com",
LDIFs: []string{"testdata/base.ldif", "testdata/test1.ldif"}, LDIFs: []string{"testdata/base.ldif", "testdata/test1.ldif"},
}) })
defer stop() defer srv.Close()
b, err := NewLDAPBackend(testLDAPAddr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com") b, err := NewLDAPBackend(srv.Addr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
if err != nil { if err != nil {
t.Fatal("NewLDAPBackend", err) t.Fatal("NewLDAPBackend", err)
} }
...@@ -243,15 +258,14 @@ func TestModel_SetResourceStatus(t *testing.T) { ...@@ -243,15 +258,14 @@ func TestModel_SetResourceStatus(t *testing.T) {
} }
func TestModel_HasAnyResource(t *testing.T) { func TestModel_HasAnyResource(t *testing.T) {
stop := ldaptest.StartServer(t, &ldaptest.Config{ srv := ldaptest.StartServer(t, &ldaptest.Config{
Dir: "../../ldaptest", Dir: "../../ldaptest",
Port: testLDAPPort,
Base: "dc=example,dc=com", Base: "dc=example,dc=com",
LDIFs: []string{"testdata/base.ldif", "testdata/test1.ldif"}, LDIFs: []string{"testdata/base.ldif", "testdata/test1.ldif"},
}) })
defer stop() defer srv.Close()
b, err := NewLDAPBackend(testLDAPAddr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com") b, err := NewLDAPBackend(srv.Addr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
if err != nil { if err != nil {
t.Fatal("NewLDAPBackend", err) t.Fatal("NewLDAPBackend", err)
} }
...@@ -283,8 +297,8 @@ func TestModel_HasAnyResource(t *testing.T) { ...@@ -283,8 +297,8 @@ func TestModel_HasAnyResource(t *testing.T) {
} }
func TestModel_SearchResource(t *testing.T) { func TestModel_SearchResource(t *testing.T) {
stop, b := startServer(t) srv, b := startServer(t)
defer stop() defer srv.Close()
for _, pattern := range []string{"uno@investici.org", "uno*"} { for _, pattern := range []string{"uno@investici.org", "uno*"} {
tx, _ := b.NewTransaction() tx, _ := b.NewTransaction()
...@@ -302,8 +316,8 @@ func TestModel_SearchResource(t *testing.T) { ...@@ -302,8 +316,8 @@ func TestModel_SearchResource(t *testing.T) {
} }
func TestModel_SetUserPassword(t *testing.T) { func TestModel_SetUserPassword(t *testing.T) {
stop, b, user := startServerAndGetUser(t) srv, b, user := startServerAndGetUser(t)
defer stop() defer srv.Close()
encPass := "encrypted password" encPass := "encrypted password"
...@@ -333,8 +347,8 @@ func TestModel_SetUserPassword(t *testing.T) { ...@@ -333,8 +347,8 @@ func TestModel_SetUserPassword(t *testing.T) {
} }
func TestModel_SetUserEncryptionKeys_Add(t *testing.T) { func TestModel_SetUserEncryptionKeys_Add(t *testing.T) {
stop, b, user := startServerAndGetUser(t) srv, b, user := startServerAndGetUser(t)
defer stop() defer srv.Close()
tx, _ := b.NewTransaction() tx, _ := b.NewTransaction()
keys := []*ct.EncryptedKey{ keys := []*ct.EncryptedKey{
...@@ -352,8 +366,8 @@ func TestModel_SetUserEncryptionKeys_Add(t *testing.T) { ...@@ -352,8 +366,8 @@ func TestModel_SetUserEncryptionKeys_Add(t *testing.T) {
} }
func TestModel_SetUserEncryptionKeys_Replace(t *testing.T) { func TestModel_SetUserEncryptionKeys_Replace(t *testing.T) {
stop, b, user := startServerAndGetUser2(t) srv, b, user := startServerAndGetUser2(t)
defer stop() defer srv.Close()
tx, _ := b.NewTransaction() tx, _ := b.NewTransaction()
keys := []*ct.EncryptedKey{ keys := []*ct.EncryptedKey{
...@@ -371,8 +385,8 @@ func TestModel_SetUserEncryptionKeys_Replace(t *testing.T) { ...@@ -371,8 +385,8 @@ func TestModel_SetUserEncryptionKeys_Replace(t *testing.T) {
} }
func TestModel_NextUID(t *testing.T) { func TestModel_NextUID(t *testing.T) {
stop, b, user := startServerAndGetUser(t) srv, b, user := startServerAndGetUser(t)
defer stop() defer srv.Close()
tx, _ := b.NewTransaction() tx, _ := b.NewTransaction()
// User UID should not be available. // User UID should not be available.
......
...@@ -100,7 +100,8 @@ func setCommonResourceAttrs(entry *ldap.Entry, rsrc *as.Resource) { ...@@ -100,7 +100,8 @@ func setCommonResourceAttrs(entry *ldap.Entry, rsrc *as.Resource) {
func (reg *resourceRegistry) FromLDAP(entry *ldap.Entry) (rsrc *as.Resource, err error) { func (reg *resourceRegistry) FromLDAP(entry *ldap.Entry) (rsrc *as.Resource, err error) {
// Since we don't know what resource type to expect, we try // Since we don't know what resource type to expect, we try
// all known handlers until one returns a valid Resource. // all known handlers until one returns a valid Resource.
// This is slightly dangerous unless all // This expects that all object types can be told apart by
// their DN or attributes.
for _, h := range reg.handlers { for _, h := range reg.handlers {
rsrc, err = h.FromLDAP(entry) rsrc, err = h.FromLDAP(entry)
if err == nil { if err == nil {
...@@ -148,6 +149,20 @@ func (h *emailResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) ...@@ -148,6 +149,20 @@ func (h *emailResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error)
email := entry.GetAttributeValue("mail") email := entry.GetAttributeValue("mail")
// Also we don't want []byte("") for the PGP key, but a nil slice.
var openPGPKey *as.OpenPGPKey
if s := entry.GetAttributeValue("openPGPKey"); s != "" {
openPGPKey = &as.OpenPGPKey{
Key: []byte(s),
ID: entry.GetAttributeValue("openPGPKeyId"),
Hashes: entry.GetAttributeValues("openPGPKeyHash"),
}
// We're ok with openPgpKeyExpiry defaulting to zero.
// nolint: errcheck
openPGPKey.Expiry, _ = strconv.ParseInt(entry.GetAttributeValue("openPGPKeyExpiry"), 10, 64)
}
return &as.Resource{ return &as.Resource{
ID: as.ResourceID(entry.DN), ID: as.ResourceID(entry.DN),
Type: as.ResourceTypeEmail, Type: as.ResourceTypeEmail,
...@@ -155,17 +170,30 @@ func (h *emailResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error) ...@@ -155,17 +170,30 @@ func (h *emailResourceHandler) FromLDAP(entry *ldap.Entry) (*as.Resource, error)
Email: &as.Email{ Email: &as.Email{
Aliases: entry.GetAttributeValues("mailAlternateAddress"), Aliases: entry.GetAttributeValues("mailAlternateAddress"),
Maildir: entry.GetAttributeValue("mailMessageStore"), Maildir: entry.GetAttributeValue("mailMessageStore"),
OpenPGPKey: openPGPKey,
}, },
}, nil }, nil
} }
func (h *emailResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute { func (h *emailResourceHandler) ToLDAP(rsrc *as.Resource) []ldap.PartialAttribute {
return []ldap.PartialAttribute{ attrs := []ldap.PartialAttribute{
{Type: "objectClass", Vals: []string{"top", "virtualMailUser"}}, {Type: "objectClass", Vals: []string{"top", "virtualMailUser"}},
{Type: "mail", Vals: s2l(rsrc.Name)}, {Type: "mail", Vals: s2l(rsrc.Name)},
{Type: "mailAlternateAddress", Vals: rsrc.Email.Aliases}, {Type: "mailAlternateAddress", Vals: rsrc.Email.Aliases},
{Type: "mailMessageStore", Vals: s2l(rsrc.Email.Maildir)}, {Type: "mailMessageStore", Vals: s2l(rsrc.Email.Maildir)},
} }
if pgpKey := rsrc.Email.OpenPGPKey; pgpKey != nil {
attrs = append(attrs, ldap.PartialAttribute{Type: "openPGPKey", Vals: by2l(pgpKey.Key)})
attrs = append(attrs, ldap.PartialAttribute{Type: "openPGPKeyId", Vals: s2l(pgpKey.ID)})
attrs = append(attrs, ldap.PartialAttribute{Type: "openPGPKeyHash", Vals: pgpKey.Hashes})
attrs = append(attrs, ldap.PartialAttribute{Type: "openPGPKeyExpiry", Vals: []string{strconv.FormatInt(pgpKey.Expiry, 10)}})
} else {
// Empty attrs.
for _, t := range []string{"openPGPKey", "openPGPKeyId", "openPGPKeyHash", "openPGPKeyExpiry"} {
attrs = append(attrs, ldap.PartialAttribute{Type: t})
}
}
return attrs
} }
func (h *emailResourceHandler) SearchQuery() *queryTemplate { func (h *emailResourceHandler) SearchQuery() *queryTemplate {
......
...@@ -19,6 +19,8 @@ sn: quattro@investici.org ...@@ -19,6 +19,8 @@ sn: quattro@investici.org
uid: quattro@investici.org uid: quattro@investici.org
uidNumber: 23801 uidNumber: 23801
userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0 userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
u2fRegistration:: BLbpGj8p0EaIWWbA6DiG4bQSxDPGW6J5U1ZV4C5Al2MIIrDIdMos5yqqvWZGCgl0zn0DgjvILPX5Wqy1uMlRrrbuJtcRvBQ9DEZZJmMP5CJAJqdKLG07kezOPeLQRNTjhKnW0Zixqzc8jIlqMX/+no675UeHYXr7VSmKALYekyVk
u2fRegistration:: BCCBvjcPNk4xn7Vi2YbJA8alBwIL7pkIkmtdZJwZ9Bcz4EzyE9As/9x43WwvNzaFHvqiB34hncw6IHq/SQrAq/XpdfSnqSm9tYskcbgWcNwsrXhpjTu9Pi9UyWNZtEG4nFGGFRmuNNpjA5C/P2A9V/DIat17nWE4hndFupMU2kVG
totpSecret: ABCDEF totpSecret: ABCDEF
dn: mail=quattro@investici.org,uid=quattro@investici.org,ou=People,dc=example,dc=com dn: mail=quattro@investici.org,uid=quattro@investici.org,ou=People,dc=example,dc=com
......
...@@ -8,6 +8,8 @@ import ( ...@@ -8,6 +8,8 @@ import (
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
) )
var debugLDAP = false
// Generic interface to LDAP - allows us to stub out the LDAP client while // Generic interface to LDAP - allows us to stub out the LDAP client while
// testing. // testing.
type ldapConn interface { type ldapConn interface {
...@@ -132,14 +134,18 @@ func (tx *ldapTX) Commit(ctx context.Context) error { ...@@ -132,14 +134,18 @@ func (tx *ldapTX) Commit(ctx context.Context) error {
if isEmptyAddRequest(ar) { if isEmptyAddRequest(ar) {
continue continue
} }
if debugLDAP {
log.Printf("issuing AddRequest: %+v", ar) log.Printf("issuing AddRequest: %+v", ar)
}
err = tx.conn.Add(ctx, ar) err = tx.conn.Add(ctx, ar)
} else { } else {
mr := mods[dn] mr := mods[dn]
if isEmptyModifyRequest(mr) { if isEmptyModifyRequest(mr) {
continue continue
} }
if debugLDAP {
log.Printf("issuing ModifyRequest: %+v", mr) log.Printf("issuing ModifyRequest: %+v", mr)
}
err = tx.conn.Modify(ctx, mr) err = tx.conn.Modify(ctx, mr)
} }
if err != nil { if err != nil {
...@@ -190,7 +196,9 @@ func (tx *ldapTX) updateModifyRequest(ctx context.Context, mr *ldap.ModifyReques ...@@ -190,7 +196,9 @@ func (tx *ldapTX) updateModifyRequest(ctx context.Context, mr *ldap.ModifyReques
// perform an Add or a Replace. // perform an Add or a Replace.
old, ok := tx.rcache[cacheKey(attr.dn, attr.attr)] old, ok := tx.rcache[cacheKey(attr.dn, attr.attr)]
if !ok { if !ok {
if debugLDAP {
log.Printf("tx: pessimistic fallback for %s %s", attr.dn, attr.attr) log.Printf("tx: pessimistic fallback for %s %s", attr.dn, attr.attr)
}
oldFromLDAP := tx.readAttributeValuesNoCache(ctx, attr.dn, attr.attr) oldFromLDAP := tx.readAttributeValuesNoCache(ctx, attr.dn, attr.attr)
if len(oldFromLDAP) > 0 { if len(oldFromLDAP) > 0 {
ok = true ok = true
......
...@@ -88,6 +88,15 @@ func s2l(s string) []string { ...@@ -88,6 +88,15 @@ func s2l(s string) []string {
return []string{s} return []string{s}
} }
// Convert a []byte to a []string with a single item, or nil if the
// slice is empty.
func by2l(b []byte) []string {
if len(b) == 0 {
return nil
}
return []string{string(b)}
}
// Returns true if a LDAP object has the specified objectClass. // Returns true if a LDAP object has the specified objectClass.
func isObjectClass(entry *ldap.Entry, class string) bool { func isObjectClass(entry *ldap.Entry, class string) bool {
classes := entry.GetAttributeValues("objectClass") classes := entry.GetAttributeValues("objectClass")
......
...@@ -4,15 +4,15 @@ import ( ...@@ -4,15 +4,15 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os"
"strings" "strings"
"git.autistici.org/ai3/accountserver" "git.autistici.org/ai3/accountserver"
"git.autistici.org/ai3/go-common/clientutil" "git.autistici.org/ai3/go-common/clientutil"
"git.autistici.org/ai3/go-common/pwhash" "git.autistici.org/ai3/go-common/pwhash"
"git.autistici.org/ai3/go-common/serverutil" "git.autistici.org/ai3/go-common/serverutil"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
auxdbbackend "git.autistici.org/ai3/accountserver/backend/auxdb" auxdbbackend "git.autistici.org/ai3/accountserver/backend/auxdb"
cachebackend "git.autistici.org/ai3/accountserver/backend/cache" cachebackend "git.autistici.org/ai3/accountserver/backend/cache"
...@@ -93,7 +93,7 @@ func (c *config) getPasswordHash() (h pwhash.PasswordHash, err error) { ...@@ -93,7 +93,7 @@ func (c *config) getPasswordHash() (h pwhash.PasswordHash, err error) {
if i, ok := c.PwHash.Params["threads"]; ok { if i, ok := c.PwHash.Params["threads"]; ok {
pThreads = i pThreads = i
} }
h = pwhash.NewArgon2WithParams(uint32(pTime), uint32(pMem), uint8(pThreads)) h = pwhash.NewArgon2StdWithParams(uint32(pTime), uint32(pMem), uint8(pThreads))
case "scrypt": case "scrypt":
pN := 16384 pN := 16384
if i, ok := c.PwHash.Params["n"]; ok { if i, ok := c.PwHash.Params["n"]; ok {
...@@ -116,12 +116,13 @@ func (c *config) getPasswordHash() (h pwhash.PasswordHash, err error) { ...@@ -116,12 +116,13 @@ func (c *config) getPasswordHash() (h pwhash.PasswordHash, err error) {
func loadConfig(path string) (*config, error) { func loadConfig(path string) (*config, error) {
// Read YAML config. // Read YAML config.
data, err := ioutil.ReadFile(path) f, err := os.Open(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer f.Close()
var c config var c config
if err := yaml.Unmarshal(data, &c); err != nil { if err := yaml.NewDecoder(f).Decode(&c); err != nil {
return nil, err return nil, err
} }
if err := c.validate(); err != nil { if err := c.validate(); err != nil {
...@@ -134,7 +135,7 @@ func getBindPw(c *config) (string, error) { ...@@ -134,7 +135,7 @@ func getBindPw(c *config) (string, error) {
// Read the bind password. // Read the bind password.
bindPw := c.LDAP.BindPw bindPw := c.LDAP.BindPw
if c.LDAP.BindPwFile != "" { if c.LDAP.BindPwFile != "" {
pwData, err := ioutil.ReadFile(c.LDAP.BindPwFile) pwData, err := os.ReadFile(c.LDAP.BindPwFile)
if err != nil { if err != nil {
return "", err return "", err
} }
......
...@@ -2,7 +2,7 @@ package accountserver ...@@ -2,7 +2,7 @@ package accountserver
import ( import (
"errors" "errors"
"io/ioutil" "os"
"git.autistici.org/ai3/go-common/clientutil" "git.autistici.org/ai3/go-common/clientutil"
"git.autistici.org/id/go-sso" "git.autistici.org/id/go-sso"
...@@ -82,7 +82,7 @@ func (c *Config) templateContext() *templateContext { ...@@ -82,7 +82,7 @@ func (c *Config) templateContext() *templateContext {
} }
func (c *Config) ssoValidator() (sso.Validator, error) { func (c *Config) ssoValidator() (sso.Validator, error) {
pkey, err := ioutil.ReadFile(c.SSO.PublicKeyFile) pkey, err := os.ReadFile(c.SSO.PublicKeyFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
...@@ -5,6 +5,7 @@ After=network.target ...@@ -5,6 +5,7 @@ After=network.target
[Service] [Service]
User=accountserver User=accountserver
Group=accountserver Group=accountserver
ConditionPathExists=/etc/accountserver/config.yml
EnvironmentFile=-/etc/default/accountserver EnvironmentFile=-/etc/default/accountserver
ExecStart=/usr/bin/accountserver --addr $ADDR ExecStart=/usr/bin/accountserver --addr $ADDR
Restart=always Restart=always
......