actions_create.go 7.23 KB
Newer Older
1 2 3 4
package accountserver

import (
	"context"
ale's avatar
ale committed
5
	"fmt"
6
	"log"
ale's avatar
ale committed
7
	"strings"
8 9 10 11 12 13

	"git.autistici.org/ai3/go-common/pwhash"
)

// CreateResourcesRequest lets administrators create one or more resources.
type CreateResourcesRequest struct {
14 15 16 17
	AdminRequestBase

	// Username the resources will belong to (optional).
	Username string `json:"username"`
ale's avatar
ale committed
18 19 20

	// Resources to create. All must either be global resources
	// (no user ownership), or belong to the same user.
21 22 23
	Resources []*Resource `json:"resources"`
}

24 25 26 27 28 29 30 31 32 33 34 35 36
// PopulateContext extracts information from the request and stores it
// into the RequestContext.
func (r *CreateResourcesRequest) PopulateContext(rctx *RequestContext) error {
	if r.Username != "" {
		user, err := getUserOrDie(rctx.Context, rctx.TX, r.Username)
		if err != nil {
			return err
		}
		rctx.User = user
	}
	return r.AdminRequestBase.PopulateContext(rctx)
}

37 38
// CreateResourcesResponse is the response type for CreateResourcesRequest.
type CreateResourcesResponse struct {
ale's avatar
ale committed
39
	// Resources that were created.
40 41 42
	Resources []*Resource `json:"resources"`
}

ale's avatar
ale committed
43 44 45 46 47 48 49 50 51 52
// Merge two resource lists by ID, return a new list. If a resource is
// duplicated (detected by type/name matching), return an error.
func mergeResources(a, b []*Resource) ([]*Resource, error) {
	tmp := make(map[string]struct{})
	var out []*Resource
	for _, l := range [][]*Resource{a, b} {
		for _, r := range l {
			key := r.String()
			if _, seen := tmp[key]; seen {
				return nil, fmt.Errorf("resource %s already exists", key)
53
			}
ale's avatar
ale committed
54 55
			tmp[key] = struct{}{}
			out = append(out, r)
56 57
		}
	}
ale's avatar
ale committed
58
	return out, nil
59 60 61 62
}

// Validate the request.
func (r *CreateResourcesRequest) Validate(rctx *RequestContext) error {
63 64 65 66 67 68 69 70 71 72 73 74 75 76
	var tplUser *User

	if rctx.User != nil {
		// To provide resource validators with a view of what the User should be
		// with the new resources, we merge the ones from the database with the
		// ones from the request. This is also a good time to check for
		// uniqueness (even though it would fail at commit time anyway).
		merged, err := mergeResources(rctx.User.Resources, r.Resources)
		if err != nil {
			return err
		}
		userCopy := rctx.User.User
		userCopy.Resources = merged
		tplUser = &userCopy
77 78 79
	}

	for _, rsrc := range r.Resources {
80 81 82 83
		// Apply resource templates.
		if err := rctx.resourceTemplates.applyTemplate(rctx.Context, rsrc, tplUser); err != nil {
			return err
		}
84 85

		// Validate the resource.
86
		if err := rctx.resourceValidator.validateResource(rctx.Context, rsrc, tplUser); err != nil {
ale's avatar
ale committed
87
			log.Printf("validation error while creating resource %s: %v", rsrc.String(), err)
88 89 90
			return err
		}
	}
91 92 93 94 95 96 97 98 99 100

	if tplUser != nil {
		// If the resource has an owner, validate it (checks that the new
		// resources do not violate user invariants).
		if err := checkUserInvariants(tplUser); err != nil {
			log.Printf("validation error while creating resources: %v", err)
			return err
		}
	}

101 102 103 104 105
	return nil
}

// Serve the request.
func (r *CreateResourcesRequest) Serve(rctx *RequestContext) (interface{}, error) {
106 107 108 109 110
	var user *User
	if rctx.User != nil {
		user = &rctx.User.User
	}
	rsrcs, err := rctx.TX.CreateResources(rctx.Context, user, r.Resources)
ale's avatar
ale committed
111 112
	if err != nil {
		return nil, err
113
	}
ale's avatar
ale committed
114 115
	for _, rsrc := range rsrcs {
		rctx.audit.Log(rctx, rsrc, "resource created")
116
	}
ale's avatar
ale committed
117 118 119
	return &CreateResourcesResponse{
		Resources: rsrcs,
	}, nil
120 121 122 123 124 125
}

// CreateUserRequest lets administrators create a new user along with the
// associated resources.
type CreateUserRequest struct {
	AdminRequestBase
ale's avatar
ale committed
126 127

	// User to create, along with associated resources.
128 129 130 131 132
	User *User `json:"user"`
}

// applyTemplate fills in default values for the resources in the request.
func (r *CreateUserRequest) applyTemplate(rctx *RequestContext) error {
ale's avatar
ale committed
133 134 135
	// Usernames must be lowercase.
	r.User.Name = strings.ToLower(r.User.Name)

136 137
	// Some fields should be always unset because there are
	// specific methods to modify them.
ale's avatar
ale committed
138
	r.User.Status = UserStatusActive
139 140 141
	r.User.Has2FA = false
	r.User.HasOTP = false
	r.User.HasEncryptionKeys = true // set to true so that resetPassword will create keys.
142
	r.User.AccountRecoveryHint = ""
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
	r.User.AppSpecificPasswords = nil
	r.User.U2FRegistrations = nil
	if r.User.Lang == "" {
		r.User.Lang = "en"
	}

	// Allocate a new user ID.
	uid, err := rctx.TX.NextUID(rctx.Context)
	if err != nil {
		return err
	}
	r.User.UID = uid

	// Apply templates to all resources in the request.
	for _, rsrc := range r.User.Resources {
158 159 160 161 162 163 164
		if err := rctx.resourceTemplates.applyTemplate(rctx.Context, rsrc, r.User); err != nil {
			return err
		}
		// Set the user shard to match the email resource shard.
		if rsrc.Type == ResourceTypeEmail {
			r.User.Shard = rsrc.Shard
		}
165
	}
ale's avatar
ale committed
166

167 168 169 170 171 172 173 174 175
	return nil
}

// Validate the request.
func (r *CreateUserRequest) Validate(rctx *RequestContext) error {
	if err := r.applyTemplate(rctx); err != nil {
		return err
	}

ale's avatar
ale committed
176 177
	// Validate the user *and* all resources.  The request must contain at
	// least one email resource with the same name as the user.
178 179 180 181 182
	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
		}
ale's avatar
ale committed
183
	}
184 185 186
	if err := rctx.userValidator(rctx.Context, r.User); err != nil {
		log.Printf("validation error while creating user %+v: %v", r.User, err)
		return err
ale's avatar
ale committed
187 188
	}

189 190 191 192 193
	return nil
}

// CreateUserResponse is the response type for CreateUserRequest.
type CreateUserResponse struct {
ale's avatar
ale committed
194
	User     *User  `json:"user"`
195 196 197
	Password string `json:"password"`
}

ale's avatar
ale committed
198 199 200 201 202 203 204
// Sanitize the response.
func (r *CreateUserResponse) Sanitize() {
	if r.Password != "" {
		r.Password = sanitizedValue
	}
}

205 206 207 208 209
// Serve the request
func (r *CreateUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
	var resp CreateUserResponse

	// Create the user first, along with all the resources.
ale's avatar
ale committed
210 211
	user, err := rctx.TX.CreateUser(rctx.Context, r.User)
	if err != nil {
212 213
		return nil, err
	}
ale's avatar
ale committed
214
	resp.User = user
215 216 217 218 219 220

	// Now set a password for the user and return it, and
	// set random passwords for all the resources
	// (currently, we don't care about those, the user
	// will reset them later). However, we could return
	// them in the response as well, if necessary.
ale's avatar
ale committed
221
	u := &RawUser{User: *user}
222 223 224 225 226 227
	newPassword := randomPassword()
	if err := u.resetPassword(rctx.Context, rctx.TX, newPassword); err != nil {
		return nil, err
	}
	resp.Password = newPassword

ale's avatar
ale committed
228 229
	// Fake a RawUser in the RequestContext just for the purpose
	// of audit logging.
ale's avatar
ale committed
230 231
	rctx.User = u
	rctx.audit.Log(rctx, nil, "user created")
ale's avatar
ale committed
232

233
	for _, rsrc := range r.User.Resources {
ale's avatar
ale committed
234
		rctx.audit.Log(rctx, rsrc, "resource created")
235 236 237
		if resourceHasPassword(rsrc) {
			if _, err := doResetResourcePassword(rctx.Context, rctx.TX, rsrc); err != nil {
				// Just log, don't fail.
ale's avatar
ale committed
238
				log.Printf("can't set random password for resource %s: %v", rsrc.String(), err)
239 240 241 242 243 244 245
			}
		}
	}

	return &resp, nil
}

ale's avatar
ale committed
246 247 248 249 250 251 252 253 254
func resourceHasPassword(r *Resource) bool {
	switch r.Type {
	case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList:
		return true
	default:
		return false
	}
}

255 256 257 258 259 260 261 262 263 264
func doResetResourcePassword(ctx context.Context, tx TX, rsrc *Resource) (string, error) {
	newPassword := randomPassword()
	encPassword := pwhash.Encrypt(newPassword)

	// TODO: this needs a resource type-switch.
	if err := tx.SetResourcePassword(ctx, rsrc, encPassword); err != nil {
		return "", err
	}
	return newPassword, nil
}