Commit c5d3b1a5 authored by ale's avatar ale

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