Laden...

ASP MVC4 Custom AuthorizeAttribute, wenn AllowMultiple = true ist

Erstellt von ZeroQool vor 10 Jahren Letzter Beitrag vor 10 Jahren 2.653 Views
Z
ZeroQool Themenstarter:in
322 Beiträge seit 2006
vor 10 Jahren
ASP MVC4 Custom AuthorizeAttribute, wenn AllowMultiple = true ist

Hallo zusammen,

in meinem ASP MVC4 Projekt habe ich mir ein CustomAuthorizeAttribut geschrieben, allerdings habe ich jetzt das Problem, wenn ich mehrere Instanzen haben....


[CustomAuthorize(Role = "Administrator", Action = "CRUD")]
[CustomAuthorize(Role = "Group1", Action = "Read")]
[CustomAuthorize(Role = "Group2", Action = "CRUD")]
public ActionResult Index()
{
...
}

Ich logge mich mit einem User, der bspw in der Rolle "Group 1" ist und nur "Leserechte Read" hat. In meiner CustomAuthorize "protected override bool AuthorizeCore(HttpContextBase httpContext)" prüfe ich nun, ob der angemeldete User in den 3 oben genannten Rollen ist. Jetzt ist der Fall eingetreten sobald ich mehrere Annotation über die Action setze, dass ich eventuell im ersten Lauf die Rolle Administrator prüfe und dort ein False zurückgebe, im 2. Lauf habe ich den Group 1 und bekomme ein True, aber durch den 1. Lauf habe ich schon automatisch HandleUnauthorizedRequest ausgelöst...weiß jetzt gerade nicht wie ich das umgehen kann oder ob das überhaupt die richtige Art ist....

Danke

16.834 Beiträge seit 2008
vor 10 Jahren

Dafür ist das Attribut nicht gedacht; ergo musst Du Dir ein eigene Attribut machen.
Ich hab ähnliche Vorgaben bzgl. der Rechteverteilung - arbeite aber völlig ohne Attribute, da mich die Magic Strings ohnehin stören.
Ich prüfe direkt in der Methode und leite ggfls. um.

S
406 Beiträge seit 2007
vor 10 Jahren

Hallo,

Ich nutze auch ein CustomAuthorize Funktion, nur das ich die Rechte und co nicht im Attribut übergebe, sondern in meiner CustomAuthorize Klasse prüfe ich welcher Controller und welche Action aufgerufen wird und entscheide dort ob der Aktuelle User die Aktion ausführen darf oder nicht. Damit sind alle Rechte in einer Klasse Untergebracht und man hält den Controller clean


namespace ContactMvc4TB.Helpers.RightsManagement
{
    /// <summary>
    /// Athorization Attribut, was auswertet ob die UserRolle die übergeben wurde auf den aktellen View oder die Action zugreifen darf,
    /// dafür muss das Attribut gesetzt werden.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class CustomAuthorizeAttribute : AuthorizeAttribute
    {
        #region Member
        private EControllers Controller { get; set; }
        private string ActionName { get; set; }

        /// <summary>
        /// Die aktuell übergebene URL Id, wenn eine übergeben wurde, sonst Null
        /// </summary>
        private string Id { get; set; }

        private AuthorizationContext CurrentContext { get; set; }
        #endregion

        #region Konstruktor
        /// <summary>
        /// Es muss nichts übergeben werden, da controller und action direkt hier ausgelesen werden können
        /// </summary>
        public CustomAuthorizeAttribute()
        {
        }
        #endregion

        #region Public Functions
        /// <summary>
        /// Funktion die ausgeführt wird, wenn das Attribut "genutzt" wird, das Authorisation Attribut wird immer als erstes Attribut ausgeführt.
        /// </summary>
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            base.OnAuthorization(filterContext);
            //Der Filtercontext wird benötigt um auf die RequestDaten zuzugreifen, z.b. auf die UserId die zugegriffen werden soll.
            CurrentContext = filterContext;
            //Setzen des aktuellen Actionnamen, der aufgerufen wird.
            ActionName = filterContext.ActionDescriptor.ActionName;
            //Auslesen der ID, wenn eine in der URL mit übergeben wurde.
            Id = filterContext.RouteData.Values.ContainsKey("id") ? filterContext.RouteData.Values["id"].ToString() : null;

            //Den aktuellen CurrentController ermitteln, dieser ist in RouteData enthalten, des aktuellen filterContext.
            Controller = (EControllers)Enum.Parse(typeof(EControllers), filterContext.RouteData.GetRequiredString(WebConstants.ControllerString));

            //Wenn kein User eingeloggt ist, dann hat er auch keinen Zugriff.
            if (CurrentSession.GetCurrentUser() == null)
            {
                //The AutorizeAttribute has nothing to do with the redirection to the Login page. The AutorizeAttribute only checks for the user rights to access the Action and sets HTTP Status Code 401 Unauthorized on the respons
                //Wenn der User nicht eingeloggt ist, auf die Loginseite verweisen
                filterContext.Result = new HttpUnauthorizedResult();
            }
            else
            {
                //Prüfen der passenden Rechte für die einzelnen Actions/Methoden
                //Wenn der User nicht für den View/Action authentifiziert ist, dann auf die Loginseite verweisen.
                if (!CheckRights())
                {
                    //Auf die Startseite verweisen, wenn der User keinen Zugriff auf den Kontent hat, den er angefordert hat.
                    filterContext.Result = new RedirectResult(string.Format("~/{0}/{1}", EControllers.Home.ToString(), EActionHome.Index.ToString()));
                }
            }

            //Wenn alles i.o. ist "nichts" unternehmen und einfach beim Aufbau der Seite weitermachen.
        }
        #endregion

        #region Private Functions
        /// <summary>
        /// Prüfen um welchen CurrentController es sich handelt und die passende Sicherheitsprüfung vornehmen
        /// </summary>
        /// <returns>TRUE->Darf zugreifen | FALSE->Darf nicht zugreifen</returns>
        private bool CheckRights()
        {
            //Prüfen welcher CurrentController das Attribut aufgerufen hat und dann schauen welche Action aufgerufen wurde.
            switch (Controller)
            {
                case EControllers.Account:
                    return CheckAccountRights();

                case EControllers.User:
                    return CheckUserRights();

                case EControllers.Home:
                    return CheckHomeRights();

                case EControllers.Administration:
                    return CheckAdministrationRights();

                case EControllers.Worktime:
                    return CheckWorktimeRights();

                case EControllers.DynamicData:
                    return WebRights.Right().Check(EUserRights.CreateDynamicDataTypes);
            }

            return false;
        }
....

mfg SquadWuschel

Mein Blog über .NET und MVC / EF | Meine kostenlose Onlinearbeitszeitverwaltung My:Worktime

16.834 Beiträge seit 2008
vor 10 Jahren

Hi,

squadwuschel, manchmal hast echt gute Ideen - die hier gehört aber leider meiner Meinung nicht dazu 😉

Was mir an Deinem Code nicht gefällt:
Du arbeitest mit irgendwelchen MagicStrings

string.Format("~/{0}/{1}", EControllers.Home.ToString(), EActionHome.Index.ToString()));

Besser wäre meiner Meinung nach
T4Routes - Code-Generierung für Asp.net MVC Routen und
ASP.NET MVC - Route Cache

Du erforderst / reagierst auf Parameter - hier Id - was ich nicht toll finde.
Sowas sollte absolut generisch sein; egal wie der Parameter heißt oder welche dabei sind.

Und irgendwelche Methodenbezeichnungen (hier ActionName) als Property zu haben finde ich bzgl. Wartung am Code fahrlässig - vor allem, wenn das Setzen / Manipulieren von Außen möglich ist.

Ich hab euch deswegen was gebastelt, was den Anforderungen nun näher kommen sollte - als netter Nebeneffekt auch meinen 😉

Als erstes brauchen wir mal die Enums, zum Filtern

public enum AuthScope
{
    NONE,
    READ,
    WRITE,
    ALL
}
public enum AuthGroup
{
    NONE,
    ADMIN,
    USER,
}

Anschließend ein eigenes Attibut als Container für den Controller bzw die Action.

[AttributeUsage( AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true )]
public class AuthorizationLevelAttribute : Attribute
{
    public AuthScope Scope { get; private set; }
    public AuthGroup Group { get; private set; }

    public AuthorizationLevelAttribute( AuthScope scope, AuthGroup @group )
    {
        Scope = scope;
        Group = @group;
    }

    private AuthorizationLevelAttribute( )
    {

    }

}

Und damit die Authentifierung dann noch funktioniert ein eigenes Attribut, das diese durchführt. AllowMultiple ist hier mit Absicht auf false, da die Authentifizierung bei AllowMultiple = True, wie es standardmäßig ist, mehrmals ausgeführt werden kann - nicht gerade toll für die Performance, wenn die Abfrage auf die DB erfolgt.

 [AttributeUsage( AttributeTargets.Class, AllowMultiple = false )]
public class MyAuthorizationRequired : AuthorizeAttribute
{
    public override void OnAuthorization( AuthorizationContext filterContext )
    {
        // ControllerDescriptor holen um dessen Attribute zu erlangen
        ControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor( filterContext.Controller.GetType( ) );
        var controllerAttributes = controllerDescriptor.GetCustomAttributes( typeof( AuthorizationLevelAttribute ), true );

        // Nun den ActionDescriptor holen
        ActionDescriptor action = filterContext.ActionDescriptor;
        var actionAttributes = action.GetCustomAttributes( typeof( AuthorizationLevelAttribute ), true );

        // Relevanten Attribute für Auth ausfiltern
        var myAuthorizationLevels = new List<AuthorizationLevelAttribute>( );


        if ( controllerAttributes.Length > 0 )
        {
            foreach ( var attribute in controllerAttributes )
            {
                if ( attribute is AuthorizationLevelAttribute )
                {
                    myAuthorizationLevels.Add( attribute as AuthorizationLevelAttribute );
                }
            }
        }

        if ( actionAttributes.Length > 0 )
        {
            foreach ( var attribute in actionAttributes )
            {
                if ( attribute is AuthorizationLevelAttribute )
                {
                    myAuthorizationLevels.Add( attribute as AuthorizationLevelAttribute );
                }
            }
        }

        // Hier haben wir nun alle Level
        foreach ( var entry in myAuthorizationLevels )
        {
            var scope = entry.Scope;
            var group = entry.Group;
        }
    }
}

In der Anwendung sieht das dann so aus:

    [AuthorizationLevel( AuthScope.NONE, AuthGroup.USER )]
    [MyAuthorizationRequired]
    public class IndexController : Controller
    {

        [AuthorizationLevel( AuthScope.READ, AuthGroup.USER )]
        [AuthorizationLevel( AuthScope.WRITE, AuthGroup.ADMIN )]
        public ActionResult Index( )
        {

Ob man daraus ein OR oder ein AND macht ist dann Implementierungssache.

Ein weiterer Vorteil dieser Variante ist:
einfach einen BasisController erstellen, der MyAuthorizationRequired mit sich führt. Alle anderen Controller dann von diesem Ableiten.
An den andren Controllern und Actions muss man dann nur noch AuthorizationLevelAttribute definieren und fertig.

Viel Spaß damit.

PS: ja, der Code ist mit Absicht etwas ausführlicher und ohne LINQ - des besseren Verständnis wegen.

16.834 Beiträge seit 2008
vor 10 Jahren

Hats jemand von euch probiert und kann was dazu sagen?

S
406 Beiträge seit 2007
vor 10 Jahren

Hallo Abt,

leider noch nicht dazu gekommen was auszuprobieren.
Aber kurz noch etwas zu meiner Lösung, mit dem Zusammenbauen der URL gebe ich dir Recht kann man sicherlich besser machen aber bisher habe ich mich erfolgreich vor Routen drücken können. Da ich meist alles mit der standardroute abbilden konnte und hier bisher nicht an meine Grenzen gekommen bin.

Daher lese ich in meinem Fall auch die ID aus oder versuche es zumindest. das könnte man natürlich auch erst in der jeweiligen Funktion machen wo man die Rechte direkt prüft, denn hier muss ich ja wissen welche Parameter ich benötige für den jeweiligen Controller und die Jeweilige Aktion.

Habe das ganze in einigen Meiner Projekte so eingebunden und setzte nur über die jeweilige Klasse mein Attribut und den Rest regelt die Authorisation von "allein". Fand ich bisher am übersichtlichsten, ist mit Sicherheit aber auch Geschmachkssache 😃

Vor allem wenn man anfängt Gruppen selbst zu erstellen und diesen dann Rechte im Programm zuweißt habe ich bei meiner Lösung auch noch die volle Kontrolle, da bin ich mir bei der hier aufgezeigten Methode nicht ganz sicher da du ja die Gruppen schon vorher festlegst. (oder ich habe es missverstanden auch möglich)

mfg SquadWuschel

Mein Blog über .NET und MVC / EF | Meine kostenlose Onlinearbeitszeitverwaltung My:Worktime

16.834 Beiträge seit 2008
vor 10 Jahren

Ich sehe in Deinem Code keine Rollen, die Du dynamsich verwaltest. Daher meine Umsetzung mit equivalenten Mitteln.
Und wenn wärs problemlos umsetzbar.

Die Standardroute ist toll für Anfänger; aber nichts für produktive Umgebungen.
Ich hab gestern mal wieder "premature optimization is evil" an den Kopf geworfen bekommen, aber man muss sich mit der Materie mal beschäfitgen:

Dynamische Routen wie die Standardroute benötigt zum Rendern ca. 40 mal so lange wie eine Standardroute. Und 20 mal so lange beim Request, um diese einer Action zuzuordnen.

Ohne jetzt tiefer auf die Rechnung einzugehen kannst Du statt 5000 Requests allein durch das Verwenden von dynamicschen Routen nur noch 3000 Request pro Sekunde ausliefern. Und nochmal 1000 Requests kannst abziehen, weil die Route auf die Action gemappt werden muss.
Du verschenkst mit diesem ganzen dynamischen Quatsch, der sich sowieso nur alle Schaltjahre ändern, unfassbar viel Ressourcen und damit verdammt viel Geld wenns um große Anwendungen geht.
Das sind nur Millisekunden pro User. Bei 5 Mio Visits am Tag reden wir bei einer Verlustzeit von optimistischen 15ms pro Request aber von 75 Millionen Millisekunden Rechenzeit => 75000 Sekunden. Das sind mehr als 20 Stunden Rechenzeit (ich glaub ich hab mich nicht verrechnet =) )

Hinweis: Visits != Benutzer. Sondern Reine Requests.
Jeder aktive Nutzer auf Facebook löst im Schnitt 20.000 Visits / Tag aus (aufgrund der ganzen Hinterrundabfragen, Like-Nachladeaktionen etc etc).

20 Stunden sind jetzt nicht nur das Anschaffen von neuer Hardware, damit die Requests verarbeitet werden können - und das NUR aufgrund der erhöhten Routen-Berechnung - sondern alles drum und dran: Stellplatzkosten, Stromkosten, Wartungskosten...
Eine kleine Nachlässigkeit: viele Tausend Euro für eine große Seite.

Für Dich, wenn Du jetzt nur 500 Requests / Tag hast, Du merkst das nicht.
Aber das hier soll ja für die Allgemeinheit gelten.

S
406 Beiträge seit 2007
vor 10 Jahren

Hallo Abt,

Ich weiß du warst von meiner Lösung nicht begeistert 😃, aber ich habe Sie noch einmal etwas "gesäubert" von z.B. "MagicStrings" und niedergeschrieben. Für kleinere Lösungen bin ich damit bisher sehr gut zurecht gekommen.

http://squadwuschel.wordpress.com/2013/06/14/asp-net-mvc-4-custom-authorize-attribute/

mfg
SquadWuschel

Mein Blog über .NET und MVC / EF | Meine kostenlose Onlinearbeitszeitverwaltung My:Worktime

16.834 Beiträge seit 2008
vor 10 Jahren

Finde ich immer noch eine unsaubere Lösung, da Du nun mit Magic Strings direkt als Controllernamen arbeitest. Für mich wär sowas nicht akzeptabel.
Du hattest ja meinen Code kritisiert, dass die Rechtelevel statisch wären (man kanns leicht umbauen).
Bei Dir wäre jedes, absolut jedes mal beim Warten der Controller ein Eingriff in die Authentifizierung notwendig.

Wenn Du also schon Kritik übst: wieso nicht die Rechtelevel und die Controllernamen dynamisch halten? 😜

Was mir noch nicht gefällt:
* Du übergibst beim return View() kein Pfad der Code-Datei -> mindest 8, maximal 24 mal langsamer als mit Pfadangabe
* Vom ModelState halte ich nichts (der ist bei mir immer deaktiviert). Daher ist die Code-Stelle (finde ich) unnütz. Ich vertraue auf IValidatableObject und einer eigenen Implementierung (dazu findet sich hier im Forum viel von mir inkl. Beispiele soweit ich weiß).
* LogOff ist nur im eingeloggten Zustand zu erreichen. Problem: das ist eine Seite, die sich gerne im Browserverlauf anlegt. Kann also sein, dass der Anwender ohne gültiges AuthTicket auf die Seite geht. Damit bekommt er entweder nen YOD oder ne 404 Error Seite; im besseren Fall die Loginseite. Finde ich irgendwie unhübsch.
* Wenn etwas schief läuft gibst Du die View inkl. gefülltem Model zurück. Das heisst, dass hier im dümmsten Fall das Passwort wiederholt über die Leitung geht und im Quellcode landen wird -> mag ich gar nicht 😉

Insgesamt ist auch der Code-Aufbau aus Architektursicht nicht wirklich das Sahnehäubchen 😉
* CurrentContext könnte man auch übergeben und die Methoden statisch halten.
Das kannst sicher besser!
* ...

PS: achte im Blog auf Grammatik und Rechtschreibung - macht sich besser 😉