Commit 0bb4fb1e authored by ale's avatar ale

Add an UpdateResource action

Since this requires validating an *existing* resource, we also split
out the availability checks in the validation code from the
syntactical validation, which is a good thing anyway.
parent f59d9c94
Pipeline #5063 failed with stages
in 3 minutes and 48 seconds
......@@ -83,7 +83,7 @@ func (r *CreateResourcesRequest) Validate(rctx *RequestContext) error {
}
// Validate the resource.
if err := rctx.resourceValidator.validateResource(rctx.Context, rsrc, tplUser); err != nil {
if err := rctx.resourceValidator.validateResource(rctx.Context, rsrc, tplUser, true); err != nil {
log.Printf("validation error while creating resource %s: %v", rsrc.String(), err)
return err
}
......@@ -176,7 +176,7 @@ func (r *CreateUserRequest) Validate(rctx *RequestContext) error {
// Validate the user *and* all resources. The request must contain at
// least one email resource with the same name as the user.
for _, rsrc := range r.User.Resources {
if err := rctx.resourceValidator.validateResource(rctx.Context, rsrc, r.User); err != nil {
if err := rctx.resourceValidator.validateResource(rctx.Context, rsrc, r.User, true); err != nil {
log.Printf("validation error while creating resource %+v: %v", rsrc, err)
return err
}
......
......@@ -3,6 +3,7 @@ package accountserver
import (
"errors"
"fmt"
"log"
)
// GetResourceRequest requests a specific resource.
......@@ -89,6 +90,76 @@ func (r *SetResourceStatusRequest) Serve(rctx *RequestContext) (interface{}, err
return nil, setResourceStatus(rctx, r.Status)
}
// 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 {
return errors.New("can't update resource ID")
}
if r.Resource.Type != rctx.Resource.Type {
return errors.New("can't update resource type")
}
if r.Resource.ParentID != rctx.Resource.ParentID {
return errors.New("can't update resource parent ID")
}
// 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.
if err := rctx.resourceValidator.validateResource(rctx.Context, r.Resource, tplUser, false); err != nil {
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)
return err
}
}
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
}
// CheckResourceAvailabilityRequest is an unauthenticated request that
// can tell if a given resource ID is available or not.
type CheckResourceAvailabilityRequest struct {
......
......@@ -239,15 +239,16 @@ func createFakeBackend() *fakeBackend {
fb.addUser(&User{
Name: testUser,
Status: UserStatusActive,
Shard: "1",
Shard: "host1",
UID: 4242,
Resources: []*Resource{
{
ID: makeResourceID(testUser, ResourceTypeEmail, testUser),
Name: testUser,
Type: ResourceTypeEmail,
Status: ResourceStatusActive,
Shard: "1",
ID: makeResourceID(testUser, ResourceTypeEmail, testUser),
Name: testUser,
Type: ResourceTypeEmail,
Status: ResourceStatusActive,
Shard: "host1",
OriginalShard: "host1",
Email: &Email{
Maildir: "example.com/testuser",
},
......@@ -634,6 +635,59 @@ func TestService_CreateResource_List(t *testing.T) {
}
}
func TestService_UpdateResource(t *testing.T) {
svc := testService("admin")
// Find the resource, modify a copy of it.
user := getUser(t, svc, testUser)
email := user.GetSingleResourceByType(ResourceTypeEmail).Copy()
email.Email.QuotaLimit = 9000
req := &AdminUpdateResourceRequest{
AdminResourceRequestBase: AdminResourceRequestBase{
ResourceRequestBase: ResourceRequestBase{
RequestBase: RequestBase{
SSO: "admin",
},
ResourceID: email.ID,
},
},
Resource: email,
}
_, err := svc.Handle(context.TODO(), req)
if err != nil {
t.Fatal("UpdateResource", err)
}
}
func TestService_UpdateResource_FailsValidation(t *testing.T) {
svc := testService("admin")
// Find the resource, modify a copy of it, but set a parameter
// that will fail validation (i.e. a bad shard).
user := getUser(t, svc, testUser)
email := user.GetSingleResourceByType(ResourceTypeEmail).Copy()
email.Shard = "invalid-shard"
req := &AdminUpdateResourceRequest{
AdminResourceRequestBase: AdminResourceRequestBase{
ResourceRequestBase: ResourceRequestBase{
RequestBase: RequestBase{
SSO: "admin",
},
ResourceID: email.ID,
},
},
Resource: email,
}
_, err := svc.Handle(context.TODO(), req)
if err == nil {
t.Fatal("UpdateResource() accepted an invalid 'shard' attribute")
}
}
func TestService_CreateUser(t *testing.T) {
svc := testService("admin")
......
......@@ -116,6 +116,7 @@ var (
{"/api/user/delete_app_specific_password", &as.DeleteApplicationSpecificPasswordRequest{}},
{"/api/resource/set_status", &as.SetResourceStatusRequest{}},
{"/api/resource/create", &as.CreateResourcesRequest{}},
{"/api/resource/update", &as.AdminUpdateResourceRequest{}},
{"/api/resource/move", &as.MoveResourceRequest{}},
{"/api/resource/reset_password", &as.ResetResourcePasswordRequest{}},
{"/api/resource/email/add_alias", &as.AddEmailAliasRequest{}},
......
......@@ -441,32 +441,26 @@ func (v *validationContext) isAvailableDatabase() ValidatorFunc {
}
func (v *validationContext) validHostedEmail() ValidatorFunc {
return allOf(
validateUsernameAndDomain(
allOf(
matchUsernameRx(),
minLength(v.config.MinUsernameLen),
maxLength(v.config.MaxUsernameLen),
notInSet(v.config.forbiddenUsernames),
),
allOf(v.isAllowedDomain(ResourceTypeEmail)),
return validateUsernameAndDomain(
allOf(
matchUsernameRx(),
minLength(v.config.MinUsernameLen),
maxLength(v.config.MaxUsernameLen),
notInSet(v.config.forbiddenUsernames),
),
v.isAvailableEmailAddr(),
allOf(v.isAllowedDomain(ResourceTypeEmail)),
)
}
func (v *validationContext) validHostedMailingList() ValidatorFunc {
return allOf(
validateUsernameAndDomain(
allOf(
matchUsernameRx(),
minLength(v.config.MinUsernameLen),
maxLength(v.config.MaxUsernameLen),
notInSet(v.config.forbiddenUsernames),
),
allOf(v.isAllowedDomain(ResourceTypeMailingList)),
return validateUsernameAndDomain(
allOf(
matchUsernameRx(),
minLength(v.config.MinUsernameLen),
maxLength(v.config.MaxUsernameLen),
notInSet(v.config.forbiddenUsernames),
),
v.isAvailableEmailAddr(),
allOf(v.isAllowedDomain(ResourceTypeMailingList)),
)
}
......@@ -491,7 +485,7 @@ func allOf(funcs ...ValidatorFunc) ValidatorFunc {
// ResourceValidatorFunc is a composite type validator that checks
// various fields in a Resource, depending on its type.
type ResourceValidatorFunc func(context.Context, *Resource, *User) error
type ResourceValidatorFunc func(context.Context, *Resource, *User, bool) error
func (v *validationContext) validateResource(_ context.Context, r *Resource, user *User) error {
// Validate the status enum.
......@@ -538,8 +532,9 @@ func (v *validationContext) validateShardedResource(ctx context.Context, r *Reso
func (v *validationContext) validEmailResource() ResourceValidatorFunc {
emailValidator := v.validHostedEmail()
newEmailValidator := allOf(emailValidator, v.isAvailableEmailAddr())
return func(ctx context.Context, r *Resource, user *User) error {
return func(ctx context.Context, r *Resource, user *User, isNew bool) error {
if err := v.validateShardedResource(ctx, r, user); err != nil {
return err
}
......@@ -552,9 +547,17 @@ func (v *validationContext) validEmailResource() ResourceValidatorFunc {
if r.Email == nil {
return errors.New("resource has no email metadata")
}
if err := emailValidator(ctx, r.Name); err != nil {
var err error
if isNew {
err = newEmailValidator(ctx, r.Name)
} else {
err = emailValidator(ctx, r.Name)
}
if err != nil {
return err
}
if r.Email.Maildir == "" {
return errors.New("empty maildir")
}
......@@ -564,17 +567,26 @@ func (v *validationContext) validEmailResource() ResourceValidatorFunc {
func (v *validationContext) validListResource() ResourceValidatorFunc {
listValidator := v.validHostedMailingList()
newListValidator := allOf(listValidator, v.isAvailableEmailAddr())
return func(ctx context.Context, r *Resource, user *User) error {
return func(ctx context.Context, r *Resource, user *User, isNew bool) error {
if err := v.validateShardedResource(ctx, r, user); err != nil {
return err
}
if r.List == nil {
return errors.New("resource has no list metadata")
}
if err := listValidator(ctx, r.Name); err != nil {
var err error
if isNew {
err = newListValidator(ctx, r.Name)
} else {
err = listValidator(ctx, r.Name)
}
if err != nil {
return err
}
if len(r.List.Admins) < 1 {
return errors.New("can't create a list without admins")
}
......@@ -584,17 +596,26 @@ func (v *validationContext) validListResource() ResourceValidatorFunc {
func (v *validationContext) validNewsletterResource() ResourceValidatorFunc {
listValidator := v.validHostedMailingList()
newListValidator := allOf(listValidator, v.isAvailableEmailAddr())
return func(ctx context.Context, r *Resource, user *User) error {
return func(ctx context.Context, r *Resource, user *User, isNew bool) error {
if err := v.validateShardedResource(ctx, r, user); err != nil {
return err
}
if r.Newsletter == nil {
return errors.New("resource has no newsletter metadata")
}
if err := listValidator(ctx, r.Name); err != nil {
var err error
if isNew {
err = newListValidator(ctx, r.Name)
} else {
err = listValidator(ctx, r.Name)
}
if err != nil {
return err
}
if len(r.Newsletter.Admins) < 1 {
return errors.New("can't create a newsletter without admins")
}
......@@ -650,10 +671,13 @@ func (v *validationContext) validDomainResource() ResourceValidatorFunc {
domainValidator := allOf(
minLength(6),
validDomainName,
)
newDomainValidator := allOf(
domainValidator,
v.isAvailableDomain(),
)
return func(ctx context.Context, r *Resource, user *User) error {
return func(ctx context.Context, r *Resource, user *User, isNew bool) error {
if err := v.validateShardedResource(ctx, r, user); err != nil {
return err
}
......@@ -666,9 +690,17 @@ func (v *validationContext) validDomainResource() ResourceValidatorFunc {
if r.Website == nil {
return errors.New("resource has no website metadata")
}
if err := domainValidator(ctx, r.Name); err != nil {
var err error
if isNew {
err = newDomainValidator(ctx, r.Name)
} else {
err = domainValidator(ctx, r.Name)
}
if err != nil {
return err
}
if r.Website.ParentDomain != "" {
return errors.New("non-empty parent_domain on domain resource")
}
......@@ -681,11 +713,14 @@ func (v *validationContext) validWebsiteResource() ResourceValidatorFunc {
nameValidator := allOf(
minLength(6),
matchSitenameRx(),
)
newNameValidator := allOf(
nameValidator,
v.isAvailableWebsite(),
)
parentValidator := v.isAllowedDomain(ResourceTypeWebsite)
return func(ctx context.Context, r *Resource, user *User) error {
return func(ctx context.Context, r *Resource, user *User, isNew bool) error {
if err := v.validateShardedResource(ctx, r, user); err != nil {
return err
}
......@@ -698,7 +733,14 @@ func (v *validationContext) validWebsiteResource() ResourceValidatorFunc {
if r.Website == nil {
return errors.New("resource has no website metadata")
}
if err := nameValidator(ctx, r.Name); err != nil {
var err error
if isNew {
err = newNameValidator(ctx, r.Name)
} else {
err = nameValidator(ctx, r.Name)
}
if err != nil {
return err
}
if err := parentValidator(ctx, r.Website.ParentDomain); err != nil {
......@@ -710,12 +752,15 @@ func (v *validationContext) validWebsiteResource() ResourceValidatorFunc {
}
func (v *validationContext) validDAVResource() ResourceValidatorFunc {
validator := allOf(
davValidator := allOf(
minLength(4),
matchIdentifierRx(),
)
newDAVValidator := allOf(
davValidator,
v.isAvailableDAV(),
)
return func(ctx context.Context, r *Resource, user *User) error {
return func(ctx context.Context, r *Resource, user *User, isNew bool) error {
if err := v.validateShardedResource(ctx, r, user); err != nil {
return err
}
......@@ -725,9 +770,16 @@ func (v *validationContext) validDAVResource() ResourceValidatorFunc {
return errors.New("resource should not have parent")
}
if err := validator(ctx, r.Name); err != nil {
var err error
if isNew {
err = newDAVValidator(ctx, r.Name)
} else {
err = davValidator(ctx, r.Name)
}
if err != nil {
return err
}
if r.DAV == nil {
return errors.New("resource has no dav metadata")
}
......@@ -740,12 +792,15 @@ func (v *validationContext) validDAVResource() ResourceValidatorFunc {
}
func (v *validationContext) validDatabaseResource() ResourceValidatorFunc {
validator := allOf(
dbValidator := allOf(
minLength(4),
matchIdentifierRx(),
)
newDBValidator := allOf(
dbValidator,
v.isAvailableDatabase(),
)
return func(ctx context.Context, r *Resource, user *User) error {
return func(ctx context.Context, r *Resource, user *User, isNew bool) error {
if err := v.validateShardedResource(ctx, r, user); err != nil {
return err
}
......@@ -762,9 +817,16 @@ func (v *validationContext) validDatabaseResource() ResourceValidatorFunc {
// return errors.New("database parent is not a website resource")
// }
if err := validator(ctx, r.Name); err != nil {
var err error
if isNew {
err = newDBValidator(ctx, r.Name)
} else {
err = dbValidator(ctx, r.Name)
}
if err != nil {
return err
}
if r.Database == nil {
return errors.New("resource has no database metadata")
}
......@@ -791,7 +853,7 @@ func newResourceValidator(v *validationContext) *resourceValidator {
}
}
func (v *resourceValidator) validateResource(ctx context.Context, r *Resource, user *User) error {
func (v *resourceValidator) validateResource(ctx context.Context, r *Resource, user *User, isNew bool) error {
// Obvious basic sanity checks on the Resource parameters.
if r.Name == "" {
return errors.New("resource name unset")
......@@ -805,7 +867,7 @@ func (v *resourceValidator) validateResource(ctx context.Context, r *Resource, u
return fmt.Errorf("unknown resource type '%s'", r.Type)
}
return rv(ctx, r, user)
return rv(ctx, r, user, isNew)
}
// Common validators for specific field types.
......
......@@ -131,7 +131,8 @@ func TestValidator_HostedEmail(t *testing.T) {
{Type: ResourceTypeEmail, Name: "existing@example.com"},
})
vc.domains = newFakeDomainBackend("example.com")
runValidationTest(t, vc.validHostedEmail(), td)
vf := allOf(vc.validHostedEmail(), vc.isAvailableEmailAddr())
runValidationTest(t, vf, td)
}
func TestValidator_HostedMailingList(t *testing.T) {
......@@ -149,5 +150,6 @@ func TestValidator_HostedMailingList(t *testing.T) {
{Type: ResourceTypeMailingList, Name: "existing@domain2.com"},
})
vc.domains = newFakeDomainBackend("domain1.com", "domain2.com")
runValidationTest(t, vc.validHostedMailingList(), td)
vf := allOf(vc.validHostedMailingList(), vc.isAvailableEmailAddr())
runValidationTest(t, vf, td)
}
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