5

I'm trying to implement custom authentication for Elmah.Mvc 2.0. I know there are two keys in my web.config (elmah.mvc.allowedRoles and elmah.mvc.allowedUsers) but it won't be enough for me.

I have a custom Forms Authentication method which adds some random salt in the cookie, so I don't have a clear username to put for elmah.mvc.allowedUsers value. Also, I have no roles implemented.

Is there any way to override ElmahController or some Elmah authentication classes/methods?

Thanks!

dove
  • 20,469
  • 14
  • 82
  • 108
Nebojsa Veron
  • 1,545
  • 3
  • 19
  • 36

3 Answers3

4

Where is ongoing discussion on that - https://github.com/alexanderbeletsky/elmah-mvc/pull/24.

For now, it's not directly possible, but in ticket you could see several solutions, including custom filters. I'm still not really sure about some special tweeks need to be done in Elmah.MVC package itself.

Alexander Beletsky
  • 19,453
  • 9
  • 63
  • 86
3

You can do exactly that and override ElmahController. In fact Alexander Beletsky has already provided a nuget package for just that called Elmah.Mvc.

Once you've created your own ElmahController then you can apply whatever authorization you like to it. In my application I have a base authorized controler which applies it. You just need to configure your routes and return an Elmah result, it's all well documented on his site.

UPDATE: been a while since I looked at this but I've my own controller, inspired by above but not actually using it.

[Authorize]
public class ElmahController : BaseAuthorizedController
{
    public ActionResult Index(string type)
    {
        return new ElmahResult(type);
    }
}

where the result is this

using System;
using System.Web;
using System.Web.Mvc;

namespace Epic.Mvc.Mvc.ActionResults
{
    public class ElmahResult : ActionResult
    {
        private readonly string _resouceType;

        public ElmahResult(string resouceType)
        {
            _resouceType = resouceType;
        }

        public override void ExecuteResult(ControllerContext context)
        {
            var factory = new Elmah.ErrorLogPageFactory();

            if (!string.IsNullOrEmpty(_resouceType))
            {
                var pathInfo = "/" + _resouceType;
                context.HttpContext.RewritePath(FilePath(context), pathInfo, context.HttpContext.Request.QueryString.ToString());
            }

            var currentApplication = (HttpApplication)context.HttpContext.GetService(typeof(HttpApplication));
            if (currentApplication == null) return;
            var currentContext = currentApplication.Context;

            var httpHandler = factory.GetHandler(currentContext, null, null, null);
            if (httpHandler is IHttpAsyncHandler)
            {
                var asyncHttpHandler = (IHttpAsyncHandler)httpHandler;
                asyncHttpHandler.BeginProcessRequest(currentContext, r => { }, null);
            }
            else
            {
                httpHandler.ProcessRequest(currentContext);
            }
        }

        private string FilePath(ControllerContext context)
        {
            return _resouceType != "stylesheet" ? context.HttpContext.Request.Path.Replace(String.Format("/{0}", _resouceType), string.Empty) : context.HttpContext.Request.Path;
        }
    }
}

and I have two routes (the second very optional)

routes.MapRoute("ElmahHandler", "elmah/{type}", new { action = "Index", controller = "Elmah", type = UrlParameter.Optional });
            routes.MapRoute("ElmahHandlerShortHand", "errors/{type}", new { action = "Index", controller = "Elmah", type = UrlParameter.Optional });
dove
  • 20,469
  • 14
  • 82
  • 108
  • When I install Elmah.Mvc nuget package, there is no Elmah Controller (unlike previous versions). Should I create new Elmah Controller? – Nebojsa Veron Aug 20 '13 at 13:14
  • correct and correct. i actually don't use the package but use it's idea in my own result. updated answer for this – dove Aug 20 '13 at 13:26
  • that said the package might have other benefits that have happened since so it might be worth seeing if you can work with that, but up to you. what I've given I've had in production for quite some time without trouble – dove Aug 20 '13 at 13:27
  • I think you are running an older version because in Elmah.Mvc nuget there is no Elmah.ErrorLogPageFactory and I cannot do what you suggested. In the current version there is no need to specify routes explicitly since all the configuration is done with 7 appSettings keys one of which is elmah.mvc.route. – Nebojsa Veron Aug 20 '13 at 18:02
  • ErrorLogPageFactory is part of Elmah itself, like i said this implementation doesn't even use Elmah.MVC but does the equivalent – dove Aug 21 '13 at 07:27
  • @dove, any way to return the built object inside of a `View` instead of `new ElmahResult(type);` so the error page could live inside of your `_layout.cshtml` and appear a little more organically? I can bounty if you think you have an angle on it – KyleMit Feb 27 '18 at 01:23
  • @KyleMit if you're storing all the errors in a database which I assume you are, then you could create a separate view over this for querying directly with filters and search etc. I do this as I also have Nlog info logs that I query for. – dove Feb 27 '18 at 08:58
  • I'm also logging errors as XML in a file share, but I was looking to leverage the `Elmah.MemoryErrorLog` logger and view and just be able to shove all of it inside of a PartialView so that it didn't have to take up it's own `` element and could be rendered inside of the master layout page, but as I'm getting into it - there doesn't seem like a good way to render a partial view from the `Elmah.ErrorLogPageFactory()` – KyleMit Feb 27 '18 at 14:27
  • It sounds like you would be better off with your own custom view reading the elmah data directly. Haven't used the memory log but that sounds like a cheap way to do it and better than reading files off disk. – dove Feb 27 '18 at 14:52
  • @dove - thanks for the input - I built my own view and hydrated it with `Elmah.ErrorLog.GetDefault().GetErrors()` and added it here. – KyleMit Feb 28 '18 at 22:16
0

Elmah has a built in viewer exposed at ~/Elmah.axd and Elmah MVC has a built in viewer at exposed ~/Elmah, both with some preset configuration options.

As dove's answer outlines, if you need to extend beyond the built in configurations, you can wrap the Elmah view inside of your own controller action method and authorize access however you like.

One possible limitation is that rendering the view from Elmah via the Elmah.ErrorLogPageFactory() isn't super friendly to an MVC application with a master layout page.
*To be fair, this constraint applies to any out-of-the-box implementation as well.

Roll Your Own

But since you're writing your own custom code to route and handle the Error Log View anyway, it's not that much addition work to write the view components as well, instead of just wrapping the provided view. This approach by far provides the most control and granularity over not only who may view, but also what they may view as well.

Here's a quick and dirty implementation of a custom Controller, Actions, and Views that exposes the data stored in Elmah.ErrorLog.GetDefault().GetErrors(0, 10, errors).

Controllers/ElmahController.cs:

public class ElmahController : Controller
{
    [Authorize(Roles = "Admin")]
    public ActionResult Index(int pageNum = 1, int pageSize = 7)
    {
        var vm = new ElmahViewModel()
        {
            PageNum = pageNum,
            PageSize = pageSize
        };

        ErrorLog log = Elmah.ErrorLog.GetDefault(System.Web.HttpContext.Current);
        vm.TotalSize = log.GetErrors(vm.PageIndex, vm.PageSize, vm.Errors);

        return View(vm);
    }

    [Authorize(Roles = "Admin")]
    public ActionResult Details(string id)
    {
        ErrorLog log = Elmah.ErrorLog.GetDefault(System.Web.HttpContext.Current);
        ErrorLogEntry errDetail = log.GetError(id);

        return View(errDetail);
    }
}

public class ElmahViewModel
{
    public List<ErrorLogEntry> Errors { get; set; } = new List<ErrorLogEntry>();
    public int TotalSize { get; set; }
    public int PageSize { get; set; }
    public int PageNum { get; set; }
    public int PageIndex => Math.Max(0, PageNum - 1);
    public int TotalPages => (int)Math.Ceiling((double)TotalSize / PageSize);
}

This adds two actions. One to display a list of errors with some optional pagination inputs, and another that'll take in the error id and return just that error. Then we can add the following two views as well:

Views/Elmah/List.cshtml:

@model CSHN.Controllers.ElmahViewModel

@{
    ViewBag.Title = "ELMAH (Error Logging Modules and Handlers)";
}

<table class="table table-condensed table-bordered table-striped table-accent">
    <thead>
        <tr>
            <th>
                Host Name
            </th>
            <th class="text-center" style="width: 85px">
                Status Code
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Errors.First().Error.Type)
            </th>
            <th style="min-width: 250px;">
                @Html.DisplayNameFor(model => model.Errors.First().Error.Message)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Errors.First().Error.User)
            </th>
            <th class="text-center" style="width: 160px">
                @Html.DisplayNameFor(model => model.Errors.First().Error.Time)
            </th>
            <th class="filter-false text-center" data-sorter="false" style="width: 75px">
                Actions
            </th>
        </tr>
    </thead>
    <tbody>
        @if (Model.Errors.Any())
        {
            foreach (var item in Model.Errors)
            {
                <tr>
                    <td>
                        @Html.DisplayFor(modelItem => item.Error.HostName)
                    </td>
                    <td class="text-center" style="width: 85px">
                        @Html.DisplayFor(modelItem => item.Error.StatusCode)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Error.Type)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Error.Message)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Error.User)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Error.Time)
                    </td>
                    <td class="disable-user-select hidden-print text-center" style="width: 75px">
                        <a href="@Url.Action("Details", "Elmah", new { id = item.Id})"
                           class="btn btn-xs btn-primary btn-muted">
                            <i class='fa fa-eye fa-fw'></i> Open
                        </a>
                    </td>
                </tr>

            }
        }
        else
        {
            <tr class="warning">
                <td colspan="7">There haven't been any errors since the last AppPool Restart.</td>
            </tr>
        }

    </tbody>
    @* We need a paginator if we have more records than currently returned *@
    @if (Model.TotalSize > Model.PageSize)
    {
        <tfoot>
            <tr>
                <th colspan="7" class="form-inline form-inline-xs">
                    <a href="@Url.Action("Index", new {pageNum = Model.PageNum - 1, pageSize = Model.PageSize})"
                       class="btn btn-default btn-sm prev @(Model.PageNum == 1?"disabled":"")">
                        <span class="fa fa-backward fa-fw"></span>
                    </a>
                    <span class="pagedisplay">
                        Page @Model.PageNum of @Model.TotalPages
                    </span>
                    <a href="@Url.Action("Index", new {pageNum = Model.PageNum + 1, pageSize = Model.PageSize})"
                       class="btn btn-default btn-sm next @(Model.PageNum == Model.TotalPages?"disabled":"")">
                        <span class="fa fa-forward fa-fw"></span>
                    </a>
                </th>
            </tr>
        </tfoot>
    }

</table>

<style>
    .table-accent thead tr {
        background: #0b6cce;
        color: white;
    }
    .pagedisplay {
        margin: 0 10px;
    }
</style>

Views/Elmah/Details.cshtml:

@model Elmah.ErrorLogEntry

@{
    ViewBag.Title = $"Error Details on {Model.Error.Time}";
}

<a href="@Url.Action("Index", "Elmah")"
   class="btn btn-sm btn-default ">
    <i class='fa fa-th-list fa-fw'></i> Back to All Errors
</a>

<div class="form-horizontal">

    <h4 class="table-header"> General </h4>
    <table class="table table-condensed table-bordered table-striped table-fixed table-accent">
        <thead>
            <tr>
                <th>Name</th>
                <th>Value</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>Message</td>
                <td>@Model.Error.Message</td>
            </tr>
            <tr>
                <td>Type</td>
                <td>@Model.Error.Type</td>
            </tr>
            <tr>
                <td>Time</td>
                <td>@Model.Error.Time</td>
            </tr>
            <tr>
                <td>Detail</td>
                <td><pre class="code-block">@Model.Error.Detail</pre></td>
            </tr>

        </tbody>
    </table>


    <h4 class="table-header"> Cookies </h4>
    <table class="table table-condensed table-bordered table-striped table-fixed table-accent">
        <thead>
            <tr>
                <th >Name</th>
                <th>Value</th>
            </tr>
        </thead>
        @foreach (var cookieKey in Model.Error.Cookies.AllKeys)
        {
            <tr>
                <td>@cookieKey</td>
                <td>@Model.Error.Cookies[cookieKey]</td>
            </tr>
        }
    </table>


    <h4 class="table-header"> Server Variables </h4>
    <table class="table table-condensed table-bordered table-striped table-fixed table-accent">
        <thead>
            <tr>
                <th >Name</th>
                <th>Value</th>
            </tr>
        </thead>
        @foreach (var servKey in Model.Error.ServerVariables.AllKeys)
        {
            if (!string.IsNullOrWhiteSpace(Model.Error.ServerVariables[servKey]))
            {
                <tr>
                    <td>@servKey</td>
                    <td>@Html.Raw(Html.Encode(Model.Error.ServerVariables[servKey]).Replace(";", ";<br />"))</td>
                </tr>
            }

        }
    </table>

</div>

<style>

    .table-header {
        background: #16168c;
        color: white;
        margin-bottom: 0;
        padding: 5px;
    }

    .table-fixed {
        table-layout: fixed;
    }

    .table-fixed td,
    .table-fixed th {
        word-break: break-all;
    }

    .table-accent thead tr {
        background: #0b6cce;
        color: white;
    }

    .table-accent thead tr th:first-child {
        width: 250px;
    }
    .table-accent td:first-child {
        font-weight: bold;
    }


    .code-block {
        overflow-x: scroll;
        white-space: pre;
        background: #ffffcc;
    }

</style>

Another added benefit to this approach is we don't need to set allowRemoteAccess="yes", which can expose some security concerns like session hijacking by exposing the SessionId.

If this implementation isn't robust enough, you can always extend it and customize it till your heart's content. And if you want to leave the local option available, you can still expose that and provide a friendly link for admins on the machine by hiding it under HttpRequest.IsLocal

KyleMit
  • 30,350
  • 66
  • 462
  • 664