Разрешения только для чтения в приложении MVC3 с минимальными изменениями

У меня есть задача создать пользователя с доступом только для чтения для нашего приложения ASP.Net MVC3. Т.е. они могут войти в систему, просмотреть все данные, но не могут обновить данные.

Я прочитал много статей / фреймворков аутентификации, например эту: Реализуйте безопасные приложения ASP.NET MVC или Fluent Security Configuration или Создание фильтров действий в ASP.Net MVC (и некоторых других, ссылки на которые я уже потерял).

Проблема с большинством подходов в том, что они требуют кардинальных изменений домена / приложения. И у меня есть только один день, чтобы реализовать эту функцию.

У нас есть около сотни контроллеров, в среднем по 4 действия на контроллер (в основном операции CRUD), и о прохождении каждого из них не может быть и речи. Также было бы легко забыть добавить атрибуты в новый код - это приведет к ошибкам.

До сих пор я придумал глобальный фильтр, который запрещает все действия на основе POST и действия контроллера под названием «Создать» для пользователя с доступом только для чтения:

public class ReadOnlyFilter : IActionFilter 
{

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var currentUser = HttpContext.Current.User;
        if (currentUser == null || !currentUser.Identity.IsAuthenticated)
            return; // user is not logged in yet


        if (!currentUser.IsInRole("Readonly")) 
            return; // user is not read-only. Nothing to see here, move on!

        // Presume User is read-only from now on.


        // if action is of type post - deny
        if (filterContext.HttpContext.Request.HttpMethod.ToUpper() == "POST")
        {
            filterContext.HttpContext.Response.Redirect("~/ReadOnlyAccess");
        }

        // if action is "Create" - deny access
        if (filterContext.ActionDescriptor.ActionName == "Create")
        {
            filterContext.HttpContext.Response.Redirect("~/ReadOnlyAccess");
        }

        // if action is edit - check if Details action exits -> redirect to it.
        //TODO get this done ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

        return;
    }


    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
        // blah! have to have this here for IActionFilter
    }
}

Следующим шагом я планирую создать атрибут [AllowReadOnlyUser] для действий публикации, таких как изменение пароля / адреса электронной почты, и в фильтре разрешить выполнение этого действия.

Интересно, есть ли способы сделать это лучше?

Обновление: приложение не предназначено для публичного использования. Он используется в корпоративном мире для отслеживания людей, активов и других скучных данных.

Обновление 2: похоже, я закончил с этой задачей. Сделал это как контроллер, как и начал. Полный код и некоторые пояснения, которые вы можете увидеть в моем блоге.


person trailmax    schedule 18.09.2012    source источник
comment
Это позволит публиковать сообщения для незарегистрированных пользователей. Также я не уверен, что POST обязательно будет в верхнем регистре.   -  person usr    schedule 18.09.2012
comment
Нашим приложением пользуются только зарегистрированные пользователи. В мире нет доступных контроллеров, кроме LoginController. Так что это нормально. Справедливый вопрос о ПОЧТЕ. Я исправлю это - спасибо!   -  person trailmax    schedule 18.09.2012
comment
Недавно мне пришлось за очень короткое время преобразовать старое приложение asp.net webforms в режим только для чтения. Мое решение заключалось в том, чтобы использовать javascript / jquery, чтобы отключить все элементы управления вводом и гиперссылки, не связанные с навигацией, и удалить весь код функции щелчка / отправки, чтобы предотвратить обратные передачи.   -  person asawyer    schedule 18.09.2012
comment
@asawyer Да, это сработает. Пока вы не получите пользователя, который знает, как использовать инструменты отладки в браузере и некоторые jquery.   -  person trailmax    schedule 18.09.2012
comment
@trailmax Я не буду отрицать, что это возможно, но я не смог вернуться и восстановить страницу до такой степени, что она могла успешно публиковаться, даже с глубоким знанием приложения и веб-форм. Кроме того, мое приложение было только внутренним и предназначалось только для государственных аудиторов, которые вряд ли попытались бы сделать что-то подобное. Я сделал это всего за несколько часов, поэтому и предложил это. Если хотите, я могу опубликовать сценарий, чтобы вы посмотрели и протестировали.   -  person asawyer    schedule 18.09.2012
comment
@asawyer Да, звучит интересно. Не возражаете опубликовать в качестве ответа? так что вы получите свою долю голосов -)   -  person trailmax    schedule 18.09.2012
comment
@trailmax Это опубликовано. Сообщите мне, если вам удастся исправить и опубликовать форму после ее запуска.   -  person asawyer    schedule 18.09.2012


Ответы (2)


вы можете использовать атрибут System.Web.Mvc.AuthorizeAttribute для своих целей. Создайте класс, производный от AuthorizeAttribute, и переопределите методы AuthorizeCore и HandleUnauthorizedRequest. В AuthorizeCore вы определяете, разрешено ли пользователю выполнять действие, в HandleUnauthorizedRequest вы определяете, что отображать, если ему не разрешено (например, показывать «NotAllowed» -View).

После создания настраиваемого атрибута авторизации вы должны добавить этот атрибут ко всем действиям контроллера, которые должны быть защищены вашей настраиваемой авторизацией. Например, все POST-методы. Но если существует метод POST, который должен быть разрешен для всех пользователей, вы просто не добавляете атрибут к этому действию контроллера.

person Dirk Trilsbeek    schedule 18.09.2012
comment
Мы уже рассматривали этот вариант и были вынуждены отказаться от него. У нас МНОГО контроллеров, и засоренный атрибут [ReadOnly] - не лучший вариант и магнит для новых контроллеров. - person trailmax; 18.09.2012
comment
вы по-прежнему можете использовать AuthorizeAttribute на уровне контроллера или даже получить свои контроллеры из настраиваемого контроллера с установленным атрибутом. Если у метода есть собственный атрибут AuthorizeAttribute, ничего не делайте на уровне контроллера. Вам придется переопределить метод OnAuthorization, чтобы проверить атрибуты уровня метода. Тем не менее, все равно будет означать изменить все ваши контроллеры. Но я не уверен, что общий перехват методов по их имени и типу запроса является хорошей идеей, поскольку он в такой же степени полагается на соглашения, применяемые на организационном уровне (мы используем Create только для методов создания). - person Dirk Trilsbeek; 18.09.2012
comment
+1. Теперь это похоже на гораздо лучшую идею. Я посмотрю на это. - person trailmax; 18.09.2012
comment
вот пример: stackoverflow.com/questions/780436/. Один из (более поздних) ответов также содержит код для авторизации на уровне контроллера / уровня метода, см. Здесь: gist.github.com / 948822 - person Dirk Trilsbeek; 18.09.2012

Вам придется немного подправить это, и прежде чем кто-нибудь скажет мне, я на 100% осознаю, что это ужасный взлом. Но это также довольно эффективно и очень быстро внедрялось, что было главной заботой в то время. Вы, конечно, захотите пропустить это через обсфикатора.

Там также есть некоторые элементы панели обновлений, которые должны быть удалены или заменены на конечные хуки ответа jQuery ajax или что-то в этом роде, если это необходимо.

О, и есть это для управления его запуском только для пользователей с доступом только для чтения:

if (isReadonly && !Page.ClientScript.IsClientScriptBlockRegistered("ReadonlyScriptController"))
{
this.Page.ClientScript.RegisterStartupScript(this.GetType(), 
  "ReadonlyScriptController", "<script>RunReadonlyScript();</script>");
}

Сценарий:

<script type="text/javascript" src="<%# Page.ResolveUrl("~/ClientScripts/jquery-1.4.2.min.js") %>"></script>
<script type="text/javascript">
    function RunReadonlyScript() {
        //Extend jquery selections to add some new functionality
        //namely, the ability to select elements based on the
        //text of the element.
        $.expr[':'].textEquals = function (a, i, m) {
            var match = $(a).text().match("^" + m[3] + "$");
            return match && match.length > 0;
        };
        //this function does all the readonly work
        var disableStuff = function () {

            //select all controls that accept input, save changes, open a popup, or change form state
            // ** customize this with your own elements **
            $('button, input:not(:hidden), textarea, select,
              a:textEquals("Clear Selection"), 
              a:textEquals("Add Message"), 
              a:textEquals("CCC EVAL"),
              a[id$="availLink"], 
              a[id$="lbtnDelete"], 
              a[id$="lbtnEdit"]')
                //disable input controls
                .attr('disabled', 'disabled')
                //remove onclick javascript
                .removeAttr('onclick')
                //remove all bound click events
                .unbind("click")
                //add a new click event that does nothing
                //this stops <a> links from working
                .click(function (e) {
                    e.preventDefault(); 
                    return false;
                });

            //zap some images with click events that we don't want enabled
            $('img[id$="imgCalendar1"], img[id$="imgDelete"]').hide();
        }
        //if we are loading the home page, don't disable things
        //or the user won't be able to use the search controls
        var urlParts = document.URL.split('/');
        var last2 = urlParts[urlParts.length - 2] + '/' + urlParts[urlParts.length - 1];
        if (last2 !== 'home/view') {
            //disable everything initially
            disableStuff();
            //setup a timer to re-disable everything once per second
            //some tab panels need this to stay disabled
            setInterval(disableStuff, 1000);
            //some pages re-enable controls after update panels complete
            //make sure to keep them disabled!
            Sys.WebForms.PageRequestManager.getInstance().add_beginRequest(disableStuff);
            Sys.WebForms.PageRequestManager.getInstance().add_endRequest(disableStuff);
        }
    }
</script>
person asawyer    schedule 18.09.2012
comment
Спасибо за скрипт, он пригодился. Я использую это как дополнение к тому, что у меня уже есть на стороне сервера: если представление предоставляет элементы ввода, и они по какой-то причине не отключены на сервере, ваш jquery срабатывает. Как только я закончу, я напишу в блоге по этой теме и поставлю под сомнение ссылку. - person trailmax; 19.09.2012
comment
Кроме того, я только что поиграл с консолью Chrome и мне удалось отправить форму через скрипт: $('button, input:not(:hidden), textarea, select').removeAttr("disabled"); $("#Comment").val("Hello, i'm from jquery console");$("form").submit(); Так что это хорошее дополнение к пользовательскому интерфейсу и должно быть подкреплено мерами на стороне сервера (если эта функция критична). - person trailmax; 19.09.2012
comment
@trailmax Я полагаю, что MVC тоже легче публиковать, чем пытаться снова запустить обратную передачу веб-формы. Я никогда не пробовал это на MVC. Я забронировал отметку с этим вопросом, и на случай, если мне это понадобится для любого из моих новых приложений! - person asawyer; 19.09.2012