package accountserver import ( "errors" "fmt" "log" ) // 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"` } // Serve the request. 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 } // SearchResourceRequest searches for resources matching a pattern. type SearchResourceRequest struct { AdminRequestBase Pattern string `json:"pattern"` Limit int `json:"limit"` } // Validate the request. func (r *SearchResourceRequest) Validate(rctx *RequestContext) error { if r.Pattern == "" { return newValidationError(nil, "pattern", "empty pattern") } 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) { results, err := rctx.TX.SearchResource(rctx.Context, r.Pattern, r.Limit) if err != nil { return nil, err } return &SearchResourceResponse{Results: results}, nil } // 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 } rctx.audit.Log(rctx, rsrc, fmt.Sprintf("status set to %s", status)) return nil } // SetResourceStatusRequest modifies the status of a resource // belonging to the user (admin-only). type SetResourceStatusRequest struct { AdminResourceRequestBase Status string `json:"status"` } // Validate the request. func (r *SetResourceStatusRequest) Validate(rctx *RequestContext) error { if !isValidStatusByResourceType(rctx.Resource.Type, r.Status) { return newValidationError(nil, "status", "invalid or unknown status") } return nil } // Serve the request. func (r *SetResourceStatusRequest) Serve(rctx *RequestContext) (interface{}, error) { 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 newValidationError(nil, "id", "can't update resource ID") } if r.Resource.Type != rctx.Resource.Type { return newValidationError(nil, "type", "can't update resource type") } if r.Resource.ParentID != rctx.Resource.ParentID { return newValidationError(nil, "parent_id", "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, 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 newValidationError(nil, "global", err.Error()) } } 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 { Type string `json:"type"` Name string `json:"name"` } // CheckResourceAvailabilityResponse is the response type for // CheckResourceAvailabilityRequest. type CheckResourceAvailabilityResponse struct { Available bool `json:"available"` } // 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 == "" { return newValidationError(nil, "name", "name is unset") } return nil } // Serve the request. func (r *CheckResourceAvailabilityRequest) Serve(rctx *RequestContext) (interface{}, error) { var check ValidatorFunc switch r.Type { case ResourceTypeEmail, ResourceTypeMailingList: 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: return nil, errors.New("unknown resource type") } var resp CheckResourceAvailabilityResponse if err := check(rctx, r.Name); err == nil { resp.Available = true } return &resp, nil } // 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. func (r *ResetResourcePasswordRequest) Validate(rctx *RequestContext) error { switch rctx.Resource.Type { case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList: case ResourceTypeEmail: return newValidationError(nil, "type", "can't reset email passwords with this API") default: return newValidationError(nil, "type", "can't reset password on this resource type") } 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} // 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)...) } // 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.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 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 { if rctx.Resource.Type != ResourceTypeEmail { return newValidationError(nil, "type", "this operation only works on email resources") } // Allow at most 5 aliases. if len(rctx.Resource.Email.Aliases) >= maxEmailAliases { return newValidationError(nil, "addr", "too many aliases") } if err := rctx.fieldValidators.newEmail(rctx, r.Addr); err != nil { 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 } rctx.audit.Log(rctx, rctx.Resource, fmt.Sprintf("added alias %s", r.Addr)) return nil, nil } // DeleteEmailAliasRequest removes an alias from an email resource. type DeleteEmailAliasRequest struct { ResourceRequestBase Addr string `json:"addr"` } // Validate the request. func (r *DeleteEmailAliasRequest) Validate(rctx *RequestContext) error { if rctx.Resource.Type != ResourceTypeEmail { return newValidationError(nil, "type", "this operation only works on email resources") } return nil } // Serve the request. func (r *DeleteEmailAliasRequest) Serve(rctx *RequestContext) (interface{}, error) { 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 } rctx.audit.Log(rctx, rctx.Resource, fmt.Sprintf("removed alias %s", r.Addr)) return nil, nil }