actions_resource.go 11.1 KB
Newer Older
1
2
3
4
package accountserver

import (
	"fmt"
ale's avatar
ale committed
5
	"log"
6
7
)

ale's avatar
ale committed
8
9
10
11
12
13
14
15
16
17
18
// GetResourceRequest requests a specific resource.
type GetResourceRequest struct {
	AdminResourceRequestBase
}

// GetResourceResponse is the response type for GetResourceRequest.
type GetResourceResponse struct {
	Resource *Resource `json:"resource"`
	Owner    string    `json:"owner"`
}

ale's avatar
ale committed
19
// Serve the request.
ale's avatar
ale committed
20
21
22
23
24
25
26
27
28
29
func (r *GetResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
	resp := GetResourceResponse{
		Resource: rctx.Resource,
	}
	if rctx.User != nil {
		resp.Owner = rctx.User.Name
	}
	return &resp, nil
}

ale's avatar
ale committed
30
31
32
33
34
// SearchResourceRequest searches for resources matching a pattern.
type SearchResourceRequest struct {
	AdminRequestBase

	Pattern string `json:"pattern"`
ale's avatar
ale committed
35
	Limit   int    `json:"limit"`
ale's avatar
ale committed
36
37
38
39
40
}

// Validate the request.
func (r *SearchResourceRequest) Validate(rctx *RequestContext) error {
	if r.Pattern == "" {
ale's avatar
ale committed
41
		return newValidationError(nil, "pattern", "empty pattern")
ale's avatar
ale committed
42
43
44
45
46
47
48
49
50
51
52
	}
	return nil
}

// SearchResourceResponse is the response type for SearchResourceRequest.
type SearchResourceResponse struct {
	Results []*RawResource `json:"results"`
}

// Serve the request.
func (r *SearchResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
ale's avatar
ale committed
53
	results, err := rctx.TX.SearchResource(rctx.Context, r.Pattern, r.Limit)
ale's avatar
ale committed
54
55
56
57
58
59
	if err != nil {
		return nil, err
	}
	return &SearchResourceResponse{Results: results}, nil
}

60
61
62
63
64
65
66
67
// setResourceStatus sets the status of a single resource (shared
// logic between enable / disable resource methods).
func setResourceStatus(rctx *RequestContext, status string) error {
	rsrc := rctx.Resource
	rsrc.Status = status
	if err := rctx.TX.UpdateResource(rctx.Context, rsrc); err != nil {
		return err
	}
ale's avatar
ale committed
68
	rctx.audit.Log(rctx, rsrc, fmt.Sprintf("status set to %s", status))
69
70
71
	return nil
}

72
73
74
75
// SetResourceStatusRequest modifies the status of a resource
// belonging to the user (admin-only).
type SetResourceStatusRequest struct {
	AdminResourceRequestBase
76

77
	Status string `json:"status"`
78
79
}

80
81
82
// Validate the request.
func (r *SetResourceStatusRequest) Validate(rctx *RequestContext) error {
	if !isValidStatusByResourceType(rctx.Resource.Type, r.Status) {
ale's avatar
ale committed
83
		return newValidationError(nil, "status", "invalid or unknown status")
84
85
	}
	return nil
86
87
88
}

// Serve the request.
89
90
func (r *SetResourceStatusRequest) Serve(rctx *RequestContext) (interface{}, error) {
	return nil, setResourceStatus(rctx, r.Status)
91
92
}

ale's avatar
ale committed
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// AdminUpdateResourceRequest updates arbitrary fields on a Resource
// (privileged management operation).
type AdminUpdateResourceRequest struct {
	AdminResourceRequestBase

	Resource *Resource `json:"resource"`
}

func replaceResource(rsrcs []*Resource, r *Resource) []*Resource {
	var out []*Resource
	for _, rsrc := range rsrcs {
		if rsrc.ID != r.ID {
			out = append(out, rsrc)
		}
	}
	out = append(out, r)
	return out
}

func (r *AdminUpdateResourceRequest) Validate(rctx *RequestContext) error {
	// Verify some invariants (resources can't be renamed, type
	// can't be changed, etc). Prevent updates that would require
	// re-templating, since we don't really support derived fields.
	if r.Resource.ID != rctx.Resource.ID {
ale's avatar
ale committed
117
		return newValidationError(nil, "id", "can't update resource ID")
ale's avatar
ale committed
118
119
	}
	if r.Resource.Type != rctx.Resource.Type {
ale's avatar
ale committed
120
		return newValidationError(nil, "type", "can't update resource type")
ale's avatar
ale committed
121
122
	}
	if r.Resource.ParentID != rctx.Resource.ParentID {
ale's avatar
ale committed
123
		return newValidationError(nil, "parent_id", "can't update resource parent ID")
ale's avatar
ale committed
124
125
126
127
128
129
130
131
132
133
134
135
136
137
	}

	// The logic here mirrors somewhat that in
	// CreateResourcesRequest.Validate, where we assemble a fake
	// User with the "new" resources, and use it for user-level
	// validation.
	var tplUser *User
	if rctx.User != nil {
		userCopy := rctx.User.User
		userCopy.Resources = replaceResource(userCopy.Resources, r.Resource)
		tplUser = &userCopy
	}

	// Validate the resource.
138
	if err := rctx.resourceValidator.validateResource(rctx, r.Resource, tplUser, false); err != nil {
ale's avatar
ale committed
139
140
141
142
143
144
145
146
147
		return err
	}

	// Validate the user.
	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 updating resources: %v", err)
ale's avatar
ale committed
148
			return newValidationError(nil, "global", err.Error())
ale's avatar
ale committed
149
150
151
152
153
154
155
156
157
158
159
160
161
162
		}
	}

	return nil
}

func (r *AdminUpdateResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
	if err := rctx.TX.UpdateResource(rctx.Context, r.Resource); err != nil {
		return nil, err
	}
	rctx.audit.Log(rctx, r.Resource, "resource updated by admin")
	return nil, nil
}

163
164
165
// CheckResourceAvailabilityRequest is an unauthenticated request that
// can tell if a given resource ID is available or not.
type CheckResourceAvailabilityRequest struct {
166
167
	Type string `json:"type"`
	Name string `json:"name"`
168
169
170
171
172
}

// CheckResourceAvailabilityResponse is the response type for
// CheckResourceAvailabilityRequest.
type CheckResourceAvailabilityResponse struct {
173
	Available bool `json:"available"`
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
}

// Authorize the request - this one requires no authentication.
func (r *CheckResourceAvailabilityRequest) Authorize(rctx *RequestContext) error {
	return nil
}

// PopulateContext is a no-op for this type.
func (r *CheckResourceAvailabilityRequest) PopulateContext(rctx *RequestContext) error {
	return nil
}

// Validate the request.
func (r *CheckResourceAvailabilityRequest) Validate(rctx *RequestContext) error {
	if r.Name == "" {
ale's avatar
ale committed
189
		return newValidationError(nil, "name", "name is unset")
190
191
192
193
194
195
196
197
	}
	return nil
}

// Serve the request.
func (r *CheckResourceAvailabilityRequest) Serve(rctx *RequestContext) (interface{}, error) {
	var check ValidatorFunc
	switch r.Type {
198
	case ResourceTypeEmail, ResourceTypeMailingList, ResourceTypeNewsletter:
199
200
201
202
203
204
205
206
207
208
		check = rctx.validationCtx.isAvailableEmailAddr()
	case ResourceTypeDomain:
		check = rctx.validationCtx.isAvailableDomain()
	case ResourceTypeWebsite:
		check = rctx.validationCtx.isAvailableWebsite()
	case ResourceTypeDAV:
		check = rctx.validationCtx.isAvailableDAV()
	case ResourceTypeDatabase:
		check = rctx.validationCtx.isAvailableDatabase()
	default:
209
		return nil, newValidationError(nil, "type", "unknown resource type")
210
211
212
	}

	var resp CheckResourceAvailabilityResponse
213
	if err := check(rctx, r.Name); err == nil {
214
215
216
217
218
		resp.Available = true
	}
	return &resp, nil
}

219
220
221
222
223
224
225
226
227
228
229
230
231
232
// ResetResourcePasswordRequest will reset the password associated
// with a resource (if the resource type supports it). It will
// generate a random password and return it to the caller.
type ResetResourcePasswordRequest struct {
	ResourceRequestBase
}

// ResetResourcePasswordResponse is the response type for
// ResetResourcePasswordRequest.
type ResetResourcePasswordResponse struct {
	Password string `json:"password"`
}

// Validate the request.
ale's avatar
ale committed
233
234
func (r *ResetResourcePasswordRequest) Validate(rctx *RequestContext) error {
	switch rctx.Resource.Type {
235
236
	case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList:
	case ResourceTypeEmail:
ale's avatar
ale committed
237
		return newValidationError(nil, "type", "can't reset email passwords with this API")
238
	default:
ale's avatar
ale committed
239
		return newValidationError(nil, "type", "can't reset password on this resource type")
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
	}
	return nil
}

// Serve the request.
func (r *ResetResourcePasswordRequest) Serve(rctx *RequestContext) (interface{}, error) {
	// TODO: this needs a resource-type switch, because in some
	// cases we may want to call out to other backends in order to
	// reset credentials for certain resources that have their own
	// secondary authentication databases (lists, mysql).
	password, err := doResetResourcePassword(rctx.Context, rctx.TX, rctx.Resource)
	if err != nil {
		return nil, err
	}
	return &ResetResourcePasswordResponse{
		Password: password,
	}, nil
}

// MoveResourceRequest is an administrative operation to move resources
// between shards. Resources that are part of a group are moved all at
// once regardless of which individual ResourceID is provided as long
// as it belongs to the group.
type MoveResourceRequest struct {
	AdminResourceRequestBase
	Shard string `json:"shard"`
}

// Validate the request.
func (r *MoveResourceRequest) Validate(rctx *RequestContext) error {
	// TODO: check shard
	return nil
}

// MoveResourceResponse is the response type for MoveResourceRequest.
type MoveResourceResponse struct {
	MovedIDs []string `json:"moved_ids"`
}

// Serve the request.
func (r *MoveResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
	resources := []*Resource{rctx.Resource}
ale's avatar
ale committed
282

283
284
285
286
287
288
	// If we have an associated user, collect all related
	// resources, as they should all be moved at once.
	if rctx.User != nil && rctx.Resource.Group != "" {
		resources = append(resources, rctx.User.GetResourcesByGroup(rctx.Resource.Group)...)
	}

ale's avatar
ale committed
289
290
291
	// We need to enforce consistency between email resources and
	// the user shard, so that temporary data can be colocated
	// with email storage.
ale's avatar
ale committed
292
	if rctx.Resource.Type == ResourceTypeEmail && rctx.User.Shard != r.Shard {
ale's avatar
ale committed
293
294
295
296
297
298
		rctx.User.Shard = r.Shard
		if err := rctx.TX.UpdateUser(rctx.Context, &rctx.User.User); err != nil {
			return nil, err
		}
	}

299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
	var resp MoveResourceResponse
	for _, rsrc := range resources {
		rsrc.Shard = r.Shard
		if err := rctx.TX.UpdateResource(rctx.Context, rsrc); err != nil {
			return nil, err
		}
		resp.MovedIDs = append(resp.MovedIDs, rsrc.ID.String())
	}
	return &resp, nil
}

// AddEmailAliasRequest adds an alias (additional address) to an email resource.
type AddEmailAliasRequest struct {
	ResourceRequestBase
	Addr string `json:"addr"`
}

// Validate the request.
func (r *AddEmailAliasRequest) Validate(rctx *RequestContext) error {
ale's avatar
ale committed
318
	if rctx.Resource.Type != ResourceTypeEmail {
ale's avatar
ale committed
319
		return newValidationError(nil, "type", "this operation only works on email resources")
320
321
322
323
	}

	// Allow at most 5 aliases.
	if len(rctx.Resource.Email.Aliases) >= maxEmailAliases {
ale's avatar
ale committed
324
		return newValidationError(nil, "addr", "too many aliases")
325
326
	}

327
	if err := rctx.fieldValidators.newEmail(rctx, r.Addr); err != nil {
328
329
330
331
332
333
334
335
336
337
338
339
340
341
		return newValidationError(nil, "addr", err.Error())
	}
	return nil
}

const maxEmailAliases = 5

// Serve the request.
func (r *AddEmailAliasRequest) Serve(rctx *RequestContext) (interface{}, error) {
	rctx.Resource.Email.Aliases = append(rctx.Resource.Email.Aliases, r.Addr)
	if err := rctx.TX.UpdateResource(rctx.Context, rctx.Resource); err != nil {
		return nil, err
	}

ale's avatar
ale committed
342
	rctx.audit.Log(rctx, rctx.Resource, fmt.Sprintf("added alias %s", r.Addr))
343
344
345
346
347
348
349
350
351
	return nil, nil
}

// DeleteEmailAliasRequest removes an alias from an email resource.
type DeleteEmailAliasRequest struct {
	ResourceRequestBase
	Addr string `json:"addr"`
}

352
353
// Validate the request.
func (r *DeleteEmailAliasRequest) Validate(rctx *RequestContext) error {
ale's avatar
ale committed
354
	if rctx.Resource.Type != ResourceTypeEmail {
ale's avatar
ale committed
355
		return newValidationError(nil, "type", "this operation only works on email resources")
356
	}
357
358
	return nil
}
359

360
361
// Serve the request.
func (r *DeleteEmailAliasRequest) Serve(rctx *RequestContext) (interface{}, error) {
362
363
364
365
366
367
368
369
370
371
372
	var aliases []string
	for _, a := range rctx.Resource.Email.Aliases {
		if a != r.Addr {
			aliases = append(aliases, a)
		}
	}
	rctx.Resource.Email.Aliases = aliases
	if err := rctx.TX.UpdateResource(rctx.Context, rctx.Resource); err != nil {
		return nil, err
	}

ale's avatar
ale committed
373
	rctx.audit.Log(rctx, rctx.Resource, fmt.Sprintf("removed alias %s", r.Addr))
374
375
	return nil, nil
}