Article
· Jan 6 23m de lecture

Le package de sécurité - Considérations sur l'API REST

Jusqu'à présent, nous avons expliqué comment utiliser ObjectScript pour gérer les utilisateurs, les rôles, les ressources et les applications. Il existe quelques autres classes dans ce package qui fonctionnent de manière similaire à celles mentionnées ci-dessus. Cependant, ces quatre classes sont celles que chacun devra utiliser pour gérer la sécurité de ses applications. Supposons que vous souhaitiez créer votre propre portail de gestion de la sécurité pour ce package. Il y aurait des problèmes spécifiques à prendre en compte pour une API. Étant donné que les classes utilisent des méthodes similaires, nous pouvons créer moins de points de terminaison d'API en utilisant l'indirection. Cependant, nous devons considérer les différences entre ces méthodes en fonction des classes. Si nous faisons cela, nous pouvons garder les choses plus cohérentes pour les appels d'API.

Avant d'entrer plus dans les détails, je ne vous rendrais pas service si je ne vous faisais pas connaître l'API REST existante par @Lorenzo Scalese. Bien que cet article soit un exercice sur la façon d’aborder vous-même le processus de création, l’œuvre de Lorenzo est également une excellente source de référence.

Nous devons également aborder la question de la sécurité. Puisqu'il s'agit d'une fonctionnalité de sécurité, nous devrions être tentés de la sécuriser à l'aide de la ressource %Admin_Secure, mais ce n'est peut-être pas la meilleure solution. Les utilisateurs qui ne disposent pas de cette ressource peuvent souhaiter modifier certaines de leurs informations personnelles, telles que leur nom complet, leur mot de passe, leur numéro de téléphone, leur opérateur téléphonique ou leur adresse e-mail. Nous pourrions y parvenir en maintenant deux API distinctes : une avec les méthodes permettant aux utilisateurs de base de mettre à jour leurs informations privées et une avec tout le reste. Cependant, puisque nous en apprenons davantage sur la sécurité, nous devons nous mettre au défi et essayer de sécuriser différents points de terminaison en utilisant diverses ressources.

Comme toujours, notre API devra étendre la classe %CSP.REST. Nous allons commencer par créer une classe abstraite qui étend la classe %CSP.REST et remplace certains de ses éléments. Tout d’abord, nous devrions examiner le bloc XData Schema, où nous verrons les définitions XML par rapport auxquelles le mappage d’URL est validé. Les nœuds de route sont définis pour nécessiter un attribut Url, Méthode et Appel. Il existe également un attribut Cors facultatif. Notre tâche ici serait d'ajouter une autre définition d'attribut comme suit :

 

<xs:attribute name="Resource" type="string" use="optional" default=" "/>

Une fois que nous avons ajouté cela au schéma XData, nous pouvons inclure un attribut Resource dans nos nœuds Route dans le mappage URL. Cependant, si nous n'avions pas ajouté cela au schéma XData, il n'aurait pas compilé les classes possédant l'attribut Resource. Dans notre cas, nous prendrons l'attribut Resource sous la forme resource:permission pour l'uniformiser avec d'autres parties du package Security. Le nœud Route peut ressembler à ceci :

<Route Url="/securetest" Method="GET" Call="securetest" Resource="MyResource:U" />

Ensuite, nous devons dire à notre classe quoi faire avec cette information si nous l’incluons. Si elle est incluse, nous voulons nous assurer que les utilisateurs disposent de cette ressource avant de les laisser accéder au point de terminaison de l'API. Pour ce faire, nous devons d’abord examiner la méthode DispatchMap. Cette méthode est une méthode génératrice. Il lit le mappage d'URL et produit du code basé sur ces informations. Examinez le bloc de code commençant par If tMap(tChild)=”Route” pour comprendre la gestion des nœuds Route. Nous pouvons remarquer une ligne de code qui substitue les groupes de capture aux arguments, nous aimerions donc la modifier pour l'ajouter à notre nouvel attribut Resource :

Set tPattern="",tArgCount=0,tUrl=tMap(tChild,"a","Url"),tCors=tMap(tChild,"a","Cors"),tResource=tMap(tChild,"a","Resource")

Nous venons d'insérer l'attribut Resource dans une variable appelée tResource. Quelques lignes en dessous, il y a une chaîne commençant par $$$GENERATE. Cela crée une liste que d’autres méthodes peuvent utiliser dans notre classe. Nous devrons également y inclure notre tResource, alors modifions-le :

$$$GENERATE(" If pIndex="_tCounter_" Quit $ListBuild(""R"","""_tPattern_""","""_tMethod_""","""_tCall_""","""_tCors_""","""_tResource_""")")

À ce stade, nous avons rendu la ressource récupérable mais nous ne l’avons pas utilisée à notre avantage. La dernière méthode que nous devrions modifier est la méthode DispatchRequest. Il utilise DispatchMap pour décider ce qui doit être fait exactement avec la requête API REST. Vers le début de cette méthode, nous définirons une variable appelée tSecurityResourceMatched :

Set tSecurityResourceMatched = 1

Cette méthode utilise ensuite un comparateur d'expressions régulières pour trouver l'entrée de la map qui correspond à l'URL de la requête. Il existe un bloc de code commençant par If tMatcher.Match(tMatchUrl). Cet élément est exécuté lorsqu'il a trouvé la méthode correspondant à l'URL. À ce stade, le tMapEntry a déjà été défini sur le bon DispatchMap. Le tResource que nous avons ajouté à la fin est la sixième entrée de cette liste, donc pour obtenir notre tResource, nous devons simplement utiliser ce qui suit :

set tResource = $List(tMapEntry,6)

Le tResource est par défaut un seul espace, donc s'il s'agit de tout sauf d'un espace, nous souhaitons effectuer un contrôle de sécurité sur la ressource et l'autorisation que nous mettons dans l'attribut Resource, comme ceci :

If tResource '= " "{
    If $SYSTEM.Security.Check($P(tResource,":",1),$P(tResource,":",2))=0{
            Set tSecurityResourceMatched=0
        }
}

Ce que nous voulons faire ici, c'est inclure le reste de la gestion des nœuds Route dans une instruction if afin de le faire exécuter uniquement si tSecurityResourceMatched = 1. Après cela, en bas, nous devons lui dire quoi faire si Le tSecurityResourceMatched vaut zéro. Vers le bas de la méthode, vous pouvez également voir comment gérer plusieurs autres erreurs possibles. Il y en a un lorsqu'aucune ressource API correspondante n'est trouvée et un autre lorsque la ressource API est trouvée, mais sa méthode n'est pas définie. Avant ces remarques, ajoutons ce qui suit :

If tSecurityResourceMatched = 0 Set tSC=..ReportHttpStatusCode(..#HTTP401UNAUTHORIZED) Quit

À ce stade, si nous incluons une ressource dans un nœud de route, cette route donnera une erreur HTTP 401 non autorisée aux utilisateurs qui ne disposent pas de cette ressource comme nous le souhaitions. Nous allons enregistrer et compiler cette classe, puis l'étendre pour notre API de package de sécurité. Si vous n’avez pas suivi et créé ce cours, ne vous inquiétez pas ! Vous pouvez trouver le code source complet de cette classe au bas de cet article. Je l'appellerai REST.Resourceful.

Maintenant que nous avons réfléchi à toutes les choses mentionnées ci-dessus, nous pouvons réfléchir à la manière dont nous pourrions développer une API et aux préoccupations que nous pourrions avoir. Étant donné que les méthodes au sein des classes sont très uniformes, si nous sommes prêts à utiliser une certaine indirection, nous pouvons obtenir de nombreuses utilisations à partir d'un petit nombre de points de terminaison. Considérez l'itinéraire et la méthode d'URL à venir :

<Route Url="/get/:classname/:name" Method="GET" Call="Get" Resource="%Admin_Secure:U" />
ClassMethod Get(classname As %String, name As %String) [ PublicList = (props, sc) ]
{
    set %response.ContentType="application/json"
    zn "%SYS"
    if classname="Applications"{
         set name=$REPLACE(name,"%2F","/")
    }
    set code = "sc=##class(Security."_classname_").Get("""_name_""",.props)"
    set @code
    set respObj={}.%New()
    do ##class(%CSP.REST).StatusToJSON(sc,.scJSON)
    do respObj.%Set("status",scJSON)
    set index = $O(props(""))
    while index '= ""{
        do respObj.%Set(index,props(index))
        set index = $O(props(index))
    }
    write respObj.%ToJSON()
    return $$$OK
}

Le code mentionné ci-dessus utilise l'indirection pour obtenir le tableau de propriétés de n'importe quelle classe que vous spécifiez et le renvoie au format JSON. Cependant, vous devez remarquer que la gestion de la classe Applications est en quelque sorte spéciale. Comme ils contiennent fréquemment des barres obliques dans leurs noms, ils ne fonctionnent pas toujours bien avec les requêtes de l'API REST. Par conséquent, selon la façon dont votre serveur Web est configuré, vous devrez les remplacer par %2F ou %252F dans votre requête.

Nous pouvons utiliser des routes et des procédures similaires pour chaque méthode standard du package Sécurité :

 

<Route Url="/exists/:classname/:name" Method="GET" Call="Exists" Resource="%Admin_Secure:U" />
<Route Url="/delete/:classname/:name" Method="DELETE" Call="Delete" Resource="%Admin_Secure:U" />
<Route Url="/modify/:classname/:name" Method="POST" Call="Get" Resource="%Admin_Secure:U" />
<Route Url="/create/:classname/:name" Method="PUT" Call="Get" Resource="%Admin_Secure:U" />

Nous pouvons faire des exceptions pour certaines classes en cours de route, de sorte que même si la méthode du package Security diffère légèrement de la norme (comme discuté dans les articles précédents), nous pouvons y faire face. Par exemple, alors que dans certains cas, les méthodes Create ou Modify peuvent utiliser un tableau de propriétés, dans d'autres classes, elles nécessitent une liste d'arguments. Vous pouvez toujours accepter JSON afin que l'utilisation soit cohérente au sein de l'API. Pourtant, n’oubliez pas d’apporter ces modifications cruciales au sein de l’API.

Considérons maintenant les utilisateurs finaux qui souhaitent modifier leurs informations personnelles. Nous pouvons créer un point de terminaison pour eux sans ressource définie en utilisant le code ci-dessous :

<Route Url="/editme" Method="POST" Call="EditMe" />
ClassMethod EditMe() As %Status
{
    set %response.ContentType="application/json"
    zn "%SYS"
    set reqjson = %request.Content.Read()
    set reqObj = {}.%FromJSON(reqjson)
    set sc = ##class(Security.Users).Get($USERNAME,.oldprops)
    if reqObj.%Get("FullName") = ""{
        set props("FullName") = oldprops("FullName")
    }
    else{
        set props("FullName") = reqObj.%Get("FullName")
    }
    if reqObj.%Get("PhoneNumber") = ""{
        set props("PhoneNumber") = oldprops("PhoneNumber")
    }
    else{
        set props("PhoneNumber") = reqObj.%Get("PhoneNumber")
    }
    if reqObj.%Get("PhoneProvider") = ""{
        set props("PhoneProvider") = oldprops("PhoneProvider")
    }
    else{
        set props("PhoneProvider") = reqObj.%Get("PhoneProvider")
    }
    if reqObj.%Get("EmailAddress") = ""{
        set props("EmailAddress") = oldprops("EmailAddress")
    }
    else{
        set props("EmailAddress") = reqObj.%Get("EmailAddress")
    }
    set sc = ##class(Security.Users).Modify($USERNAME,.props)
    set respObj = {}.%New()
    do ##class(%CSP.REST).StatusToJSON(sc,.scJSON)
    do respObj.%Set("Status",scJSON)
    write respObj.%ToJSON()
    return $$$OK
}

The abovementioned code will allow them to edit exclusively the full name, phone number, phone provider, and email address for their username. In this method, we ought to be really careful with picking out only those pieces we want to allow.

If you keep these principles in mind, it will be a piece of cake for you to create your own basic Security API.

The code for our customized %CSP.REST class follows:

/// Extends the %CSP.REST class to include securing of endpoints by resource in the URL map
Class REST.Resourceful Extends %CSP.REST [ Abstract ]
{
XData Schema [ Internal ]
{
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" >
<xs:element name="Routes">
<xs:complexType>
<xs:choice  minOccurs="0" maxOccurs="unbounded">
<xs:element name="Route">
<xs:complexType>
<xs:attribute name="Url"    type="string" use="required"/>
<xs:attribute name="Method" type="string" use="required"/>
<xs:attribute name="Call" type="call" use="required"/>
<xs:attribute name="Cors" type="xs:boolean" use="optional" default="false"/>
<xs:attribute name="Resource" type="string" use="optional" default=" "/>
</xs:complexType>
</xs:element>
<xs:element name="Map">
<xs:complexType>
<xs:attribute name="Prefix" type="string" use="required"/>
<xs:attribute name="Forward" type="forward" use="required"/>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
<xs:simpleType name="call">
<xs:restriction base="xs:string">
<xs:pattern value="([%]?[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)*:)?[%]?[a-zA-Z][a-zA-Z0-9]*"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="forward">
<xs:restriction base="xs:string">
<xs:pattern value="([%]?[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)*)"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="string">
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>
}
/// This generator creates the DispatchMap Method used to dispatch the URL and Method to the associated target method
ClassMethod DispatchMap(pIndex As %Integer) As %String [ CodeMode = generator ]
{
#dim tSC As %Status = $$$OK
    #dim e As %Exception.AbstractException
   #dim tStream As %Stream.TmpCharacter
    #dim tHandler As %XML.ImportHandler
    #dim tCompiledClass As %Dictionary.CompiledClass
    #dim tArgCount,tIndex,tI,tCounter As %Integer
    #dim tArgs,tChild,tClassName,tCall,tCors,tForward,tError,tMap,tMethod,tPattern,tPiece,tPrefix,tType,tUrl,tResource As %String
    Try {
        Set tClassName=%classname
        #; Don't run on base class
        If tClassName="%CSP.REST" Quit
        #; Find named XDATA block
        If ##class(%Dictionary.CompiledXData).%ExistsId(tClassName_"||UrlMap") {
            Set tCompiledClass=##class(%Dictionary.CompiledClass).%OpenId(tClassName,,.tSC)
            If '$IsObject(tCompiledClass)||$$$ISERR(tSC) Quit
            Set tIndex = tCompiledClass.XDatas.FindObjectId(tClassName_"||UrlMap")
            If tIndex="" Set tSC=$$$ERROR($$$XDataBlockMissing,tClassName,"UrlMap") Quit
            #; Get XDATA as stream
            Set tStream = tCompiledClass.XDatas.GetAt(tIndex).Data
            Do tStream.Rewind()
            #; Create an XML import handler ( use the internal handler )
            Set tHandler=##class(%XML.ImportHandler).%New("CacheTemp",$$$IntHandler)
            #; Create the Entity Resolver
            Set tResolver=##class(%XML.SAX.XDataEntityResolver).%New(tClassName)
            #; Parse the XML data in the specfied stream
            Set tSC=##Class(%XML.SAX.Parser).ParseStream(tStream,tHandler,tResolver,,,"Schema")
            If $$$ISERR(tSC) Quit       
            #; Copy tree because handler will delete its copy when it goes out of scope
            Merge tMap=@tHandler.DOMName@(tHandler.Tree)
            If $Data(tMap("error"))||$Data(tMap("warning")) {
                Set tSC=$$$ERROR($$$InvalidDispatchMap)
        For tType="error","warning" {
                    Set tIndex = "" For {
                        Set tIndex=$Order(tMap(tType,tIndex),1,tError) If tIndex="" Quit
                        Set tSC=$$$ADDSC(tSC,$$$ERROR($$$GeneralError,tError))
                    }
                }
                Quit
            }
            #; Walk the xml and generate the routing map
            Set tChild="",tCounter=0 
            For {
                Set tChild=$Order(tMap(1,"c",tChild)) If tChild="" Quit
                If tMap(tChild)="Route" {
                #; Need to substitute capture groups for arguments
                #; Added setting of tResource based on URL map - DLH
                Set tPattern="",tArgCount=0,tUrl=tMap(tChild,"a","Url"),tCors=tMap(tChild,"a","Cors"),tResource=tMap(tChild,"a","Resource")
                #; Substitute variable placeholders for capture group
                For tI=1:1:$Length(tUrl,"/") {
                    Set tPiece=$Piece(tUrl,"/",tI)
                    If $Extract(tPiece)=":" {
                        Set $Piece(tPattern,"/",tI)="([^"_$Char(0)_"]+)"
                    } else {
                        Set $Piece(tPattern,"/",tI)=tPiece                  }
                }
                Set tPattern=$Translate(tPattern,$Char(0),"/")
                Set tCounter=$Increment(tCounter),tMethod=tMap(tChild,"a","Method"),tCall=$Get(tMap(tChild,"a","Call"))
                #; Added getting resource from the URL Map here. - DLH
                $$$GENERATE(" If pIndex="_tCounter_" Quit $ListBuild(""R"","""_tPattern_""","""_tMethod_""","""_tCall_""","""_tCors_""","""_tResource_""")")
            } else {
                Set tCounter=$Increment(tCounter),tPrefix=tMap(tChild,"a","Prefix"),tForward=$Get(tMap(tChild,"a","Forward"))
                #; Need to substitute capture groups for arguments
                Set tPattern=""
                For tI=2:1:$Length(tPrefix,"/") {
                    Set tPiece=$Piece(tPrefix,"/",tI)
                    If $Extract(tPiece)=":" {
                        Set tPattern=tPattern_"/[^/]+"
                    } else {
                        Set tPattern=tPattern_"/"_tPiece
                    }
                }
                Set tPattern = "("_ tPattern _ ")/.*"
                $$$GENERATE(" If pIndex="_tCounter_" Quit $ListBuild(""M"","""_tPattern_""","""_tForward_""")")
            }
            }
            $$$GENERATE(" Quit """"")
        } else {
            #; The specified class must have an XDATA Block named UrlMap
            Set tSC=$$$ERROR($$$XDataBlockMissing,tClassName,"UrlMap")
        }
    } Catch (e) {
        Set tSC=e.AsStatus()
    }
    Quit tSC
}
/// Dispatch a REST request according to URL and Method
ClassMethod DispatchRequest(pUrl As %String, pMethod As %String, pForwarded As %Boolean = 0) As %Status
{
    #dim tSC As %Status = $$$OK
    #dim e As %Exception.AbstractException
    #dim tMatcher As %Regex.Matcher
    #dim tArgs,tClass,tMatchUrl,tMapEntry,tRegEx,tCall,tForward,tAccess,tSupportedVerbs,tTarget,tType As %String
    #dim tI,tIndex As %Integer
    #dim tResourceMatched,tContinue As %Boolean
    #dim tMethodMatched As %Boolean
    Try {
        Set (tResourceMatched,tMethodMatched)=0
        #; Initializing tSecurityResourceMatched - DLH
        Set tSecurityResourceMatched = 1
        #; Extract the match url from the application name
        If (0=pForwarded) {
            Set tMatchUrl="/"_$Extract(pUrl,$Length(%request.Application)+1,*)
        } else {
            Set tMatchUrl=pUrl
        }
        #; Uppercase the method
        Set pMethod=$ZCVT(pMethod,"U")
        #; Pre-Dispatch
        Set tContinue=1,tSC=..OnPreDispatch(tMatchUrl,pMethod,.tContinue)
        If $$$ISERR(tSC) Quit
        #; It's the users responsibility to return the response in OnPreDispatch() if Continue = 0
        If tContinue=0 Quit
        #; Walk the dispatch map in collation order of defintion
        For tIndex=1:1 {
            #; Get the next map entry
            Set tMapEntry=..DispatchMap(tIndex) If tMapEntry="" Quit
            #; Pick out the RegEx
            Set tRegEx=$List(tMapEntry,2)
            #; Create a matcher
            Set tMatcher=##class(%Regex.Matcher).%New(tRegEx)
            #; Test each regular expression in turn, extracting the arguments,
            #; dispatching to the named method
            If tMatcher.Match(tMatchUrl) {
                #; We have matched the resource
                Set tResourceMatched=1
                #; Logic to check the resource from the URL map
                set tResource = $List(tMapEntry,6)
                If tResource '= " "{
                    If $SYSTEM.Security.Check($P(tResource,":",1),$P(tResource,":",2))=0{
                        Set tSecurityResourceMatched=0
                    }
                }
     #; Added an if so the method only gets dispatched if we have the resource permission
                If tSecurityResourceMatched = 1{
                Set tType=$List(tMapEntry,1)
                 #; If we are a simple route
                 If tType="R" {
                     #; Support OPTIONS VERB (cannot be overriden)
                    If pMethod="OPTIONS" {
                         Set tMethodMatched=1
                         Set tSC=..OnHandleOptionsRequest(tMatchUrl)
                         If $$$ISERR(tSC) Quit
                         #; Dispatch CORS
                         Set tSC=..ProcessCorsRequest(pUrl,$list(tMapEntry,5))
                         If $$$ISERR(tSC) Quit
                         Quit
                     }
                     #; comparison is case-insensitive now
                     If pMethod'=$ZCVT($List(tMapEntry,3),"U") Continue
                     Set tTarget=$List(tMapEntry,4)
                     #; We have matched a method
                     Set tMethodMatched=1
                     #; Dispatch CORS
                     Set tSC=..ProcessCorsRequest(pUrl,$list(tMapEntry,5))
                     If $$$ISERR(tSC) Quit
                     #; Got a match, marshall the arguments can call directly
                     If tMatcher.GroupCount {
                         For tI=1:1:tMatcher.GroupCount Set tArgs(tI)=tMatcher.Group(tI)
                         Set tArgs=tI
                     } else {
                         Set tArgs=0
                     }
                     #; Check for optional ClassName prefix
                     Set tClass=$classname()
                     If tTarget[":" Set tClass=$Piece(tTarget,":"),tTarget=$Piece(tTarget,":",2)
                     #; Dispatch
                     Set tSC=$zobjclassmethod(tClass,tTarget,tArgs...)
                 } else {
                     #; We are a map, massage the URL and forward the request
                     Set tMatchUrl=$piece(tMatchUrl,tMatcher.Group(1),"2",*),tForward=$ListGet(tMapEntry,3)
                     Set (tResourceMatched,tMethodMatched)=1
                     #; Dispatch with modified URL
                     Set tSC=$zobjclassmethod(tForward,"DispatchRequest",tMatchUrl,pMethod,1)
                 }
                 If $$$ISERR(tSC) Quit
                 #; Don't want multiple matches
                 Quit
                }
            }
        }
        #; Didn't have permission for the resource required by this enpoint; return 401 - DLH
        If tSecurityResourceMatched = 0 Set tSC=..ReportHttpStatusCode(..#HTTP401UNAUTHORIZED) Quit
        #; Didn't have a match for the resource, report not found
        If tResourceMatched=0 Set tSC=..ReportHttpStatusCode(..#HTTP404NOTFOUND) Quit
        #; Had a match for resource but method not matched
        If tMethodMatched=0 {
            Set tSC=..SupportedVerbs(tMatchUrl,.tSupportedVerbs)
            If $$$ISERR(tSC) Quit
            Set tSC=..Http405(tSupportedVerbs) Quit
        }
    } Catch (e) {
        Set tSC=e.AsStatus()
    }
    Quit tSC
}
}
Discussion (1)1
Connectez-vous ou inscrivez-vous pour continuer

NDT : Je pense a voir trouvé une autre façon de protéger une route.
Je ne protège pas la route à proprement parler, mais la méthode vers laquelle elle pointe à l'aide de [ Requires = "myResource:Use" ] :

/// Sample API
Class Test.Api Extends %CSP.REST
{

 XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
 {
	<Routes>
		<Route Url="/test" Method="GET" Call="Test" />
	</Routes>
 }

 ClassMethod Test() As %Status [ Requires = "myResource:Use" ]
 {
  Do ##class(%REST.Impl).%WriteResponse("api method test : OK")
  Return $$$OK
 }
}