Commit c5d3b1a5 authored by ale's avatar ale
Browse files

Add shard and status to the User type

The shard is kept in sync with the email resource shard. CreateUser
validation enforces a single email resource per account.
parent 8a40fce7
Pipeline #1563 passed with stages
in 1 minute and 37 seconds
......@@ -115,6 +115,7 @@ type CreateUserRequest struct {
func (r *CreateUserRequest) applyTemplate(rctx *RequestContext) error {
// Some fields should be always unset because there are
// specific methods to modify them.
r.User.Status = UserStatusActive
r.User.Has2FA = false
r.User.HasOTP = false
r.User.HasEncryptionKeys = true // set to true so that resetPassword will create keys.
......@@ -136,6 +137,7 @@ func (r *CreateUserRequest) applyTemplate(rctx *RequestContext) error {
for _, rsrc := range r.User.Resources {
rctx.resourceTemplates.applyTemplate(rctx.Context, rsrc, r.User)
}
return nil
}
......@@ -145,17 +147,38 @@ func (r *CreateUserRequest) Validate(rctx *RequestContext) error {
return err
}
// Validate the user *and* all resources.
// Validate the user *and* all resources. The request must contain at
// least one email resource with the same name as the user.
if err := rctx.userValidator(rctx.Context, r.User); err != nil {
log.Printf("validation error while creating user %+v: %v", r.User, err)
return err
}
var emailCount int
for _, rsrc := range r.User.Resources {
if err := rctx.resourceValidator.validateResource(rctx.Context, rsrc, r.User); err != nil {
log.Printf("validation error while creating resource %+v: %v", rsrc, err)
return err
}
if rsrc.ID.Type() == ResourceTypeEmail {
emailCount++
}
}
if emailCount == 0 {
return errors.New("missing email resource")
}
if emailCount > 1 {
return errors.New("too many email resources")
}
email := r.User.GetSingleResourceByType(ResourceTypeEmail)
if email.Name != r.User.Name {
return errors.New("user and email resource names do not match")
}
// Now that validation is done, finalize the object by setting some derived parameters.
// Set the user shard to the email shard.
r.User.Shard = email.Shard
return nil
}
......
......@@ -115,6 +115,16 @@ func (r *MoveResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
resources = append(resources, rctx.User.GetResourcesByGroup(rctx.Resource.Group)...)
}
// We need to enforce consistency between email resources and
// the user shard, so that temporary data can be colocated
// with email storage.
if rctx.Resource.ID.Type() == ResourceTypeEmail && rctx.User.Shard != r.Shard {
rctx.User.Shard = r.Shard
if err := rctx.TX.UpdateUser(rctx.Context, &rctx.User.User); err != nil {
return nil, err
}
}
var resp MoveResourceResponse
for _, rsrc := range resources {
rsrc.Shard = r.Shard
......
......@@ -183,13 +183,16 @@ func createFakeBackend() *fakeBackend {
encryptionKeys: make(map[string][]*UserEncryptionKey),
}
fb.addUser(&User{
Name: "testuser",
UID: 4242,
Name: "testuser",
Status: UserStatusActive,
Shard: "1",
UID: 4242,
Resources: []*Resource{
{
ID: NewResourceID(ResourceTypeEmail, "testuser", "testuser@example.com"),
Name: "testuser@example.com",
Status: ResourceStatusActive,
Shard: "1",
Email: &Email{
Maildir: "example.com/testuser",
},
......@@ -259,7 +262,8 @@ func TestService_GetUser_ResourceGroups(t *testing.T) {
svc, _ := newAccountServiceWithSSO(fb, testConfig(), &fakeValidator{})
fb.addUser(&User{
Name: "testuser2",
Name: "testuser2",
Status: UserStatusActive,
Resources: []*Resource{
{
ID: NewResourceID(ResourceTypeDAV, "testuser2", "dav1"),
......
......@@ -10,10 +10,17 @@ import (
// GetUserRequest is the request type for GetUserAction.
type GetUserRequest struct {
UserRequestBase
// Whether to return an inactive user.
IncludeInactive bool `json:"include_inactive"`
}
// Serve the request.
func (r *GetUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
if !r.IncludeInactive && rctx.User.Status != UserStatusActive {
return nil, ErrUserNotFound
}
// Return the public User object contained within the RawUser.
return &rctx.User.User, nil
}
......
......@@ -127,9 +127,11 @@ func newUser(entry *ldap.Entry) (*as.RawUser, error) {
uidNumber, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
user := &as.RawUser{
User: as.User{
Name: entry.GetAttributeValue("uid"),
Lang: entry.GetAttributeValue(preferredLanguageLDAPAttr),
UID: uidNumber,
Name: entry.GetAttributeValue("uid"),
Lang: entry.GetAttributeValue(preferredLanguageLDAPAttr),
UID: uidNumber,
Status: entry.GetAttributeValue("status"),
Shard: entry.GetAttributeValue("host"),
LastPasswordChangeStamp: decodeShadowTimestamp(entry.GetAttributeValue(passwordLastChangeLDAPAttr)),
AccountRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr),
U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)),
......@@ -162,6 +164,8 @@ func userToLDAP(user *as.User) (attrs []ldap.PartialAttribute) {
{Type: "loginShell", Vals: []string{"/bin/false"}},
{Type: "homeDirectory", Vals: []string{"/var/empty"}},
{Type: passwordLastChangeLDAPAttr, Vals: []string{"12345"}},
{Type: "status", Vals: []string{user.Status}},
{Type: "host", Vals: []string{user.Shard}},
{Type: "shadowWarning", Vals: []string{"7"}},
{Type: "shadowMax", Vals: []string{"99999"}},
{Type: preferredLanguageLDAPAttr, Vals: s2l(user.Lang)},
......
......@@ -15,7 +15,8 @@ const (
testLDAPPort = 42871
testLDAPAddr = "ldap://127.0.0.1:42871"
testUser1 = "uno@investici.org"
testUser2 = "due@investici.org"
testUser2 = "due@investici.org" // has encryption keys
testUser3 = "tre@investici.org" // has OTP
)
func startServerAndGetUser(t testing.TB) (func(), as.Backend, *as.RawUser) {
......@@ -26,6 +27,10 @@ func startServerAndGetUser2(t testing.TB) (func(), as.Backend, *as.RawUser) {
return startServerAndGetUserWithName(t, testUser2)
}
func startServerAndGetUser3(t testing.TB) (func(), as.Backend, *as.RawUser) {
return startServerAndGetUserWithName(t, testUser3)
}
func startServer(t testing.TB) (func(), as.Backend) {
stop := ldaptest.StartServer(t, &ldaptest.Config{
Dir: "../ldaptest",
......@@ -35,6 +40,7 @@ func startServer(t testing.TB) (func(), as.Backend) {
"testdata/base.ldif",
"testdata/test1.ldif",
"testdata/test2.ldif",
"testdata/test3.ldif",
},
})
......@@ -119,15 +125,21 @@ func TestModel_GetUser(t *testing.T) {
}
}
func TestModel_GetUser_Has2FA(t *testing.T) {
func TestModel_GetUser_HasEncryptionKeys(t *testing.T) {
stop, _, user := startServerAndGetUser2(t)
defer stop()
if !user.Has2FA {
t.Errorf("user %s does not appear to have 2FA enabled", testUser2)
}
if !user.HasEncryptionKeys {
t.Errorf("user %s does not appear to have encryption keys", testUser2)
t.Errorf("user %s does not appear to have encryption keys", user.Name)
}
}
func TestModel_GetUser_Has2FA(t *testing.T) {
stop, _, user := startServerAndGetUser3(t)
defer stop()
if !user.Has2FA {
t.Errorf("user %s does not appear to have 2FA enabled", user.Name)
}
}
......
......@@ -32,7 +32,7 @@ userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL
TjNUTS53YXkyZHZSd1g2YTQ0dVVXZ2tYL1pzbkc4YXdHRFhYVGYwNU1VeE1saWdIMA==
uidNumber: 19475
host: host2
mailAlternateAddress: uno@anche.no
mailAlternateAddress: alias@example.com
recoverAnswer: {crypt}$1$wtEa4TKB$lxeyenkQ1yfxECn7WVQQ0/
gidNumber: 2000
mail: uno@investici.org
......
......@@ -19,7 +19,6 @@ sn: due@investici.org
uid: due@investici.org
uidNumber: 256799
userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
totpSecret: ABCDEFGH
dn: mail=due@investici.org,uid=due@investici.org,ou=People,dc=example,dc=com
creationDate: 01-08-2013
......@@ -33,6 +32,57 @@ originalHost: host2
status: active
uidNumber: 256799
userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
storagePublicKey:: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUU0d0VBWUhLb1pJemowQ0FRWUZLNEVFQUNFRE9nQUVTeVVyVFhaaHRTeFpreityQjYwaFM4VnhINWozM3Ftbgphb3h2WG9IeG9vYU9Sc0x5TXNnVE5RVDR1bU1XdU12U3ROamszeWdWR2FNPQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0K
storageEncryptedSecretKey:: rffWTx7AjmhqD8li78Tal+7zfIbOFSyX4sKxKFa/bj5XAlWLyq7ANWiJB/0PRC1y8JBg+ezti5DjC5Ft82f5uwb2+3vIxjrxyz5vAmSUjRYo7o9alu5vLXRapsyhiYgJGmJrBJZxkQ9rGDXsM4OfZQNlxP4AVMobQFQU9X4QBUWFo2MwKuvwQiHg359hLufUrmr2bmjzPsU5Uj+8vAeQWHsVxWuUwUuob630A2V619iO5cp5nPzk5itYMNkdl1eR3KvUonvqwz++HLRJNqwh7qn2CjdUIA5ljexFg88UbNbzrpa+6Atmd4iXieYPewYHPHtuRFV3eHHlnBbv8VMcQdVZ0sqJokWDRvFjJbg=
storagePublicKey:: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUU0d0VBWUhLb1pJemowQ0FRWUZLNEVFQUNFRE9nQUVKaDFSdW1MTkt3M1dBTXNSMFFpMVRrc2pJSC9udCtwaApGRjZjdmhPZFN3anhsOVhFVVovVzlONWdDcUlqbGl0eFk2VHdSZWlzZThrPQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0K
storageEncryptedSecretKey:: bWFpbjoBAQAAAAAQAAAEIKLsDnzqCxHAmVXiSi3WT77YqNDY5sW0les/rC0owl0MegC3frvzAGG7vr6PhYNozksn2Fw4tQbj7G52HeQr3V8R58J3F2CHLdiwLGDMKCNy1hjyCN6rXCp1OQqg4VWtEovumogA4FaJtZS74WnCP2YGcxJy/0Am3U2TlFmx0e0jzuCk9lZ8piX+YKR6c8Qh/bv5vjq2gZ9AO2nh5Q==
dn: ftpname=due,uid=due@investici.org,ou=People,dc=example,dc=com
status: active
givenName: Private
cn: due
objectClass: top
objectClass: person
objectClass: posixAccount
objectClass: shadowAccount
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: ftpAccount
loginShell: /bin/false
userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
shadowWarning: 7
uidNumber: 256799
host: host2
shadowMax: 99999
ftpname: due
gidNumber: 33
gecos: FTP Account for due@investici.org
sn: Private
homeDirectory: /home/users/investici.org/due
uid: due
creationDate: 01-08-2013
shadowLastChange: 12345
originalHost: host2
dn: alias=due,uid=due@investici.org,ou=People,dc=example,dc=com
status: active
parentSite: autistici.org
objectClass: top
objectClass: subSite
alias: due
host: host2
documentRoot: /home/users/investici.org/due/html-due
creationDate: 01-08-2013
originalHost: host2
statsId: 2193
option: php
dn: dbname=due,alias=due,uid=due@investici.org,ou=People,dc=example,dc=com
clearPassword: tae1tei8eir7wae1OZaeXXX
dbname: due
dbuser: due
host: host2
originalHost: host2
mysqlPassword: *7DD66AA8CD1E7687B993732C3A87CFFA43B95E27
objectClass: top
objectClass: dbMysql
status: active
dn: uid=tre@investici.org,ou=People,dc=example,dc=com
cn: tre@investici.org
gecos: tre@investici.org
gidNumber: 2000
givenName: Private
homeDirectory: /var/empty
loginShell: /bin/false
objectClass: top
objectClass: person
objectClass: posixAccount
objectClass: shadowAccount
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: investiciUser
shadowLastChange: 12345
shadowMax: 99999
shadowWarning: 7
sn: tre@investici.org
uid: tre@investici.org
uidNumber: 256799
userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
totpSecret: ABCDEF
dn: mail=tre@investici.org,uid=tre@investici.org,ou=People,dc=example,dc=com
creationDate: 01-08-2013
gidNumber: 2000
host: host2
mail: tre@investici.org
mailMessageStore: investici.org/tre/
objectClass: top
objectClass: virtualMailUser
originalHost: host2
status: active
uidNumber: 256800
userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
......@@ -577,6 +577,29 @@ func TestIntegration_AddEmailAlias(t *testing.T) {
}
}
func TestIntegration_MoveResource(t *testing.T) {
stop, be, c := startService(t)
defer stop()
var resp as.MoveResourceResponse
err := c.request("/api/resource/move", &as.MoveResourceRequest{
AdminResourceRequestBase: as.AdminResourceRequestBase{
ResourceRequestBase: as.ResourceRequestBase{
RequestBase: as.RequestBase{
SSO: c.ssoTicket(testAdminUser),
},
ResourceID: as.NewResourceID(as.ResourceTypeEmail, "uno@investici.org", "uno@investici.org"),
},
},
Shard: "host1",
}, &resp)
if err != nil {
t.Fatalf("MoveResource: %v", err)
}
checkUserInvariants(t, be, "uno@investici.org", "password")
}
// Verify that some user authentication invariants are true. Returns
// the RawUser for further checks.
func checkUserInvariants(t *testing.T, be as.Backend, username, primaryPassword string) *as.RawUser {
......@@ -598,6 +621,15 @@ func checkUserInvariants(t *testing.T, be as.Backend, username, primaryPassword
}
}
// Verify that the user shard matches the email resource shard.
email := user.GetSingleResourceByType(as.ResourceTypeEmail)
if email == nil {
t.Fatalf("no email resources for user %s", username)
}
if user.Shard != email.Shard {
t.Fatalf("user and email shards differ ('%s' vs '%s')", user.Shard, email.Shard)
}
return user
}
......
......@@ -16,11 +16,13 @@ sn: Private
homeDirectory: /var/empty
uid: uno@investici.org
givenName: Private
shadowLastChange: 12345
shadowLastChange: 17849
shadowWarning: 7
preferredLanguage: it
userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL3JWTlA4N1lT
TjNUTS53YXkyZHZSd1g2YTQ0dVVXZ2tYL1pzbkc4YXdHRFhYVGYwNU1VeE1saWdIMA==
status: active
host: host2
dn: mail=uno@investici.org,uid=uno@investici.org,ou=People,dc=example,dc=com
status: active
......@@ -63,7 +65,7 @@ sn: Private
homeDirectory: /home/users/investici.org/uno
uid: uno
creationDate: 01-08-2013
shadowLastChange: 12345
shadowLastChange: 17849
originalHost: host2
userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL3JWTlA4N1lT
TjNUTS53YXkyZHZSd1g2YTQ0dVVXZ2tYL1pzbkc4YXdHRFhYVGYwNU1VeE1saWdIMA==
......
......@@ -19,6 +19,8 @@ sn: due@investici.org
uid: due@investici.org
uidNumber: 256799
userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
status: active
host: host2
dn: mail=due@investici.org,uid=due@investici.org,ou=People,dc=example,dc=com
creationDate: 01-08-2013
......
dn: uid=tre@investici.org,ou=People,dc=example,dc=com
cn: tre@investici.org
gecos: tre@investici.org
gidNumber: 2000
givenName: Private
homeDirectory: /var/empty
loginShell: /bin/false
objectClass: top
objectClass: person
objectClass: posixAccount
objectClass: shadowAccount
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: investiciUser
shadowLastChange: 12345
shadowMax: 99999
shadowWarning: 7
sn: tre@investici.org
uid: tre@investici.org
uidNumber: 256799
userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
totpSecret: ABCDEF
status: active
host: host2
dn: mail=tre@investici.org,uid=tre@investici.org,ou=People,dc=example,dc=com
creationDate: 01-08-2013
gidNumber: 2000
host: host2
mail: tre@investici.org
mailMessageStore: investici.org/tre/
objectClass: top
objectClass: virtualMailUser
originalHost: host2
status: active
uidNumber: 256800
userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
......@@ -14,6 +14,12 @@ import (
"github.com/tstranex/u2f"
)
// Possible values for user status.
const (
UserStatusActive = "active"
UserStatusInactive = "inactive"
)
// User information, public: includes data *about* credentials, but
// not the credentials themselves. Every user has a unique
// identifier, which may be an email address.
......@@ -31,6 +37,12 @@ type User struct {
// RFC3339 string in JSON.
LastPasswordChangeStamp time.Time `json:"last_password_change_stamp"`
// User status.
Status string `json:"status"`
// Shard for temporary resources (must match the email resources).
Shard string `json:"shard"`
// Has2FA is true if the user has a second-factor authentication
// mechanism properly set up. In practice, this is the case if either
// HasOTP is true, or len(U2FRegistrations) > 0.
......
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