Commit b790a71a authored by ale's avatar ale

Add missing validations and an availability API endpoint

Previously some resource types were not validating requests for
uniqueness (the creation would have failed later, at database commit
time).

The availability API allows unauthenticated callers (i.e. everyone) to
query for existence of a specific resource, or resources that would
conflict with it in the global namespace. It's basically a public
validation check, meant so that applications that let users create
accounts can provide early feedback on availability.
parent ef7d8d83
Pipeline #4182 passed with stages
in 4 minutes and 47 seconds
......@@ -528,6 +528,20 @@ Request parameters:
* `sso` - SSO ticket
* `resources` - list of resource objects to create
### `/api/resource/check_availability`
Verify if a resource (identified here by type and name) exists or not,
with the purpose of providing early feedback to applications creating
new services.
Request parameters:
* `type` - resource type
* `name` - resource name
Response attributes:
* `available` - bool
## Type-specific resource endpoints
......
......@@ -89,6 +89,62 @@ func (r *SetResourceStatusRequest) Serve(rctx *RequestContext) (interface{}, err
return nil, setResourceStatus(rctx, r.Status)
}
// CheckResourceAvailabilityRequest is an unauthenticated request that
// can tell if a given resource ID is available or not.
type CheckResourceAvailabilityRequest struct {
Type string
Name string
}
// CheckResourceAvailabilityResponse is the response type for
// CheckResourceAvailabilityRequest.
type CheckResourceAvailabilityResponse struct {
Available bool
}
// 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 errors.New("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.Context, 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.
......
......@@ -61,6 +61,7 @@ func New(service *as.AccountService, backend as.Backend) *APIServer {
s.Register("/api/user/delete_app_specific_password", &as.DeleteApplicationSpecificPasswordRequest{})
s.Register("/api/resource/get", &as.GetResourceRequest{})
s.Register("/api/resource/search", &as.SearchResourceRequest{})
s.Register("/api/resource/check_availability", &as.CheckResourceAvailabilityRequest{})
s.Register("/api/resource/set_status", &as.SetResourceStatusRequest{})
s.Register("/api/resource/create", &as.CreateResourcesRequest{})
s.Register("/api/resource/move", &as.MoveResourceRequest{})
......
......@@ -93,6 +93,7 @@ type AccountService struct {
audit auditLogger
umdb umdbc.Client
validationCtx *validationContext
fieldValidators *fieldValidators
resourceValidator *resourceValidator
userValidator UserValidatorFunc
......@@ -135,6 +136,7 @@ func newAccountServiceWithSSO(backend Backend, config *Config, ssoValidator sso.
if err != nil {
return nil, err
}
s.validationCtx = vc
s.fieldValidators = newFieldValidators(vc)
s.resourceValidator = newResourceValidator(vc)
s.userValidator = vc.validUser()
......
......@@ -222,6 +222,7 @@ func matchRegexp(rx *regexp.Regexp, errmsg string) ValidatorFunc {
}
}
// The generic username regexp allows for standard usernames.
var usernameRx = regexp.MustCompile(`^([a-z0-9]+[.-]?)+[a-z0-9]$`)
func matchUsernameRx() ValidatorFunc {
......@@ -232,6 +233,13 @@ func matchSitenameRx() ValidatorFunc {
return matchUsernameRx()
}
// The identifier regexp is stricter and forbids [-_.] characters.
var identifierRx = regexp.MustCompile(`^[a-z0-9]+$`)
func matchIdentifierRx() ValidatorFunc {
return matchRegexp(identifierRx, "value must be alphanumeric")
}
func validateUsernameAndDomain(validateUsername, validateDomain ValidatorFunc) ValidatorFunc {
return func(ctx context.Context, value string) error {
parts := strings.SplitN(value, "@", 2)
......@@ -379,6 +387,52 @@ func (v *validationContext) isAvailableWebsite() ValidatorFunc {
}
}
func (v *validationContext) isAvailableDAV() ValidatorFunc {
return func(ctx context.Context, value string) error {
rel := []FindResourceRequest{
{
Type: ResourceTypeDAV,
Name: value,
},
}
// Run the presence check in a new transaction. Unavailability
// of the server results in a validation error (fail close).
tx, err := v.backend.NewTransaction()
if err != nil {
return err
}
// Errors will cause to consider the resource unavailable.
if ok, _ := tx.HasAnyResource(ctx, rel); ok { // nolint
return errors.New("name unavailable")
}
return nil
}
}
func (v *validationContext) isAvailableDatabase() ValidatorFunc {
return func(ctx context.Context, value string) error {
rel := []FindResourceRequest{
{
Type: ResourceTypeDatabase,
Name: value,
},
}
// Run the presence check in a new transaction. Unavailability
// of the server results in a validation error (fail close).
tx, err := v.backend.NewTransaction()
if err != nil {
return err
}
// Errors will cause to consider the resource unavailable.
if ok, _ := tx.HasAnyResource(ctx, rel); ok { // nolint
return errors.New("name unavailable")
}
return nil
}
}
func (v *validationContext) validHostedEmail() ValidatorFunc {
return allOf(
validateUsernameAndDomain(
......@@ -629,6 +683,11 @@ func (v *validationContext) validWebsiteResource() ResourceValidatorFunc {
}
func (v *validationContext) validDAVResource() ResourceValidatorFunc {
validator := allOf(
minLength(4),
matchIdentifierRx(),
v.isAvailableDAV(),
)
return func(ctx context.Context, r *Resource, user *User) error {
if err := v.validateShardedResource(ctx, r, user); err != nil {
return err
......@@ -639,6 +698,9 @@ func (v *validationContext) validDAVResource() ResourceValidatorFunc {
return errors.New("resource should not have parent")
}
if err := validator(ctx, r.Name); err != nil {
return err
}
if r.DAV == nil {
return errors.New("resource has no dav metadata")
}
......@@ -651,6 +713,11 @@ func (v *validationContext) validDAVResource() ResourceValidatorFunc {
}
func (v *validationContext) validDatabaseResource() ResourceValidatorFunc {
validator := allOf(
minLength(4),
matchIdentifierRx(),
v.isAvailableDatabase(),
)
return func(ctx context.Context, r *Resource, user *User) error {
if err := v.validateShardedResource(ctx, r, user); err != nil {
return err
......@@ -668,6 +735,9 @@ func (v *validationContext) validDatabaseResource() ResourceValidatorFunc {
// return errors.New("database parent is not a website resource")
// }
if err := validator(ctx, r.Name); err != nil {
return err
}
if r.Database == nil {
return errors.New("resource has no database metadata")
}
......
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