Commit ce95ac1c authored by ale's avatar ale

Add APIs to create users and resources

parent 33f0b133
......@@ -674,7 +674,7 @@ func (s *AccountService) CreateResources(ctx context.Context, tx TX, req *Create
log.Printf("validation error while creating resource %+v: %v", r, err)
return err
}
if err := tx.UpdateResource(ctx, r); err != nil {
if err := tx.CreateResource(ctx, r); err != nil {
return err
}
resp.IDs = append(resp.IDs, r.ID)
......@@ -684,6 +684,44 @@ func (s *AccountService) CreateResources(ctx context.Context, tx TX, req *Create
return &resp, err
}
type CreateUserRequest struct {
SSO string `json:"sso"`
User *User `json:"user"`
}
type CreateUserResponse struct {
User *User `json:"user,omitempty"`
}
func (s *AccountService) CreateUser(ctx context.Context, tx TX, req *CreateUserRequest) (*CreateUserResponse, error) {
ctx, err := s.authorizeAdminGeneric(ctx, tx, req.SSO)
if err != nil {
return nil, err
}
var resp CreateUserResponse
err = s.withRequest(ctx, req, func(ctx context.Context) error {
// Validate the user *and* all resources.
if err := s.userValidator(ctx, req.User); err != nil {
log.Printf("validation error while creating user %+v: %v", req.User, err)
return err
}
for _, r := range req.User.Resources {
if err := s.resourceValidator.validateResource(ctx, r); err != nil {
log.Printf("validation error while creating resource %+v: %v", r, err)
return err
}
}
if err := tx.CreateUser(ctx, req.User); err != nil {
return err
}
resp.User = req.User
return nil
})
return &resp, err
}
const appSpecificPasswordLen = 64
func randomBase64(n int) string {
......
......@@ -27,6 +27,11 @@ func (b *fakeBackend) GetUser(_ context.Context, username string) (*User, error)
return b.users[username], nil
}
func (b *fakeBackend) CreateUser(_ context.Context, user *User) error {
b.users[user.Name] = user
return nil
}
func (b *fakeBackend) GetResource(_ context.Context, resourceID ResourceID) (*Resource, error) {
return b.resources[resourceID.User()][resourceID.String()], nil
}
......@@ -36,6 +41,11 @@ func (b *fakeBackend) UpdateResource(_ context.Context, r *Resource) error {
return nil
}
func (b *fakeBackend) CreateResource(_ context.Context, r *Resource) error {
b.resources[r.ID.User()][r.ID.String()] = r
return nil
}
func (b *fakeBackend) SetUserPassword(_ context.Context, user *User, password string) error {
b.passwords[user.Name] = password
return nil
......@@ -317,7 +327,7 @@ func TestService_AddEmailAlias(t *testing.T) {
}
}
func TestService_Create(t *testing.T) {
func TestService_CreateResource(t *testing.T) {
svc, tx := testService("")
req := &CreateResourcesRequest{
......@@ -349,3 +359,36 @@ func TestService_Create(t *testing.T) {
t.Fatal("creating a duplicate resource did not fail")
}
}
func TestService_CreateUser(t *testing.T) {
svc, tx := testService("")
req := &CreateUserRequest{
User: &User{
Name: "testuser2@example.com",
Resources: []*Resource{
&Resource{
ID: NewResourceID(ResourceTypeDomain, "testuser2@example.com", "example2.com"),
Name: "example2.com",
Status: ResourceStatusActive,
Shard: "host2",
OriginalShard: "host2",
Website: &Website{
URL: "https://example2.com",
DocumentRoot: "/home/sites/example2.com",
AcceptMail: true,
},
},
},
},
}
// The request should succeed the first time around.
resp, err := svc.CreateUser(context.Background(), tx, req)
if err != nil {
t.Fatal("CreateResources", err)
}
if resp.User.Name != "testuser2@example.com" {
t.Fatalf("unexpected user in response: got %s, expected testuser2", resp.User.Name)
}
}
......@@ -109,10 +109,48 @@ func newUser(entry *ldap.Entry) (*accountserver.User, error) {
return user, nil
}
func userToLDAP(user *accountserver.User) (attrs []ldap.PartialAttribute) {
// Most attributes are read-only and have specialized methods to set them.
attrs = append(attrs, []ldap.PartialAttribute{
{Type: "objectClass", Vals: []string{"top", "person", "posixAccount", "shadowAccount", "organizationalPerson", "inetOrgPerson", "totpAccount"}},
{Type: "uid", Vals: s2l(user.Name)},
{Type: "cn", Vals: s2l(user.Name)},
{Type: "givenName", Vals: []string{"Private"}},
{Type: "sn", Vals: []string{"Private"}},
{Type: "gecos", Vals: s2l(user.Name)},
{Type: "loginShell", Vals: []string{"/bin/false"}},
{Type: "homeDirectory", Vals: []string{"/var/empty"}},
{Type: "shadowLastChange", Vals: []string{"12345"}},
{Type: "shadowWarning", Vals: []string{"7"}},
{Type: "shadowMax", Vals: []string{"99999"}},
{Type: preferredLanguageLDAPAttr, Vals: s2l(user.Lang)},
}...)
return
}
func (tx *backendTX) getUserDN(user *accountserver.User) string {
return joinDN("uid="+user.Name, "ou=People", tx.backend.baseDN)
}
// CreateUser creates a new user.
func (tx *backendTX) CreateUser(ctx context.Context, user *accountserver.User) error {
dn := tx.getUserDN(user)
tx.create(dn)
for _, attr := range userToLDAP(user) {
tx.setAttr(dn, attr.Type, attr.Vals...)
}
// Create all resources.
for _, r := range user.Resources {
if err := tx.CreateResource(ctx, r); err != nil {
return err
}
}
return nil
}
// GetUser returns a user.
func (tx *backendTX) GetUser(ctx context.Context, username string) (*accountserver.User, error) {
// First of all, find the main user object, and just that one.
......@@ -329,6 +367,21 @@ func (tx *backendTX) GetResource(ctx context.Context, rsrcID accountserver.Resou
return tx.backend.resources.FromLDAPWithType(rsrcID.Type(), result.Entries[0])
}
// CreateResource creates a new LDAP-backed resource object.
func (tx *backendTX) CreateResource(ctx context.Context, r *accountserver.Resource) error {
dn, err := tx.backend.resources.GetDN(r.ID)
if err != nil {
return err
}
tx.create(dn)
for _, attr := range tx.backend.resources.ToLDAP(r) {
tx.setAttr(dn, attr.Type, attr.Vals...)
}
return nil
}
// UpdateResource updates a LDAP-backed resource that was obtained by a previous GetResource call.
func (tx *backendTX) UpdateResource(ctx context.Context, r *accountserver.Resource) error {
dn, err := tx.backend.resources.GetDN(r.ID)
......
......@@ -42,13 +42,15 @@ type ldapTX struct {
conn ldapConn
cache map[string][]string
newDNs map[string]struct{}
changes []ldapAttr
}
func newLDAPTX(conn ldapConn) *ldapTX {
return &ldapTX{
conn: conn,
cache: make(map[string][]string),
conn: conn,
cache: make(map[string][]string),
newDNs: make(map[string]struct{}),
}
}
......@@ -72,6 +74,12 @@ func (tx *ldapTX) search(ctx context.Context, req *ldap.SearchRequest) (*ldap.Se
return res, nil
}
// Announce the intention to create a new object. To be called before
// setAttr() on the new DN.
func (tx *ldapTX) create(dn string) {
tx.newDNs[dn] = struct{}{}
}
// setAttr modifies a single attribute of an object. To delete an
// attribute, pass an empty list of values.
func (tx *ldapTX) setAttr(dn, attr string, values ...string) {
......@@ -85,33 +93,67 @@ func (tx *ldapTX) setAttr(dn, attr string, values ...string) {
func (tx *ldapTX) Commit(ctx context.Context) error {
// Iterate through the changes, and generate ModifyRequest
// objects grouped by DN (while preserving the order of DNs).
var dns []string
mods := make(map[string]*ldap.ModifyRequest)
for _, c := range tx.changes {
mr, ok := mods[c.dn]
if !ok {
mr = ldap.NewModifyRequest(c.dn)
mods[c.dn] = mr
dns = append(dns, c.dn)
}
tx.updateModifyRequest(ctx, mr, c)
}
adds, mods, dns := tx.aggregateChanges(ctx)
// Now issue all ModifyRequests, one by one. Abort on the first error.
// Now issue all Modify or Add requests, one by one, in the
// same order as we have seen them. Abort on the first error.
for _, dn := range dns {
mr := mods[dn]
if isEmptyModifyRequest(mr) {
continue
var err error
if ar, ok := adds[dn]; ok {
if isEmptyAddRequest(ar) {
continue
}
log.Printf("issuing AddRequest: %+v", ar)
err = tx.conn.Add(ctx, ar)
} else {
mr := mods[dn]
if isEmptyModifyRequest(mr) {
continue
}
log.Printf("issuing ModifyRequest: %+v", mr)
err = tx.conn.Modify(ctx, mr)
}
log.Printf("issuing ModifyRequest: %+v", mr)
if err := tx.conn.Modify(ctx, mr); err != nil {
if err != nil {
return err
}
}
// Cleanup
tx.changes = nil
tx.newDNs = make(map[string]struct{})
return nil
}
// Helper for Commit that aggregates changes into add and modify lists.
func (tx *ldapTX) aggregateChanges(ctx context.Context) (map[string]*ldap.AddRequest, map[string]*ldap.ModifyRequest, []string) {
var dns []string
mods := make(map[string]*ldap.ModifyRequest)
adds := make(map[string]*ldap.AddRequest)
for _, c := range tx.changes {
if _, isNew := tx.newDNs[c.dn]; isNew {
ar, ok := adds[c.dn]
if !ok {
ar = ldap.NewAddRequest(c.dn)
adds[c.dn] = ar
dns = append(dns, c.dn)
}
if len(c.values) > 0 {
ar.Attribute(c.attr, c.values)
}
} else {
mr, ok := mods[c.dn]
if !ok {
mr = ldap.NewModifyRequest(c.dn)
mods[c.dn] = mr
dns = append(dns, c.dn)
}
tx.updateModifyRequest(ctx, mr, c)
}
}
return adds, mods, dns
}
func (tx *ldapTX) updateModifyRequest(ctx context.Context, mr *ldap.ModifyRequest, attr ldapAttr) {
old, ok := tx.cache[cacheKey(attr.dn, attr.attr)]
......@@ -161,6 +203,10 @@ func isEmptyModifyRequest(mr *ldap.ModifyRequest) bool {
len(mr.ReplaceAttributes) == 0)
}
func isEmptyAddRequest(ar *ldap.AddRequest) bool {
return len(ar.Attributes) == 0
}
// Unordered list comparison.
func stringListEquals(a, b []string) bool {
if len(a) != len(b) {
......
......@@ -230,3 +230,29 @@ func TestIntegration_ChangeUserPassword_SetsEncryptionKeys(t *testing.T) {
}
}
}
func TestIntegration_CreateResource(t *testing.T) {
stop, c := startService(t)
defer stop()
err := c.request("/api/resource/create", &accountserver.CreateResourcesRequest{
SSO: c.ssoTicket(testAdminUser),
Resources: []*accountserver.Resource{
&accountserver.Resource{
ID: accountserver.NewResourceID(accountserver.ResourceTypeDomain, "uno@investici.org", "example2.com"),
Name: "example2.com",
Status: accountserver.ResourceStatusActive,
Shard: "host2",
OriginalShard: "host2",
Website: &accountserver.Website{
URL: "https://example2.com",
DocumentRoot: "/home/sites/example2.com",
AcceptMail: true,
},
},
},
}, nil)
if err != nil {
t.Fatal("CreateResourcesRequest", err)
}
}
......@@ -86,6 +86,13 @@ func (s *AccountServer) handleDisableResource(tx as.TX, w http.ResponseWriter, r
})
}
func (s *AccountServer) handleCreateResources(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.CreateResourcesRequest
return handleJSON(w, r, &req, func(ctx context.Context) (interface{}, error) {
return s.service.CreateResources(ctx, tx, &req)
})
}
func (s *AccountServer) handleMoveResource(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.MoveResourceRequest
return handleJSON(w, r, &req, func(ctx context.Context) (interface{}, error) {
......@@ -143,6 +150,7 @@ func (s *AccountServer) Handler() http.Handler {
h.HandleFunc("/api/app_specific_password/delete", s.withTx(s.handleDeleteApplicationSpecificPassword))
h.HandleFunc("/api/resource/enable", s.withTx(s.handleEnableResource))
h.HandleFunc("/api/resource/disable", s.withTx(s.handleDisableResource))
h.HandleFunc("/api/resource/create", s.withTx(s.handleCreateResources))
h.HandleFunc("/api/resource/move", s.withTx(s.handleMoveResource))
h.HandleFunc("/api/recover_password", s.withTx(s.handleRecoverPassword))
return h
......
......@@ -38,9 +38,12 @@ type TX interface {
GetResource(context.Context, ResourceID) (*Resource, error)
UpdateResource(context.Context, *Resource) error
CreateResource(context.Context, *Resource) error
SetResourcePassword(context.Context, *Resource, string) error
HasAnyResource(context.Context, []FindResourceRequest) (bool, error)
GetUser(context.Context, string) (*User, error)
CreateUser(context.Context, *User) error
SetUserPassword(context.Context, *User, string) error
SetPasswordRecoveryHint(context.Context, *User, string, string) error
GetUserEncryptionKeys(context.Context, *User) ([]*UserEncryptionKey, error)
......@@ -50,8 +53,6 @@ type TX interface {
DeleteApplicationSpecificPassword(context.Context, *User, string) error
SetUserTOTPSecret(context.Context, *User, string) error
DeleteUserTOTPSecret(context.Context, *User) error
HasAnyResource(context.Context, []FindResourceRequest) (bool, error)
}
// FindResourceRequest contains parameters for searching a resource by name.
......@@ -72,6 +73,7 @@ type AccountService struct {
fieldValidators *fieldValidators
resourceValidator *resourceValidator
userValidator UserValidatorFunc
}
// NewAccountService builds a new AccountService with the specified configuration.
......@@ -99,6 +101,7 @@ func newAccountServiceWithSSO(backend Backend, config *Config, ssoValidator sso.
}
s.fieldValidators = newFieldValidators(vc)
s.resourceValidator = newResourceValidator(vc)
s.userValidator = vc.validUser()
return s, nil
}
......
......@@ -413,3 +413,13 @@ func newFieldValidators(v *validationContext) *fieldValidators {
email: v.validHostedEmail(),
}
}
type UserValidatorFunc func(context.Context, *User) error
// A custom validator for User objects.
func (v *validationContext) validUser() UserValidatorFunc {
nameValidator := v.validHostedEmail()
return func(ctx context.Context, user *User) error {
return nameValidator(ctx, user.Name)
}
}
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