Validating / managing inputs in a multi-tenant web API

Suppose we have an application for a multi-user blog. Each user of the application may have several blogs hosted by the service.

Our API allows you to read and write blog posts. In some cases, specifying BlogId is optional, for example, receiving all posts tagged using ASP.NET:

/api/posts?tags=aspnet

If we wanted to see all posts tagged with ASP.NET on a particular blog, we could request:

/api/posts?blogId=10&tags=aspnet

Some API methods require a valid BlogId, for example, when creating a new blog post:

POST: /api/posts
{
    "blogid" : "10",
    "title" : "This is a blog post."
}

BlogId , , () . , ( , ).

IAccountContext, . .

{
    bool ValidateBlogId(int blogId);
    string GetDefaultBlog();
}

ASP.NET Web API, :

  • BlogId , uri, , . 400, .
  • BlogID , BlogDd IAccountContext . , , IAccountContext .

[Update]

Twitter @Aliostad Blog Uri ( ), ..

GET api/blog/1/posts -- get all posts for blog 1
PUT api/blog/1/posts/5 -- update post 5 in blog 1

​​ Post id ( ).

BlogID. , Uri, @alexanderb . ActionFilter:

public class ValidateBlogAttribute : ActionFilterAttribute
{
    public IBlogValidator Validator { get; set; }

    public ValidateBlogAttribute()
    {
        // set up a fake validator for now
        Validator = new FakeBlogValidator();
    }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var blogId = actionContext.ActionArguments["blogId"] as int?;

        if (blogId.HasValue && !Validator.IsValidBlog(blogId.Value))
        {
            var message = new HttpResponseMessage(HttpStatusCode.BadRequest);
            message.ReasonPhrase = "Blog {0} does not belong to you.".FormatWith(blogId);
            throw new HttpResponseException(message);
        }

        base.OnActionExecuting(actionContext);
    }
}

public class FakeBlogValidator : IBlogValidator
{
    public bool IsValidBlog(int blogId)
    {
        return blogId != 999; // so we have something to test
    }
}

blogId / [ValidateBlog].

, @alexanderb , .

+5
4

(, -API ASP.NET).

, - . , :

public class BlogPost
{
    [Required]
    [ValidateBlogId]
    public string BlogId { get; set; }

    [Required]
    public string Title { get; set; }
}

. blogId , . ,

public class ValidateBlogId : ValidationAttribute
{
    [Inject]
    public IAccountContext Context { get; set; }

    public override bool IsValid(object value)
    {
        var blogId = value as string;
        if (!string.IsNullOrEmpty(blogId))
        {
            return Context.ValidateBlogId(blogId);
        }

        return true;
    }
}

( Ninject, ).

blogId. .

public class InitializeBlogIdAttribute : ActionFilterAttribute
{
    [Inject]
    public IAccountContext Context { get; set; }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var blogPost = actionContext.ActionArguments["blogPost"] as BlogPost;
        if (blogPost != null) 
        {
            blogPost.BlogId = blogPost.BlogId ?? Context.DefaultBlogId();
        }
    }
}

, blogPost , .

, , API

public class PostsController : ApiController
{
    [InitializeBlogId]
    public HttpResponseMessage Post([FromBody]BlogPost blogPost) 
    {
        if (ModelState.IsValid)
        {
            // do the job
            return new HttpResponseMessage(HttpStatusCode.Ok);
        }

        return new HttpResponseMessage(HttpStatusCode.BadRequest);
    }
}

. VS, , .

, .

+5

, , , , , .

, , , blogId? , . REST - , , , , () , HTTP.

BlogId, , - , URL. , , URL/URI - . , , .

, . , . .

+10

, HttpParameterBinding . Mike Hongmei .

:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Metadata;

namespace MvcApplication49.Controllers
{
public class PostsController : ApiController
{
    public string Get([BlogIdBinding]int blogId, string tags = null)
    {
        return ModelState.IsValid + blogId.ToString();
    }

    public string Post([BlogIdBinding]BlogPost post)
    {
        return ModelState.IsValid + post.BlogId.ToString();
    }
}

[DataContract]
public class BlogPost
{
    [DataMember]
    public int? BlogId { get; set; }

    [DataMember(IsRequired = true)]
    public string Title { get; set; }

    [DataMember(IsRequired = true)]
    public string Details { get; set; }
}

public class BlogIdBindingAttribute : ParameterBindingAttribute
{
    public override System.Web.Http.Controllers.HttpParameterBinding GetBinding(System.Web.Http.Controllers.HttpParameterDescriptor parameter)
    {
        return new BlogIdParameterBinding(parameter);
    }
}

public class BlogIdParameterBinding : HttpParameterBinding
{
    HttpParameterBinding _defaultUriBinding;
    HttpParameterBinding _defaultFormatterBinding;

    public BlogIdParameterBinding(HttpParameterDescriptor desc)
        : base(desc)
    {
        _defaultUriBinding = new FromUriAttribute().GetBinding(desc);
        _defaultFormatterBinding = new FromBodyAttribute().GetBinding(desc);
    }

    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
                                HttpActionContext actionContext, CancellationToken cancellationToken)
    {
        Task task = null;

        if (actionContext.Request.Method == HttpMethod.Post)
        {
            task = _defaultFormatterBinding.ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken);
        }
        else if (actionContext.Request.Method == HttpMethod.Get)
        {
            task = _defaultUriBinding.ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken);
        }

        return task.ContinueWith((tsk) =>
            {
                IPrincipal principal = Thread.CurrentPrincipal;

                object currentBoundValue = this.GetValue(actionContext);

                if (actionContext.Request.Method == HttpMethod.Post)
                {
                    if (currentBoundValue != null)
                    {
                        BlogPost post = (BlogPost)currentBoundValue;

                        if (post.BlogId == null)
                        {
                            post.BlogId = **<Set User Default Blog Id here>**;
                        }
                    }
                }
                else if (actionContext.Request.Method == HttpMethod.Get)
                {
                    if(currentBoundValue == null)
                    {
                        SetValue(actionContext, **<Set User Default Blog Id here>**);
                    }
                }
            });
    }
}

}

[ ] ActionFilter. :

public class PostsController : ApiController
{
    [BlogIdFilter]
    public string Get(int? blogId = null, string tags = null)
    {
    }

    [BlogIdFilter]
    public string Post(BlogPost post)
    {
    }
}

public class BlogIdFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (actionContext.Request.Method == HttpMethod.Get && actionContext.ActionArguments["blogId"] == null)
        {
            actionContext.ActionArguments["blogId"] = <Set User Default Blog Id here>;
        }
        else if (actionContext.Request.Method == HttpMethod.Post)
        {
            if (actionContext.ActionArguments["post"] != null)
            {
                BlogPost post = (BlogPost)actionContext.ActionArguments["post"];

                if (post.BlogId == null)
                {
                    post.BlogId = <Set User Default Blog Id here>;
                }
            }
        }
    }
}
+3

, , , , , .

, , BlogId Uri, ( ).

, :

  • .
  • , .
  • , (, ) 400 Bad Request.

, . , , :

public abstract class ApiControllerBase : ApiController {

    public int BlogId { get; set; }

    public override Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken) {

        var query = controllerContext.Request.RequestUri.ParseQueryString();
        var accountContext = controllerContext.Request.GetDependencyScope().GetService(typeof(IAccountContext));
        if (query.AllKeys.Any(x => x.Equals("BlogId", StringComparison.OrdinalIgnoreCase | StringComparison.InvariantCulture))) {

            int blogId;
            if (int.TryParse(query["BlogId"], out blogId) && accountContext.ValidateBlogId(blogId)) {

                BlogId = blogId;
            }
            else {

                ModelState.AddModelError("BlogId", "BlogId is invalid");

                TaskCompletionSource<HttpResponseMessage> tcs = 
                    new TaskCompletionSource<HttpResponseMessage>();
                tcs.SetResult(
                    controllerContext.Request.CreateErrorResponse(
                        HttpStatusCode.BadRequest, ModelState));
                return tcs.Task;
            }
        }
        else {

            BlogId = accountContext.GetDefaultBlogId();
        }

        return base.ExecuteAsync(controllerContext, cancellationToken);
    }
}

You may also consider using IValidatableObject for your RequestModel, but this may make your model a bit related to other parts of your application.

+2
source

All Articles