ASP.Net Web Api Help page based on authorization - c#

I'm using the ASP.Net Web API behind Windows Authentication and using the [Authorize] attribute to dictate what controllers and functions users have access to. This works great. The problem is that I would like to have the help area reflect only what the user has been granted for access. Curious if anyone has achieved this in some fashion. Is this done at the level of the controller, the App Start, or the help controller.
Thanks in advance...
Code snippet of one of my controllers
[Authorize]
public class TaktTimeController : ApiController
{
private BIDataContainer db = new BIDataContainer();
// GET api/TaktTime
[Authorize(Roles="Admins")]
public IQueryable<TaktTime> GetTaktTimes()
{
return db.TaktTimes;
}
// GET api/TaktTime/5
[ResponseType(typeof(TaktTime))]
[Authorize(Roles = "Admins")]
public IHttpActionResult GetTaktTime(string id)
{
TaktTime takttime = db.TaktTimes.Find(id);
if (takttime == null)
{
return NotFound();
}
return Ok(takttime);
}

You will need to modify HelpController.cs and add the following method:
using System.Collections.ObjectModel;
private Collection<ApiDescription> FilteredDescriptions()
{
var descriptionsToShow = new Collection<ApiDescription>();
foreach (var apiDescription in Configuration.Services.GetApiExplorer().ApiDescriptions)
{
var actionDescriptor = apiDescription.ActionDescriptor as ReflectedHttpActionDescriptor;
var authAttribute = actionDescriptor?.MethodInfo.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == nameof(System.Web.Http.AuthorizeAttribute));
var roleArgument = authAttribute?.NamedArguments?.FirstOrDefault(x => x.MemberName == nameof(System.Web.Http.AuthorizeAttribute.Roles));
var roles = roleArgument?.TypedValue.Value as string;
if (roles?.Split(',').Any(role => User.IsInRole(role.Trim())) ?? false)
{
descriptionsToShow.Add(apiDescription);
}
}
return descriptionsToShow;
}
And call it from the Index() action:
return View(FilteredDescriptions());

This can be achieved in razor view something like the following would be what you need.
#if (User.IsInRole("admin"))
{
<div>
<!--Text for admin here-->
</div>
}
#if (User.IsInRole("user"))
{
<div>
<!--Text for user here-->
</div>
}
The same logic can be used in WebApi controllers
public string Get()
{
if(User.IsInRole("admin"))
{
return "Text for admin";
}
if(User.IsInRole("user"))
{
return "Text for user";
}
}

Building upon Stanislav's approach, I have added support for AllowAnonymous, username-based authorization, controller attributes and global authorization filters.
public ActionResult Index()
{
ViewBag.DocumentationProvider = Configuration.Services.GetDocumentationProvider();
//return View(Configuration.Services.GetApiExplorer().ApiDescriptions);
return View(FilteredDescriptions());
}
private Collection<ApiDescription> FilteredDescriptions()
{
var list = Configuration.Services.GetApiExplorer().ApiDescriptions
.Where(apiDescription =>
{
// action attributes
if (apiDescription.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count != 0)
{
return true;
}
var actionAuthorizeAttributes = apiDescription.ActionDescriptor.GetCustomAttributes<AuthorizeAttribute>();
if (actionAuthorizeAttributes.Count != 0)
{
return actionAuthorizeAttributes.All(IsUserAuthorized);
}
// controller attributes
if (apiDescription.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count != 0)
{
return true;
}
var controllerAuthorizeAttributes = apiDescription.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AuthorizeAttribute>();
if (controllerAuthorizeAttributes.Count != 0)
{
return controllerAuthorizeAttributes.All(IsUserAuthorized);
}
// global attributes
if (apiDescription.ActionDescriptor.Configuration.Filters.OfType<AllowAnonymousAttribute>().Any())
{
return true;
}
var globalAuthorizeAttributes = apiDescription.ActionDescriptor.Configuration.Filters.OfType<AuthorizeAttribute>().ToList();
if (globalAuthorizeAttributes.Count != 0)
{
return globalAuthorizeAttributes.All(IsUserAuthorized);
}
return true;
})
.ToList();
return new Collection<ApiDescription>(list);
}
private bool IsUserAuthorized(AuthorizeAttribute authorizeAttribute)
{
return User.Identity.IsAuthenticated
&& (authorizeAttribute.Roles == "" || authorizeAttribute.Roles.Split(',').Any(role => User.IsInRole(role.Trim())))
&& (authorizeAttribute.Users == "" || authorizeAttribute.Users.Split(',').Any(user => User.Identity.Name == user));
}

Related

C# Controller behavior changes based on variable name

I don't know enough about C#, .NET, or the MVC pattern to know exactly what is relevant to include here, but I'm pulling my hair out with a very simple change I'm working on.
I have a controller with a Search action (method?) that looks like:
public string Search(int id)
{
return $"The id was {id}";
}
and when I hit the route I get the expected response, e.g.
$ curl https://localhost:7180/Players/Search/1
The id was 1
but when I change the variable name from id to anything else, the behavior changes and the value goes to 0 for some reason.
public string Search(int thing)
{
return $"The thing was {thing}";
}
$ curl https://localhost:7180/Players/Search/1
The thing was 0
I thought maybe it had to do with the Model itself, because the model code at least has an Id attribute
public class Player
{
public int Id { get; set; }
public string? Name { get; set; }
}
but renaming that variable to name (which seems analogous) also doesn't help.
So what concept am I missing here? Why can't I just rename that variable to whatever I want? Thanks in advance!
(I don't know how better to communicate all the different aspects of the code, so here is a link to the line in question, inside the project)
By default MVC registers (see either Program or Startup) next default route, so it can bind id parameter of method as positional part of path:
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
You can change the parameter name for example using attribute routing:
[Route("[controller]/search/{thing}")]
public string Search(int thing)
{
return $"The thing was {thing}";
}
Or using HTTP verb templates:
[HttpGet("[controller]/search/{thing}")]
public string Search(int thing)
{
return $"The thing was {thing}";
}
Check the linked docs for other options/details.
I believe this has to do with the way you've defined your route in Program.cs:
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
You'll want to add a new definition like this:
app.MapControllerRoute(
name: "default",
pattern: "Players/Search/{thing?}");
or, you could use attribute-based route definitions to move the route pattern definition closer to the actual code. See the MSFT docs for details. Basically, add app.MapControllers(); to Program.cs, then for your individual routes, do something like this:
[Route("Players/Search/{thing}")]
public string Search(int thing)
{
return $"The thing was {thing}";
}
You can decorate the method and define the parameter.
// GET api/values/5
[HttpGet("{id}")]
public virtual async Task<ActionResult<IEntity>> Get(string id)
{
var entity = await Repository.GetEntity(x => x.Id == id);
if (entity == null) return NotFound();
return Ok(entity);
}
Here is another example of an API Controller
[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
private readonly GameDataContext context;
public UserController(GameDataContext context)
{
this.context = context;
}
// GET: /<controller>/
// GET api/user
[HttpGet]
public ActionResult<IEnumerable<User>> Get()
{
return context.Users.Where(x => x.IsDeleted == false).ToArray();
}
// GET api/user/5
[HttpGet("{id}", Name = "GetUser")]
public ActionResult<User> Get(int id)
{
var user = context.Users.FirstOrDefault(x => x.Id == id && x.IsDeleted == false);
if (user != null)
{
return user;
}
return NotFound();
}
// GET api/user/username/5
[HttpGet("username/{id}", Name = "GetUserByGameId")]
public ActionResult<User> GetByUser(string gameId)
{
var user = context.Users.FirstOrDefault(x => x.UserGameId.Equals(gameId) && x.IsDeleted == false);
if (user != null)
{
return user;
}
return NotFound();
}
// POST api/user
[HttpPost]
public async Task<ActionResult> Post([FromBody] User value)
{
if (value == null)
{
return BadRequest();
}
var user = context.Users.FirstOrDefault(x => x.UserGameId.Equals(value.UserGameId) && x.IsDeleted == false);
if (user != null)
{
return BadRequest("User already exists!");
}
context.Users.Add(value);
await context.SaveChangesAsync();
return CreatedAtRoute("GetUser", new { id = value.Id }, value);
}
// PUT api/user/steamId
[HttpPut("{gameId}")]
public async Task<ActionResult> Put(string gameId, [FromBody] User value)
{
if (value == null || !value.UserGameId.Equals(gameId))
{
return BadRequest();
}
var user = context.Users.FirstOrDefault(x => x.UserGameId.Equals(gameId) && x.IsDeleted == false);
if (user == null)
{
return NotFound();
}
user.UserGameId = value.UserGameId;
user.FirstName = value.FirstName;
user.MiddleName = value.MiddleName;
user.LastName = value.LastName;
user.Email = value.Email;
context.Users.Update(user);
await context.SaveChangesAsync();
return new NoContentResult();
}
// DELETE api/user/steamId
[HttpDelete("{gameId}")]
public async Task<ActionResult> Delete(string gameId)
{
if (string.IsNullOrEmpty(gameId))
{
return BadRequest();
}
var user = context.Users.FirstOrDefault(x => x.UserGameId.Equals(gameId) && x.IsDeleted == false);
if (user == null)
{
return NotFound();
}
user.IsDeleted = true;
context.Users.Update(user);
var scores = context.Scores.Where(x => x.UserId == user.Id);
foreach (var score in scores)
{
score.IsDeleted = true;
context.Scores.Update(score);
}
await context.SaveChangesAsync();
return new NoContentResult();
}
}

ASP.NET Core 5 MVC Missing default action for all controllers except HomeController

I use ASP.NET Core 5 MVC and I have an interesting issue. When I want load the Index method of any controller, then I have to write index action name into URL too. If i write only controller name into the URL, then it load nothing. I don't get any exception. I haven't folder in root directory with name what matching with any controller name. I use default routing, so i don't understand what is the problem. But i noticed an interesting stuff while debugging. I tried set a custom route path with data-annotation. When I renamed the controller, then I get an AmbiguousMatch exception.
"AmbiguousMatchException: The request matched multiple endpoints.
Matches:
CanteenFeedback2._0.Controllers.AdminPanelController.Login
(CanteenFeedback2.0)
CanteenFeedback2._0.Controllers.AdminPanelController.EditQuestion
(CanteenFeedback2.0)
CanteenFeedback2._0.Controllers.AdminPanelController.Statistic
(CanteenFeedback2.0)
CanteenFeedback2._0.Controllers.AdminPanelController.GetStatistic
(CanteenFeedback2.0)
CanteenFeedback2._0.Controllers.AdminPanelController.Index
(CanteenFeedback2.0)
CanteenFeedback2._0.Controllers.AdminPanelController.AuthError
(CanteenFeedback2.0)
CanteenFeedback2._0.Controllers.HomeController.Index
(CanteenFeedback2.0)"
A detail from AdminPanelController:
public class AdminPanelController : Controller
{
private readonly DatabaseContext DB;
public AdminPanelController(DatabaseContext dB)
{
DB = dB;
}
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult Index()
{
string userName = User.Claims.Where(claim => claim.Type == ClaimTypes.NameIdentifier).First().Value;
string lastName = User.Claims.Where(claim => claim.Type == ClaimTypes.GivenName).First().Value;
bool exists = false;
string userId = User.Claims.Where(claim => claim.Type == ClaimTypes.NameIdentifier).First().Value;
if (DB.IdmAccounts.FirstOrDefault(i => i.AccountId == userId) != null)
{
exists = true;
}
else
{
exists = false;
}
IEnumerable<Category> categories;
IEnumerable<Subcategory> subcategories;
IEnumerable<Question> questions;
try
{
categories = DB.Categories;
subcategories = DB.Subcategories;
questions = DB.Questions
.Where(q => q.isArchived == false)
.Include(q => q.Categories)
.Include(q => q.Subcategories)
.ThenInclude(sc => sc.Category);
}
catch (SqlException)
{
return Content("Nem sikerült betölteni a kérdéseket");
}
catch (Exception)
{
return Content("Nem sikerült betölteni a kérdéseket");
}
ViewBag.lastName = lastName;
ViewBag.exists = exists;
ViewBag.categories = categories;
ViewBag.subcategories = subcategories;
ViewBag.questions = questions;
return View();
}
[HttpPost]
public IActionResult SessionSave(string token)
{
HttpContext.Session.SetString("JWToken", token);
return Redirect("http://localhost:5000/AdminPanel/Index");
}
[HttpGet]
public IActionResult Login()
{
return View();
}
public IActionResult AuthError()
{
return View();
}
}
And a detail from HomeController:
public class HomeController : Controller
{
public IActionResult Index()
{
// Cookie options
CookieOptions cookieOptions = new CookieOptions();
cookieOptions.Expires = DateTime.Now.AddDays(365);
// Add default cookie
Response.Cookies.Append("lang_cookie", Languages.HU.ToString(), cookieOptions);
// Setting language to view
Language language = new Language(Pages.Home, Languages.HU);
return View(language.GetPageTexts());
}
[Route("{changedLan}")]
public IActionResult Index(Languages changedLan)
{
// Cookie options
CookieOptions cookieOptions = new CookieOptions();
cookieOptions.Expires = DateTime.Now.AddDays(365);
// Update cookie
Response.Cookies.Append("lang_cookie", changedLan.ToString(), cookieOptions);
Language language = new Language(Pages.Home, changedLan);
return View(language.GetPageTexts());
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}

Hide parameter from Swagger (Swashbuckle)

I have a C# .NET 5.0 ASP.NET Core Web API application with "Enable OpenAPI support" selected. I want to hide the optional parameter in the below example from what shows up on the swagger page. I have found numerous posts about hiding a property or the controller but none of these solutions seem to work for just the parameter in the given code:
[HttpGet]
[Route("search")]
[Authorize]
public async Task<IActionResult> Search(string query, string optional = "")
{
return OK();
}
You can create a custom attibute and an operation filter inhering from Swashbuckle.AspNetCore.SwaggerGen.IOperationFilter to exclude the desired parameters from swagger.json generation
public class OpenApiParameterIgnoreAttribute : System.Attribute
{
}
public class OpenApiParameterIgnoreFilter : Swashbuckle.AspNetCore.SwaggerGen.IOperationFilter
{
public void Apply(Microsoft.OpenApi.Models.OpenApiOperation operation, Swashbuckle.AspNetCore.SwaggerGen.OperationFilterContext context)
{
if (operation == null || context == null || context.ApiDescription?.ParameterDescriptions == null)
return;
var parametersToHide = context.ApiDescription.ParameterDescriptions
.Where(parameterDescription => ParameterHasIgnoreAttribute(parameterDescription))
.ToList();
if (parametersToHide.Count == 0)
return;
foreach (var parameterToHide in parametersToHide)
{
var parameter = operation.Parameters.FirstOrDefault(parameter => string.Equals(parameter.Name, parameterToHide.Name, System.StringComparison.Ordinal));
if (parameter != null)
operation.Parameters.Remove(parameter);
}
}
private static bool ParameterHasIgnoreAttribute(Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription parameterDescription)
{
if (parameterDescription.ModelMetadata is Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata metadata)
{
return metadata.Attributes.ParameterAttributes.Any(attribute => attribute.GetType() == typeof(OpenApiParameterIgnoreAttribute));
}
return false;
}
}
Put it in your controller's parameter
[HttpGet]
[Route("search")]
[Authorize]
public async Task<IActionResult> Search(string query, [OpenApiParameterIgnore] string optional = "")
{
return Ok();
}
Then configure it in Status.cs
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API Title", Version = "v1" });
c.OperationFilter<OpenApiParameterIgnoreFilter>();
});

How to redirect from one Razor Page to another

I have an Page called Page1.
Inside of OnGet(string parameter1) I check a parameter, and in some case want to route the user to another Page.
The Page is located here:
Pages/App/App.cshtml
I have tried this:
this.RedirectToPage("/App/App");//
But the user does not get redirected. It just shows the same page as expected if the redirect was not there. I want them to see the App page.
So how do I redirect to the App page?
This is what worked:
public async Task<IActionResult> OnGet(string web_registration_key)
{
//Check parameter here
if(doRedirect)
{
return RedirectToPage("/App/App")
}
}
return null;
}
The return null seems odd, but not sure what else to return.
I used the return Page();
Here is an example from on of my projects:
public IActionResult OnGet(int id)
{
var MenuItemFromDb = _db.MenuItem.Include(m => m.CategoryType).Include(m => m.FoodType)
.Where(x => x.Id == id).FirstOrDefault();
if (MenuItemFromDb == null)
{
return RedirectToPage("./Index");
}
else
{
ShowCart(id);
return Page();
}
}
private void ShowCart(int id)
{
var MenuItemFromDb = _db.MenuItem.Include(m => m.CategoryType).Include(m => m.FoodType)
.Where(x => x.Id == id).FirstOrDefault();
CartObj = new ShoppingCart()
{
MenuItemId = MenuItemFromDb.Id,
MenuItem = MenuItemFromDb
};
}

AllowAnonymous not Working MVC5

I'm using a custom filter (defined as follows):
if (user == null || !user.Active)
{
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary
{
{"controller", "Home"},
{"action", "NotAuthorized"}
});
}
base.OnActionExecuting(filterContext);
This is run site-wide (in RegisterGlobalFilters() within FilterConfig.cs. However, there is one page I'd like to allow access to - the NotAuthorized page. In the HomeController, I have created the following ActionResult method:
[AllowAnonymous]
public ActionResult NotAuthorized()
{
return View();
}
Being unauthorized does lead the user to this view, but it results in a redirect loop (likely because the filter is still being run on this page).
How can I allow anonymous users to access this page?
You need to check for the attribute in your custom filter.
Try:
if (!filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false)
&& !filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false)
&& (user == null || !user.Active))
{
//....
}
Check for the AllowAnonymousAttribute in your custom filter. Here is one, resuable way to do it.
Add the following extension method.
public static class MyExtensionMethods
{
public static bool HasAttribute(this ActionExecutingContext context, Type attribute)
{
var actionDesc = context.ActionDescriptor;
var controllerDesc = actionDesc.ControllerDescriptor;
bool allowAnon =
actionDesc.IsDefined(attribute, true) ||
controllerDesc.IsDefined(attribute, true);
return allowAnon;
}
}
Then use it in your filter.
public class MyActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// use the extension method in your filter
if (filterContext.HasAttribute(typeof(AllowAnonymousAttribute)))
{
// exit early...
return;
}
// ...or do whatever else you need to do
if (user == null || !user.Active)
{
filterContext.Result =
new RedirectToRouteResult(new RouteValueDictionary
{
{ "controller", "Home" },
{ "action", "NotAuthorized" }
});
}
base.OnActionExecuting(filterContext);
}
}
Here is a fiddle that implements a solution.

Categories

Resources