Sécurité des tables
Power Portals Pro sécurise les données à trois niveaux : quelles tables un utilisateur peut toucher, quelles colonnes de ces tables il peut voir et modifier, et quelles lignes reviennent du tout. Les trois sont décidées lors d’un seul enregistrement par tableau. Décrivez cette inscription avec AddTableAccess — c’est la manière d’accorder une table, et ce que les modèles de projet utilisent pour chaque table qu’ils enregistrent. Là où une règle est indescriptible, les mêmes trois réponses peuvent être écrites comme une ITablePermissionProvider classe ; toutes deux produisent la même chose et composent de la même manière. Rien n’est accordé tant que l’une d’elles ne le dit pas : une table couverte par aucun enregistrement est inaccessible, ce qui est le défaut de faille-fermeture.
Conseil
Vous n’êtes pas obligé d’écrire cela à partir d’une page blanche. Les projets générés livrent une
secure-a-tablecompétence : demandez à votre agent de codage de sécuriser une table et il passe par la table, à qui elle s’adresse, à quelles lignes, quelles opérations et quelles colonnes, puis écrit l’enregistrement et effectue les modifications correspondantes dans Dataverse via le serveur Power Portals Pro MCP — créant le rôle du portail si l’accès est lié au rôle, et accordant à l’utilisateur de l’application les privilèges que les réponses impliquent. Ensuite,check_setupsignalez toute table à laquelle vos pages sont liées et à laquelle rien n’accorde d’accès, ainsi que toute inscription qui se retire de la sécurité des colonnes. Voir Démarrer → serveur MCP.
Tout le monde, ou seulement quelques personnes
Chaque enregistrement indique à qui il s’adresse, et il y a deux réponses. Enregistrez-le auprès du portail et il applique à un public vos noms de code — tout le monde, ou chaque utilisateur connecté. Enregistrez-le à l’intérieur AddPortalRole et cela s’applique aux titulaires de ce rôle, ce qui est une décision de données prise par un administrateur dans Dataverse. L’accès lui-même est décrit de la même manière dans les deux cas.
| Enregistrement | S’applique à | Pour changer qui l’obtient |
|---|---|---|
AddTableAccess("account", t => t.ForSignedInUsers()…) |
tous ceux que le public couvre, à chaque demande | Modifier le code et redéployer |
AddPortalRole("Credit Manager", role => role.WithTableAccess("account", …)) |
des contacts occupant ce rôle de Portail, et personne d’autre | assigner ou désassigner le rôle dans Dataverse — pas de déploiement |
Un tableau peut être enregistré une fois pour chaque audience à l’échelle du portail — ForEveryone() pour ce que chacun peut atteindre, ForSignedInUsers() pour ce que la connexion apporte — et les deux s’additionnent. Un visiteur obtient le premier ; un utilisateur connecté obtient les deux, chaque opération étant confinée aux rangées de l’enregistrement qui l’accorde. Avec les deux inscriptions ci-dessous, tout le monde lit chaque article, et un auteur connecté peut aussi en écrire un et éditer le sien :
builder.Services.AddTableAccess("ppp_article", t => t
.ForEveryone()
.AllRows()
.Allow(TableSecurityPermission.Read)
.Reading("ppp_title", "ppp_body"));
builder.Services.AddTableAccess("ppp_article", t => t
.ForSignedInUsers()
.RowsOwnedByContact("ppp_authorid")
.Allow(TableSecurityPermission.Create | TableSecurityPermission.Write)
.Writing("ppp_title", "ppp_body"));
Les rôles s’additionnent ; ils ne soustraient jamais. L’accès d’un utilisateur correspond aux inscriptions à l’échelle du portail plus chaque rôle qu’il occupe, syndiqué — donc celui à l’échelle du portail est le plancher sur lequel tout le monde se tient, et un rôle ne peut que l’augmenter. Un rôle ne peut pas reprendre une colonne ou une ligne accordée par l’enregistrement à l’échelle du portail ; pour retenir quelque chose à la plupart des gens, l’accorder dans un rôle plutôt que dans tout le portail. (Réglé SecurityOptions.Roles.CompositionBehavior à Restrictive si vous avez besoin que chaque groupe soit d’accord à la place.)
Donc : si chaque utilisateur de ce type devait avoir cet accès, enregistrez-le auprès du portail. Si seulement certains le devraient — et quelle personne est une question dont la réponse change sans autorisation — le mettront dans un rôle. Une inscription à un rôle n’accorde rien tant que quelqu’un ne crée pas un enregistrement de rôle de portail avec ce nom et ne l’attribue, ce qui explique pourquoi on peut s’asseoir dans un projet frais sans danger.
Description de l’accès d’une table
La plupart des tables veulent les mêmes quelques règles : pour qui est la table, quelles lignes ils atteignent, ce qu’ils peuvent faire, et quelles colonnes ils voient. AddTableAccess dit les quatre en un seul registre et produit un ordinaire ITablePermissionProvider — rien en aval ne fait la différence. Ce qu’il achète, c’est que les trois décisions qui doivent s’accorder (le filtre, la création et les colonnes écrivables) sont prises une fois au lieu de trois.
builder.Services.AddTableAccess("incident", t => t
.ForSignedInUsers()
.RowsOwnedByContact("customerid")
.Allow(TableSecurityPermission.Read | TableSecurityPermission.Create)
.Reading("ticketnumber", "statuscode", "createdon")
.Writing("title", "description"));
Un public, une ligne de contrôle et Allow(…) tous sont obligatoires. Aucun d’eux n’a de défaut, car chaque défaut plausible est un moyen de publier des données en omettant quelque chose — et un enregistrement qui se trompe échoue au démarrage plutôt qu’en production.
Lunettes à rangées
Le scope filtre chaque requête pour la table — grilles, recherches, sous-grilles, graphiques, et la récupération avant une sauvegarde ou une suppression. Les longerons nommés couvrent les formes dont les portails ont besoin constamment :
| Quelles rangées | Portée | Sur créer |
|---|---|---|
| Chaque rangée | AllRows() |
rien n’est fixé |
| Les propres enregistrements de l’utilisateur | RowsOwnedByContact("customerid") |
la recherche est assignée à eux |
| Les dossiers de leurs collègues, au même compte | RowsOfTheirAccount("customerid") |
La recherche est configurée sur leur compte |
| Des dossiers suspendus à un parent qu’ils peuvent atteindre | RowsViaParent("salesorderid", "salesorder", p => …) |
Rien n’est fixé — c’est l’accès du parent qui décide |
| Des archives qui les pointent de plus d’un point | RowsMatchingAny(r => r.RowsOwnedByContact("customerid"), r => r.RowsOfTheirAccount("customerid")) |
Ce que la première portée de propriété listée définit |
| Leur propre conflit de contacts | OwnRecordOnly() |
Pas de création |
Les champs de propriété définissent eux-mêmes la colonne lors de la création d’un enregistrement, après les vérifications des permissions : l’utilisateur n’a pas besoin d’accès en écriture ni d’Append To sur la table vers laquelle il pointe, la valeur provient de la session plutôt que de la requête, et une sauvegarde ultérieure qui la modifierait est refusée. C’est ce qui maintient un enregistrement créé par quelqu’un dans le filtre auquel il est soumis. Utilisez OwnedBy('column') quand la colonne propriétaire n’est pas celle qui filtre les lignes, et WithoutStamping() quand un plugin la définit déjà.
Quand la lecture et le changement diffèrent
One scope répond à chaque opération sauf indication contraire. RowsForWriting Réduit les lignes qu’un utilisateur peut modifier sans réduire celles qu’il peut voir — « chacun lit chaque contact, chacun n’édite que le sien » est cette distinction et rien d’autre — et RowsForDeleting réduit la destruction encore plus, car être autorisé à corriger un enregistrement n’est pas la même chose que pouvoir le détruire. Chacun revient à celui au-dessus, donc un enregistrement qui ne fait aucune distinction ne fait aucune distinction. Les créations ne sont pas affectées : il n’y a pas encore de ligne à filtrer.
builder.Services.AddTableAccess("contact", t => t
.ForSignedInUsers()
.AllRows() // reads: every contact
.RowsForWriting(r => r.OwnRecordOnly()) // updates and deletes: their own row
.Allow(TableSecurityPermission.Read | TableSecurityPermission.Write)
.ReadingAllColumns());
Quand les lunettes nommées ne s’ajustent pas
RowsMatching Prend un filtre à vous sans quitter l’enregistrement. Donnez-lui FetchXML — un <filter> élément ou un tout <fetch> dont les filtres et jointures sont copiés, avec {currentUserId} pour le contact connecté — ou un rappel qui s’exécute par requête et renvoie soit le FetchXML, composé à partir de la réponse de vos services, soit un filtre qu’il a construit. Les deux reçoivent la même TableQueryContext chose qu’un fournisseur, ils savent donc qui demande et si les lignes vont être lues, mises à jour ou supprimées. Ne rien renvoyer ne donne aucune ligne : TableQueryFilter.AllRows c’est ainsi que vous dites chaque ligne.
.RowsMatching("<filter><condition attribute='ppp_owner' operator='eq' value='{currentUserId}' /></filter>")
.RowsMatching(async (context, services) =>
{
var territories = services.GetRequiredService<ITerritoryService>();
var filter = context.CreateFilter();
filter.AddCondition("ppp_territory", ConditionOperator.In, await territories.ForContactAsync(context.User.Id));
return filter;
})
Description de l’accès d’un rôle
À l’intérieur AddPortalRole du rôle se trouve le public, donc l’inscription ne nomme aucun autre. Les rôles ajoutent et ne soustrait jamais : l’accès d’un utilisateur correspond aux inscriptions à l’échelle du portail plus chaque rôle qu’il occupe, syndiqué. Les rôles du portail eux-mêmes — ce qu’ils sont, comment ils sont attribués, et comment ils se composent — sont la section suivante.
builder.Services.AddPortalRole("Case Manager", role => role
.WithTableAccess("incident", t => t
.RowsOfTheirAccount("customerid")
.Allow(TableSecurityPermission.Read | TableSecurityPermission.Write)
.Reading("ticketnumber", "statuscode")
.Writing("title", "description")));
Note
Une classe de fournisseur reste le moyen d’exprimer tout ce qu’une description ne peut pas, et elle compose de manière identique —
AddTablePermissionProvider<T>etWithPermissions<T>reste inchangée. Les projets générés n’en livrent plus un : chaque table qu’ils enregistrent est décrite, y compriscontactet le rôle de Gestionnaire de crédit.
Rôles du portail
Un rôle de portail est la méthode préférée pour sécuriser un portail. Il lie les fournisseurs de permissions à un nom que les administrateurs attribuent aux contacts dans Dataverse, de sorte que les fournisseurs ne s’appliquent qu’aux personnes occupant ce rôle — et qui le détient sont des données, pas du code. Un fournisseur enregistré directement Program.cs s’applique à chaque utilisateur en comparaison.
La division est délibérée. Dataverse possède les noms et les attributions, que l’administrateur gère dans l’application pilotée par modèle. Votre code possède ce que chaque nom signifie. Accorder ou révoquer l’accès de quelqu’un est alors un changement de données : pas de reconstruction, pas de redéploiement.
1. Créer le rôle et l’attribuer
Créez un enregistrement de rôle Portal et attribuez-le aux contacts qui devraient l’avoir. L’enregistrement de rôle ne contient qu’un nom et une description — aucune donnée d’autorisation ne réside dans Dataverse.
2. Lier le rôle à ses prestataires
Faites correspondre au nom du dossier de poste et indiquez ce qu’il accorde. Les types de prestataires sont enregistrés pour vous, donc vous ne les enregistrez pas vous-même.
builder.Services.AddPortalRole("Dealer", role => role
.WithPermissions<DealerAccountPermissionProvider>()
.WithPermissions<DealerOpportunityPermissionProvider>());
Ce dernier point est important : ne jamais enregistrer aussi le fournisseur d’un rôle avec AddTablePermissionProvider<T>(). Cela le place dans l’ensemble global et l’accorde à tout le monde, annulant silencieusement le rôle. Réservez AddTablePermissionProvider<T>() les règles qui s’appliquent réellement à chaque utilisateur.
Comment les rôles se combinent
Les permissions d’un utilisateur proviennent des fournisseurs enregistrés mondialement ainsi que de chaque rôle qu’il occupe, et ces groupes se rejoignent ainsi :
- À travers les rôles — additif. Si un rôle accorde une opération, c’est l’utilisateur qui l’a. Occuper plus de rôles ne peut qu’offrir plus d’access, donc un rôle ne peut jamais enlever quelque chose. Réglez
SecurityOptions.Roles.CompositionBehaviorsurRestrictivesi vous voulez que les rôles se resserrent mutuellement. - Dans un même rôle — inchangé. Plusieurs prestataires dans le même rôle se replient sous
SecurityOptions.MultiplePermissionHandlersBehavior, exactement comme les prestataires enregistrés mondialement l’ont toujours fait. - Les colonnes s’additionnent aussi. Parce que l’accès à la colonne est volontaire, un rôle qui ne mentionne jamais une colonne ne peut pas élargir ce qu’un autre rôle a publié — mais un rôle qui nomme une colonne le confère, car le nommer était un acte délibéré. En basculant
RoleCompositionBehaviorsurRestrictiveet les opérations et les colonnes se croisent à la place.
Conseil
Sous composition additive , rien ne refuse — quelque chose reste fermé uniquement pendant qu’aucun groupe ne l’ouvre. Un fournisseur enregistré mondialement est le bon foyer pour une règle qui s’applique à chaque utilisateur, car elle est toujours dans le groupe, mais ce n’est pas un veto : un rôle qui accorde l’opération, ou nomme la colonne, la rouvre. Pour garder quelque chose fermé, quels que soient les rôles qu’un utilisateur occupe, ne l’accordez aucun rôle — ou fixez-le
SecurityOptions.Roles.CompositionBehavioràRestrictive, où chaque groupe doit être d’accord.
Rôles dans les pages et la navigation
Les noms de rôles atteignent le navigateur en tant que revendications de rôle ASP.NET standard, donc l’autorisation de page et l’interface utilisateur conditionnelle fonctionnent sans rien de plus :
@attribute [Authorize(Roles = "Dealer")]
<AuthorizeView Roles="Dealer">
<p>Dealer-only content</p>
</AuthorizeView>
La pile React lit les mêmes noms depuis auth.user.roles, et <AuthorizedView roles="Dealer"> porte le contenu de la même manière. Les connexions internes (utilisateurs système) portent ici leurs noms de rôles de sécurité Dataverse, donc une seule vérification couvre les deux types d’utilisateurs.
Autoriser sur le bon type de poste
Un rôle de portail et un rôle de sécurité Dataverse peuvent partager un nom, et [Authorize(Roles = "…")] ne peuvent pas les différencier — il correspond à la revendication de rôle non typé, qui porte les deux. Deux attributs posent la question plus étroite :
@attribute [AuthorizePortalRole("Dealer")]
// L’un ou l’autre rôle est accepté.
[AuthorizePortalRole("Dealer", "Distributor")]
public class PricingController : Controller { }
// Un rôle de portail nommé « Administrateur système » ne satisfait pas cette option.
[AuthorizeSecurityRole("System Administrator")]
public class ImportController : Controller { }
Les noms des rôles vont mot pour mot : virgules, deux-points et guillemets sont tous corrects, car le nom n’est jamais extrait d’une liste. Empiler deux attributs nécessite les deux, comme les attributs empilés [Authorize] l’ont toujours fait ; lister deux noms dans un seul attribut nécessite l’un ou l’autre. La même vérification est disponible dans le code sous le nom User.IsInPortalRole("Dealer") et User.IsInSecurityRole("Sales Manager").
La même distinction dans le balisage Blazor. <AuthorizeView Roles="…"> lit la revendication de rôle non typé et porte donc la même ambiguïté que l’attribut plain ; <PortalRoleView> demande un type. Donnez-lui PortalRole / SecurityRole pour un seul nom, ou PortalRoles / SecurityRoles pour plusieurs — une collection plutôt qu’une chaîne séparée par des virgules, car les noms Dataverse contiennent régulièrement des virgules :
<PortalRoleView PortalRole="Dealer">
<a href="/pricing">Dealer pricing</a>
</PortalRoleView>
@* Un rôle de portail nommé « Administrateur système » ne l’ouvrira pas. *@
<PortalRoleView SecurityRole="System Administrator">
<BulkImport />
</PortalRoleView>
Celui de <AuthorizedView> Réaction prend les mêmes deux portes, en plus de l’existante roles . Fournir plusieurs noms dans une porte signifie n’importe lequel d’entre eux ; fournir deux portes signifie les deux. Les prédicats derrière eux — hasPortalRole, hasSecurityRole, hasRole — sont exportés pour des contrôles impératifs :
<AuthorizedView portalRoles="Dealer">
<Link to="/pricing">Dealer pricing</Link>
</AuthorizedView>
<AuthorizedView securityRoles="System Administrator">
<BulkImport />
</AuthorizedView>
Bonne pratique — filtrer à la source
Utilisez ces fonctions chaque fois que la protection est réellement administrative. Créer un rôle de portail est une barre délibérément plus basse que de se voir attribuer un rôle de sécurité Dataverse, donc un chèque qui accepte permet à un administrateur de délivrer l’accès que le rôle Dataverse protégeait simplement en nommant un rôle de portail d’après celui-ci.
Note
Les rôles ne sont pas gratuits. Chacun qu’un utilisateur détient est écrit deux fois dans le cookie d’authentification — une fois comme revendication de rôle standard, donc
[Authorize(Roles)]fonctionne, et une fois comme revendication spécifique au type qui rend possibles les vérifications ci-dessus — ce qui mesure environ 160 octets par rôle. Environ quinze rôles par utilisateur peuvent encore contenir un seul cookie ; au-delà de cela, le cookie se divise en morceaux, et la première chose à céder est généralement la limite d’en-tête d’un proxy inverse, qui rapporte un 400 opaque plutôt que de mentionner des rôles. Préfère quelques rôles généraux plutôt que de nombreux rôles étroits.
Donner aux nouveaux utilisateurs un rôle de départ
Nommer les rôles que chaque nouveau compte devrait avoir au départ, ainsi que le rôle applicable aux visiteurs déconnectés :
builder.Services.Configure<SecurityOptions>(options =>
{
options.Roles.DefaultRoleNames = ["Customer"];
options.Roles.AnonymousRoleName = "Anonymous";
});
Ou décidez-le dans Dataverse, sans publication. La table de configuration des rôles du portail associe un rôle à un mode d’attribution, et un administrateur peut changer qui reçoit quoi via l’application pilotée par modèle :
- Aucun — configuré mais en sommeil. La rangée n’accorde rien tant que quelqu’un ne change pas le mode.
- Attribuer automatiquement aux nouveaux utilisateurs — chaque compte reçoit ce rôle au fur et à mesure de sa création, à condition que la ligne de configuration et le rôle qu’elle nomme soient actifs . Désactiver l’un ou l’autre l’arrête : une ligne de configuration est une instruction permanente pour attribuer un rôle, et un rôle désactivé ne donne rien, donc une ligne active nommant un rôle inactif est une instruction sans rien derrière. La réactivation reprend le problème.
- Assigner aux nouveaux utilisateurs depuis les critères — réservé à la correspondance sur les propres données du nouvel utilisateur. Les critères ne sont pas encore évalués, donc une ligne dans ce mode n’attribue le rôle à personne plutôt qu’à tout le monde, et le portail enregistre un avertissement le nommant pour qu’il ne soit pas perçu comme une règle qui n’a exécuté et correspondu à personne.
Lorsque les rôles dépendent de la personne — son domaine email, le fournisseur avec lequel elle s’est connecté, une invitation — inscrivez-vous et IPortalRoleAssignmentProvider à la place. Chaque fournisseur enregistré est consulté, donc le vôtre s’ajoute à la liste configurée plutôt que de la remplacer. Dans tous les cas, l’attribution s’exécute une fois, lors de la création du compte ; elle n’est jamais réappliquée lors de la connexion, donc un rôle qu’un administrateur supprime reste supprimé.
Note
Un utilisateur récupère un nouveau rôle attribué en se déconnectant puis en se reconnectant — se connecter relit ses rôles. Les attributions de rôles sont autrement mises en cache par utilisateur pendant 24 heures, donc pour appliquer un changement à quelqu’un déjà connecté, il faut vider le cache des rôles du portail depuis la zone d’administration du site : sa session le récupère lors de la prochaine rafraîchissement en dix minutes, et un rôle supprimé cesse de fonctionner alors qu’un rôle ajouté commence. La table de configuration des rôles du portail est mise en cache séparément pendant quinze minutes, donc une ligne ajoutée à cet endroit atteint la prochaine inscription — videz le cache « PortalRoleConfiguration » pour le rendre immédiat. Désactiver une ligne de configuration empêche la distribution de son rôle, de la même manière que désactiver un rôle le révoque.
Écrire plutôt un prestataire
Choisissez une classe lorsqu’une description ne peut pas dire ce que vous voulez dire — une règle qui dépend de quelque chose que seul votre code connaît, ou une règle qui doit inspecter l’enregistrement en cours. ITablePermissionProvider C’est ce que chaque enregistrement devient dans tous les cas : un fournisseur gouverne une table, répond à ce qu’un utilisateur peut en faire, à quelles colonnes il peut lire et écrire, et — via le filtre de requête ci-dessous — quelles lignes il peut voir. Une inscription décrite et une écrite composent de la même maman, donc un portail peut utiliser les deux, et une table peut être accordée par l’une et élargie par l’autre.
L’interface comporte un membre obligatoire. GetPermissionsAsync reçoit un UserInfo contenant l’identifiant de l’utilisateur, ses rôles de portail et leurs rôles de sécurité Dataverse, et retourne un TablePermissions document décrivant tout ce que cet utilisateur reçoit :
public class AccountPermissionProvider : ITablePermissionProvider
{
public string Table => "account";
public Task<TablePermissions> GetPermissionsAsync(UserInfo user)
{
if (!user.IsAuthenticated)
return Task.FromResult(TablePermissions.None);
// Les colonnes sont volontaires : seul ce qui est nommé ici revient.
return Task.FromResult(TablePermissions
.Allowing(TableSecurityPermission.Read | TableSecurityPermission.Write)
.Reading("name", "telephone1", "websiteurl", "creditlimit")
.Writing("telephone1", "websiteurl"));
}
}
Enregistrement d’un prestataire
Un fournisseur enregistré de cette manière s’applique à tous les utilisateurs, connectés ou non :
builder.Services.AddTablePermissionProvider<AccountPermissionProvider>();
C’est aussi la seule façon de retirer quelque chose à tout le monde : les rôles se composent de manière additive par défaut, donc un rôle peut ajouter un accès sans jamais le supprimer. Les restrictions à l’échelle du portail appartiennent à un fournisseur enregistré globalement, qui fait toujours partie du groupe.
Opérations
TablePermissions.Operations est une TableSecurityPermission valeur de drapeaux — Read, Create, Write, Delete, et Append AppendTo. Deux choses méritent d’être connues à son sujet :
- C’est une porte au niveau de la table, pas un filtre de lignes. Accordez
Readsi l’utilisateur peut voir l’une des lignes de la table, alors réduisez lesquelles dansGetQueryFilterAsync. - Une table sans fournisseur est inaccessible. C’est le défaut fail-fermé, et cela s’applique aussi aux colonnes : un fournisseur qui n’accorde
Readmais ne nomme aucune colonne retourne des enregistrements ne contenant que leur id.
Note
Il n’y a délibérément pas de crochet « inspecter chaque ligne après son chargement ». Filtrer ainsi casse la pagination — une page de 50 en donne moins — rend le total des comptes erronés, et paie Dataverse pour récupérer les lignes qu’il jette puis jette. L’extension de la requête permet plutôt de faire en sorte que la pagination et les comptes soient corrects par construction.
Bonne pratique — filtrer à la source
Étends les données dans la requête, pas après. Le framework n’offre délibérément aucun hook qui inspecte chaque ligne une fois qu’elle a été récupérée : filtrer ainsi casse la pagination (il manque une page de 50), rend le compte total erroné, et paie Dataverse pour récupérer les lignes qu’il élimine puis rejette.
GetQueryFilterAsyncAjoute les conditions avant l’exécution de la requête, donc le pageage et les comptages restent corrects par construction et les lignes qu’un utilisateur ne voit pas ne sont jamais récupérées.
Sécurité au niveau des lignes
GetQueryFilterAsync renvoie un filtre que le framework ajoute à la requête avant que Dataverse ne l’exécute, de sorte que les lignes que l’utilisateur peut ne pas voir ne sont jamais récupérées. Il s’applique à chaque requête que le portail émet pour la table — grilles, recherches, sous-grilles, graphiques et chargements à enregistrement unique — et il s’exécute sous les deux modèles de sécurité, ce qui le reste en vigueur même lorsque la sécurité native Dataverse contourne la porte d’opération.
Construisez le filtre avec context.CreateFilter() et retournez-le ; retourner null signifie pas de restriction de ligne. Le contexte porte l’utilisateur actuel comme EntityReference, les rôles qu’il détient, et à quoi servent les lignes. Les conditions sont ajoutées via la même FetchXMLBuilder API utilisée partout ailleurs :
FetchXMLBuilder
Ce fournisseur accorde trois colonnes de contact et relie chaque requête de contact à l’enregistrement propre de l’utilisateur connecté :
public class ContactPermissionProvider : ITablePermissionProvider
{
public string Table => "contact";
public Task<TablePermissions> GetPermissionsAsync(UserInfo user)
=> Task.FromResult(TablePermissions
.Allowing(TableSecurityPermission.Read | TableSecurityPermission.Write)
.ReadingAndWriting("firstname", "lastname", "telephone1"));
public Task<TableQueryFilter?> GetQueryFilterAsync(TableQueryContext context)
{
var filter = context.CreateFilter();
filter.AddCondition("contactid", ConditionOperator.Equal, context.CurrentUser.Id);
return Task.FromResult<TableQueryFilter?>(filter);
}
}
Lecture, mise à jour et suppression
TableQueryContext.Operation Explique pourquoi les rangées sont récupérées, afin qu’un prestataire puisse les définir différemment pour chacune :
Read— une grille, une recherche, une sous-grille, un graphique ou un enregistrement chargé. Les lignes restantes sont celles que l’utilisateur peut voir.Update— la récupération pré-vol avant une sauvegarde. Les lignes restantes sont celles que l’utilisateur peut modifier, qui sont généralement un ensemble plus restreint. « Voir chaque compte, modifier uniquement le mien » est cette distinction et rien d’autre.Delete— la récupération pré-vol avant une suppression. Encore plus étroit dans la plupart des portails : être autorisé à corriger un enregistrement n’est pas la même chose que pouvoir le détruire.
public Task<TableQueryFilter?> GetQueryFilterAsync(TableQueryContext context)
{
// Tout le monde lit tout ; seules les écritures sont attribuées au propriétaire.
if (context.Operation == TableQueryOperation.Read)
return Task.FromResult<TableQueryFilter?>(TableQueryFilter.AllRows);
var filter = context.CreateFilter();
filter.AddCondition("ownerid", ConditionOperator.Equal, context.CurrentUser.Id);
return Task.FromResult<TableQueryFilter?>(filter);
}
Comment plusieurs filtres se combinent
Une table peut avoir plus d’un fournisseur — un enregistré mondialement, plus un de chaque rôle occupé par l’utilisateur. Le cadre décide comment leurs filtres se combinent, de sorte qu’un fournisseur n’a jamais à savoir ce que les autres ont fait :
- Au sein d’un même groupe, ils se croisent. Deux fournisseurs enregistrés à l’échelle mondiale, ou deux liés au même rôle, sont censés s’entendre, donc une ligne doit satisfaire les deux.
- À travers les rôles, ils s’unissent. Occuper un autre rôle élargit les rangées que vous voyez, correspondant à la composition des opérations. En basculant
RoleCompositionBehaviorversRestrictiveet ils se croisent à la place. - Les jointures sont prises en charge. Un alias qui entre en collision avec un autre déjà utilisé par la requête est renommé et les références du filtre le suivent ; lorsque des filtres sont unifiés, une jointure interne est assouplie en externe afin que la jointure d’un rôle ne puisse pas exclure les lignes qu’un autre rôle permet ; et une jointure pouvant retourner le même enregistrement plusieurs fois voit ces doublons effondrés.
Note
Les lignes échouent à se fermer, comme les colonnes : un fournisseur qui retourne
null— ou un filtre sans conditions — n’accorde aucune ligne, et au sein d’un groupe qui vide le groupe. Chaque ligne doit être demandée avecTableQueryFilter.AllRows. C’est là que réside la seule asymétrie : l’union de chaque ligne avec n’importe quel est chaque ligne, donc si un groupe qui accorde la table répondAllRows, la table revient sans filtre, aussi soigneusement que les autres rôles l’aient soigneusement délimitée. C’est ce que signifie la composition additive, mais c’est la seule façon dont le filtre d’un rôle peut sembler être ignoré.
Filtrage via une table associée
AddLink rejoint une table pour que le filtre puisse tester ses colonnes. Placez le test dans le filtre plutôt que sur le lien — les conditions à l’intérieur du filtre continuent de fonctionner lorsque le cadre assouplit la jointure vers l’extérieur, et les conditions sur le lien ne fonctionneraient pas :
public Task<TableQueryFilter?> GetQueryFilterAsync(TableQueryContext context)
{
var filter = context.CreateFilter(LogicalOperator.Or);
// L’alias est rendu unique si la requête l’utilise déjà.
filter.AddLink("ppp_membership", from: "ppp_accountid", to: "accountid", alias: "m");
filter.AddCondition("primarycontactid", ConditionOperator.Equal, context.CurrentUser.Id);
filter.AddCondition("m", "ppp_contactid", ConditionOperator.Equal, context.CurrentUser.Id);
return Task.FromResult<TableQueryFilter?>(filter);
}
Note
La jointure ci-dessus s’étend à plusieurs lignes, donc Dataverse rend le compte une fois par adhésion — une grille répétant les lignes et un total qui les compte. Le framework reconnaît cette forme et marque la requête
distinct, projetant la clé primaire de la table afin que la déduplication ne puisse supprimer que les lignes ajoutées par la jointure. Une jointure qui s’étend vers le haut , rejoignant la clé primaire de la table concernée, correspond au maximum à une ligne et reste exactement comme avant. Dans tous les cas, vous écrivez le filtre et rien d’autre — mais il est utile de savoir qu’une jointure n’est pas gratuite, et qu’une règle que vous pouvez exprimer comme des conditions plates sur les colonnes propres à la table est la forme moins chère.
Conseil
Quand une table nécessite un champ d’application différent par rôle, vous pouvez toujours écrire un fournisseur et brancher —
context.User.PortalRolesmais vous n’êtes plus obligé. Deux rôles peuvent chacun porter leur propre fournisseur et le framework réunionne ses filtres pour vous.
Qui possède un nouveau disque
Un filtre de lignes permet de lire les lectures, de mettre à jour et de supprimer, mais un mode de création n’a pas de ligne à filtrer. Ainsi, un tableau avec la portée de « lignes que je possède » permettrait toujours à un utilisateur de créer une ligne appartenant à quelqu’un d’autre — ou à personne, que son créateur ne peut alors pas voir. Déclarez la colonne qui indique qui possède un enregistrement, et le cadre le définit :
// Un compte appartient au contact qui le crée.
builder.Services.AddContactOwnership("account", "primarycontactid");
- Lors de la création , la colonne est définie comme contact connecté après les vérifications des permissions, et toute valeur envoyée par la requête est rejetée. L’utilisateur n’a pas besoin d’accès en écriture à la colonne ni
AppendTode « on »contact: lier un enregistrement à soi-même n’est pas quelque chose dont on lui fait confiance. - Lors de la mise à jour , une demande qui changerait le propriétaire est refusée, et une qui répète le propriétaire stocké est ignorée.
- Les utilisateurs internes (connexions systemuser) sont exemptés : Dataverse assure leur sécurité, et la réattribution d’un enregistrement fait partie de son administration.
Associez-le à un filtre de lignes sur la même colonne, de sorte que les enregistrements créés par un utilisateur soient ceux qu’il peut voir. Pour une propriété qui n’est pas simplement le contact créateur — par exemple le compte du créateur — implémentez-le ITableRecordOwnershipProvider et enregistrez-le avec AddTableRecordOwnershipProvider<T>(). Ne définissez pas un propriétaire dans un ITableRecordInterceptor instead : les intercepteurs s’exécutent avant les vérifications des permissions, donc ce qu’ils définissent est jugé comme l’écriture propre de l’utilisateur.
public sealed class CaseOwnershipProvider(IOrganizationServiceProvider organizationServiceProvider)
: ITableRecordOwnershipProvider
{
public string Table => "incident";
public IReadOnlyList<string> Columns { get; } = ["customerid"];
public async Task<IReadOnlyDictionary<string, LookupValue>> GetOwnersAsync(TableRecordOwnershipContext context)
{
// Lisez comme l’utilisateur de l’application : à quelle entreprise appartient quelqu’un n’est pas la question de la demande.
var contact = await organizationServiceProvider.GetOrganizationService()
.RetrieveAsync("contact", context.CurrentUser.Id, new ColumnSet("parentcustomerid"), context.CancellationToken);
var company = contact.GetAttributeValue<EntityReference>("parentcustomerid");
// Déposez le dossier au nom de leur entreprise, ou au nom d’eux quand ils n’en ont pas.
var owner = company?.LogicalName == "account"
? new LookupValue { TableName = "account", Value = company.Id }
: new LookupValue { TableName = "contact", Value = context.CurrentUser.Id };
return new Dictionary<string, LookupValue> { ["customerid"] = owner };
}
}
// Program.cs
builder.Services.AddTableRecordOwnershipProvider<CaseOwnershipProvider>();
Le framework l’appelle à chaque création de cette table, après les vérifications des permissions, et définit chaque colonne Columns dans ce qu’elle retourne — en rejetant tout ce que la requête envoyait, et en refusant une mise à jour ultérieure qui le modifierait, exactement comme pour OwnedBy. Associez-le à un filtre de ligne correspondant à ce qu’il définit, de sorte que les personnes sous lesquelles il enregistre un enregistrement soient celles qui peuvent le voir — ici RowsMatchingAny(r => r.RowsOfTheirAccount("customerid"), r => r.RowsOwnedByContact("customerid")), avec WithoutStamping() donc l’enregistrement laisse la colonne à ce fournisseur : deux qui définissent une colonne différemment échouent à la sauvegarde.
Sécurité au niveau de la colonne
La sécurité des colonnes est un consentement volontaire. Un fournisseur accorde les colonnes qu’il veut exposer, et tout le reste — y compris une colonne ajoutée à la table l’année suivante — reste retenu jusqu’à ce que ce soit nommé. L’identifiant principal du dossier est toujours lisible, car un enregistrement que le framework ne peut identifier n’est utile à personne.
Les lectures et écritures sont des ensembles indépendants : une colonne peut être lisible mais pas écrivable (le cas « afficher la limite de crédits, ne pas les laisser la changer »), ou être écrite mais non lisible. Nommez-les avec Reading, Writing et ReadingAndWriting, ou désinscrivez-vous entièrement d’une dimension avec ReadingAllColumns / WritingAllColumns:
public Task<TablePermissions> GetPermissionsAsync(UserInfo user)
{
// Chaque colonne lisible ; deux d’entre elles non écribles.
return Task.FromResult(TablePermissions
.Allowing(TableSecurityPermission.Read | TableSecurityPermission.Write)
.ReadingAllColumns()
.WritingAllColumnsExcept("creditlimit", "creditonhold"));
}
Note
ReadingAllColumns()est délibérément quelque chose que vous devez taper. Il ne s’ouvre pas pour tout ce qui est ajouté à la table ultérieurement, ce qui n’est le bon échange que lorsque la table est réellement publique — donc le cadre ne l’assumera pas en votre nom.
L’écriture peut aussi différer entre créer un enregistrement et en modifier un. Writing couvre les deux. WritingOnCreate permet à l’utilisateur de définir une colonne lors de la création de l’enregistrement et de le rendre en lecture seule à partir de là — la priorité d’un cas, choisie lors de son enregistrement. WritingOnUpdate est l’inverse : modifiable sur un enregistrement existant, non réglable au début. Les deux rendent la colonne lisible aussi, comme Writing, et les deux existent sur TablePermissions pour un fournisseur à vous. Ils imitent les permissions distinctes de création et de mise à jour accordées par la sécurité de champ de Dataverse :
builder.Services.AddTableAccess("incident", t => t
.ForSignedInUsers()
.RowsOwnedByContact("customerid")
.Allow(TableSecurityPermission.Read | TableSecurityPermission.Create | TableSecurityPermission.Write)
.Reading("ticketnumber", "statuscode")
.Writing("title", "description") // Défini lors de la création, modifiable après
.WritingOnCreate("prioritycode") // Défini lors de la création, lecture seule après
.WritingOnUpdate("resolution")); // Changeable après, non réglé au départ
Le cadre impose le résultat à chaque chemin que les données franchissent la frontière du serveur :
- Lectures — une colonne retenue est soit complètement omise de l’enregistrement, soit retournée avec sa valeur masquée, et est cachée des métadonnées des tables et des colonnes de grille, de sorte qu’un formulaire ne rend jamais un éditeur lié à une valeur qui n’arrivera jamais.
- Requêtes — une lecture n’est jamais rejetée à cause des permissions de colonne. Les vues sont régulièrement partagées entre audiences, et le fait qu’un utilisateur voie une colonne et un autre ne peut pas ne devienne pas une erreur qui vous oblige à maintenir une vue séparée par audience. Chaque référence à une colonne retenue est supprimée à la place : projections, tris, conditions de filtre (ainsi que le filtre une fois vide) et agrégations ou groupements par données. Supprimer une condition élargit l’ensemble des résultats, mais seulement sur les lignes que l’utilisateur peut déjà lire — la visibilité des lignes provient des filtres de requête des fournisseurs, ajoutés après ce passage et jamais touchés par celui-ci.
- Écritures — contrairement aux lectures, une création ou mise à jour qui modifie une colonne que l’utilisateur ne peut pas écrire est rejetée plutôt que silencieusement coupée : une sauvegarde rejetée est le seul moyen d’indiquer à l’utilisateur que sa modification n’a pas été correcte, tandis qu’une lecture restreinte est invisible et inoffensive. Une création est jugée selon ce qui peut être défini lors de la création, et une mise à jour selon ce qui peut être modifié — par une seule sauvegarde et par un lot de groupes.
Note
La colonne principale d’identification d’une table n’est jamais retenue, quoi que dise l’ensemble de lecture. L’id est l’identité plutôt que les données — il est déjà dans l’URL utilisée par l’appelant pour accéder à l’enregistrement — et les clés lignes, les liens d’édition, les relations de sous-grille et la suppression se défont sans cela. Une liste d’autorisation qui nomme les colonnes dont une page a besoin et oublie l’id démonterait autrement le portail sans rien protéger.
Comment se comportent les composants
Les composants du framework respectent les autorisations de colonne par eux-mêmes — vous ne câblez rien. Une colonne que l’utilisateur ne peut pas écrire rend en lecture seule et désactivée partout où elle apparaît : chaque éditeur la récupère via sa résolution partagée en lecture seule, et la grille ne donne pas d’éditeur en ligne à la cellule, donc elle revient au texte d’affichage brut. Une colonne fournie readOnly="false" par le consommateur ne la remplace pas — le serveur rejetterait la sauvegarde de toute façon.
Une colonne que l’utilisateur ne peut pas lire — une colonne masquée — est rendue à travers <ColumnDisplayValue> la colonne au lieu d’un éditeur, dans les deux piles. Ce n’est pas une simple question : une colonne masquée n’a aucune valeur, seulement le masque comme valeur formatée , donc un éditeur lié à elle se rendrait vide ; ColumnDisplayValue lit la valeur formatée et affiche donc le placeholder, quel que soit le type de colonne. Les colonnes retenues par omission n’atteignent jamais un composant — elles ne sont pas enregistrées dans l’enregistrement et les métadonnées du tableau ne les listent pas.
Ce que reçoit le client
Les permissions de colonne viennent sous forme de ColumnPermissions carte — nom logique de colonne vers une ColumnSecurityPermission paire de drapeaux — pour les colonnes dont l’accès est restreint, ce Write qui signifie que la colonne peut être modifiée sur un enregistrement existant. Tout ce qui est absent a un accès complet, donc une table non restreinte ne contient rien du tout. Un enregistrement récupéré seul porte la carte ; les enregistrements récupérés ensemble partagent une carte, envoyée une fois sur la réponse, puisque les permissions appartiennent à l’utilisateur et à la table plutôt qu’à une ligne — les clients des deux piles la remettent sur chaque enregistrement, donc lire les permissions d’un enregistrement fonctionne de la même manière dans les deux cas. Les réponses en grille les rapportent également par colonne sur ResolvedColumn.Permissions. Un enregistrement en création n’a pas encore de carte propre, donc le point de terminaison des permissions de table le retourne NewRecordColumnPermissions aussi — la même forme, jugée par ce qui peut être défini lors de la création — et les deux piles RecordContext le placent sur un nouvel enregistrement pour vous. Les colonnes retenues par omission sont délibérément absentes de chaque carte — l’omission existe donc rien dans la réponse ne révèle la colonne, et la nommer là annulerait cela. Lisez-la vous-même à travers record.CanReadColumn(column) / record.CanWriteColumn(column) dans Blazor, ou les correspondances canReadColumn / canWriteColumn aides dans @powerportalspro/core React.
Omission ou masquage
Par défaut, une colonne retenue est masquée : l’enregistrement ne porte aucune valeur pour elle, et SecurityOptions.WithheldColumnMask (***** par défaut) comme sa valeur formatée, donc un formulaire ou une grille affiche toujours le champ et indique clairement que la valeur est retenue plutôt que vide. Le masque est le même que la colonne ait une valeur ou non, donc il la cache aussi. Réglez SecurityOptions.WithheldColumnBehavior sur Omit pour supprimer complètement la colonne de l’enregistrement, comme si la requête ne l’avait jamais demandée, donc rien dans la réponse ne révèle que la colonne existe — le bon choix lorsque l’existence même de la colonne est sensible. Retenir une colonne retient également toute colonne qui la republie — un compagnon de nom de recherche, le jumeau de la monnaie de base d’une colonne d’argent — donc une liste de refus nommant une colonne ne peut pas être contournée par une autre. Un seul tableau peut passer outre le choix à l’échelle du site via TableColumnPermissions.WithheldColumnBehavior.
builder.Services.Configure<SecurityOptions>(options =>
{
options.WithheldColumnBehavior = WithheldColumnBehavior.Mask;
options.WithheldColumnMask = "*****";
});
Note
Associez une colonne masquée à un déni d’écriture. Une colonne masquée n’a aucune valeur sur l’enregistrement, donc un appelant qui renvoie un enregistrement chargé complet ne renvoie rien pour celui-ci — mais le framework prend une charge utile de sauvegarde à la valeur nominale, donc une requête qui définit la colonne, y compris le masque, écrit au-dessus de la valeur stockée si la colonne est écrivable. Le refus d’écriture est ce qui le refuse.
Chroniques qui republient une autre chronique
Retenir une colonne retient également toute colonne portant la même valeur sous un second nom. Dataverse crée pour vous : une recherche …name et …yominame des partenaires, une étiquette de choix, et un jumeau de monnaie de base pour chaque colonne de change — creditlimit_base à côté creditlimitde . Ce sont des colonnes ordinaires, donc une dénomination creditlimit par liste de refus ne dit rien sur creditlimit_base, et sans cela, un montant masqué reviendrait directement par son jumeau. Le lien provient des métadonnées propres de la plateforme plutôt que d’une règle de nommage, donc il s’applique aussi à vos propres colonnes, et s’applique aux lectures, à une requête nommant explicitement le compagnon, dans les métadonnées de table, dans les colonnes de grille et sur les sauvegardes.
Bonne pratique — filtrer à la source
Un cas que le framework ne peut pas voir pour vous : une colonne calculée ou regroupante, et les colonnes composites de la plateforme —
fullname,yomifullname,address1_compositeet leurs sœurs. Dataverse ne rapporte pas quelles colonnes les alimentent, donc la retenuelastnamene retient pasfullname, et une colonne calculée sur une colonne que vous avez refusée affichera toujours son résultat. Nommez-les vous-même dans le jeu de lecture lorsque leurs entrées sont sensibles. Une liste de permis (ColumnPermissions.Allow(…)) évite complètement le problème, puisque rien n’est lisible tant que vous ne l’avez pas nommé — ce qui est le meilleur choix par défaut pour une table contenant tout ce qui mérite de retenir.
Note
Lorsque plusieurs fournisseurs couvrent une même table, leurs ensembles de colonnes se composent de la même manière que leurs opérations : de manière additive entre les rôles, et sous
MultiplePermissionHandlersBehaviorle même groupe. Un fournisseur qui existe uniquement pour définir les lignes et ne dit rien sur les colonnes est complètement exclu du pliage de colonnes, ce qui ne permet ni d’élargir ni de restreindre ce que les autres ont accordé.
Dataverse-Native Security (Aperçu)
Le modèle de sécurité recommandé pour la suite permet à Dataverse lui-même d’appliquer la sécurité pour vos utilisateurs de portails, basé sur la fonctionnalité de sécurité unifiée de Power Pages . Au lieu d’écrire des gestionnaires de permissions dans le code, vous gérez l’accès avec des rôles web Power Pages et des permissions de table, que la plateforme transforme en rôles de sécurité Dataverse que Dataverse applique à chaque requête. Le modèle est actuellement en version preview — à la fois la fonctionnalité de Microsoft et le support de Power Portals Pro — et Power Portals Pro suivra le calendrier de sortie de Microsoft jusqu’à la disponibilité générale.
Une fois activé, PowerPortalsPro fournit automatiquement un système géré (C2) systemuser pour chaque contact portail — à l’enregistrement pour les nouveaux contacts, et paresseusement à la connexion pour les existants — ainsi que l’enregistrement powerpagesusermapping qui relie le contact, l’utilisateur système et votre site Power Pages. Chaque appel Dataverse pour la session se fait alors passer pour cet utilisateur système, de sorte que les rôles de sécurité que Power Pages synchronise à partir de vos rôles web et vos permissions de table régissent exactement ce que l’utilisateur peut voir et faire. La session comporte un PortalUserType de ContactStubUser afin que votre interface puisse toujours distinguer ces utilisateurs provisionnés des utilisateurs internes réels.
Note
Pour utiliser ce modèle de sécurité, vous devez créer un site web Power Pages dans votre environnement Dataverse — c’est ce qui installe les composants du cadre Microsoft et permet d’activer la fonctionnalité de sécurité unifiée. Vous n’avez pas besoin d’un forfait Power Pages payant, et vous pouvez supprimer le site web lui-même immédiatement après avoir installé et activé la fonctionnalité. L’enregistrement de site Power Pages (
powerpagesite) référencé parPowerPagesSiteIddoit exister dans l’environnement, car les mappages utilisateur et la synchronisation sécurité-rôle se résolvent contre celui-ci.
Activation de la sécurité Dataverse Native
Activez le modèle dans SecurityOptions le fichier de Program.csvotre serveur , en fournissant l’identifiant de votre site Power Pages :
builder.Services.Configure<SecurityOptions>(options =>
{
options.DataverseSecurity.ProvisionSystemUsers = true;
options.DataverseSecurity.PowerPagesSiteId = new Guid("<your powerpagesite id>");
});
La configuration est validée au démarrage de l’application : lorsqu’elle ProvisionSystemUsers est activée, PowerPagesSiteId elle doit être définie et doit se résoudre à un enregistrement existant powerpagesite , sinon l’application ne démarre pas avec des instructions.
Accordez l’accès exactement comme le fait Power Pages : configurez les rôles web et les permissions de table — dans l’application Power Pages Management, ou sur la page Web Roles de votre portail via la solution Power Portals Pro Enhanced Authorization — et attribuez des rôles web aux contacts (via l’application Management, de manière programmatique ou automatique pour les nouveaux utilisateurs avec des règles sur cette même page). La plateforme synchronise ces rôles en rôles de sécurité auto-générés (nommés PowerPages_*) et les assigne automatiquement en tant que changements d’appartenance aux rôles web — comme la configuration réside entièrement dans Power Pages, votre portail peut se déplacer vers ou depuis un site Power Pages réel sans aucune friction. Notez que les changements de rôles et de privilèges peuvent prendre de quelques secondes à quelques minutes pour se propager dans les caches de Dataverse avant de prendre effet.
Suivez le guide de configuration étape par étape
Associer une connexion externe à un contact
Lorsqu’une personne se connecte à Microsoft, Google ou à un autre fournisseur externe, le portail doit déterminer à quel contact existant — le cas échéant — cette connexion appartient. Cela résout cela en deux étapes :
- Un lien existant. Si le compte s’est déjà connecté, une
adx_externalidentityligne le relie déjà à un contact. C’est une déclaration explicite stockée « ce compte est cette personne », donc elle gagne toujours et n’est jamais affectée par les paramètres ci-dessous. - Le plan de secours par e-mail. Sans encore de lien, le portail fait correspondre l’adresse e-mail fournie par le fournisseur avec les colonnes du contact. C’est l’étape que vous pouvez configurer — et celle qui décide si un client récurrent rejoint son contact actuel ou reçoit un double.
Par défaut, deux colonnes sont recherchées, nom d’utilisateur du portail en premier. Les contacts créés via le portail portent adx_identity_username, tandis que les contacts importés ou saisis par le personnel n’ont généralement que emailaddress1 — la recherche des deux permet à un client existant de se connecter et d’accéder à son propre dossier au lieu d’un nouveau :
builder.Services.Configure<SecurityOptions>(options =>
{
// The default: portal username first, then the primary email column.
options.ContactMatching.ExternalLoginEmailColumns =
[
ContactEmailColumn.IdentityUsername,
ContactEmailColumn.EmailAddress1,
];
});
L’ordre est important : c’est la préséance, pas seulement une liste. Les colonnes sont recherchées ensemble dans une seule requête, et la première colonne de votre liste correspondant à quelque chose décide du gagnant — donc si un contact détient l’adresse comme nom d’utilisateur du portail et qu’un autre la détient, emailaddress1la correspondance du nom d’utilisateur l’emporte dans l’ordre par défaut.
Listez moins de colonnes pour affiner : [ContactEmailColumn.IdentityUsername] cela reste aux comptes créés par portail, et une liste vide désactive complètement le repli par email, laissant un lien d’identité externe existant comme seule façon pour un compte de se résoudre à un contact. Ajoutez ContactEmailColumn.EmailAddress2 et ContactEmailColumn.EmailAddress3 élargissez — ils sont délibérément exclus du système par défaut, pour la raison ci-dessous.
Bonne pratique — filtrer à la source
Chaque colonne que vous listez est un moyen pour que celui qui contrôle une adresse e-mail soit reconnu comme le contact qui la détient. Listez uniquement les colonnes que votre processus de saisie de données considère comme appartenant au contact lui-même — si
emailaddress2c’est là que votre équipe enregistre l’adresse d’un assistant ou une boîte de réception partagée de l’entreprise, l’inscrire permet à celui qui lit cette boîte de réception de se connecter sous le nom de ce contact.
Bonne pratique — filtrer à la source
La correspondance fait de l’adresse e-mail un identifiant, pas seulement une étiquette — elle décide de quel contact existant devient une connexion. Ainsi, le portail ne saute l’email de confirmation que lorsque le fournisseur d’identité a effectivement vérifié cette adresse exacte. Si l’adresse a été saisie au-dessus de celle fournie par le fournisseur, ou si le fournisseur ne l’a jamais vérifiée, un lien de confirmation est envoyé et la connexion externe n’est pas associée au contact tant que ce lien n’est pas suivi.
Les connexions qui comptent comme vérifiées ne sont pas une propriété du fournisseur. Google vérifie chaque adresse qu’il affirme. Microsoft ne le fait pas : un compte personnel Microsoft a vérifié son adresse, mais un compte Entra professionnel ou scolaire fournit ce qui est dans l’attribut de
verified_primary_emailrevendicationverified_secondary_emailou son nom la liste, ouxms_edovindique que son domaine appartient au locataire propre de l’utilisateur. Entra n’envoie ces revendications que lorsque l’enregistrement de l’application les demande (voir Configuration de connexion Entra ID) ; sans elles, les connexions professionnelles et scolaires confirment par e-mail la première fois.
Note
Ces paramètres ne s’appliquent qu’à la connexion externe. La connexion, l’enregistrement et la réinitialisation du mot de passe locaux continuent de correspondre
adx_identity_usernamede manière autonome, délibérément — le flux de réinitialisation du mot de passe envoie son lien à l’adresse saisie, donc correspondre aux colonnes d’email secondaires à cet endroit permettrait à quelqu’un de réinitialiser le mot de passe d’une autre personne.
Ne laisser se connecter que les contacts existants
Par défaut, n’importe qui peut devenir utilisateur du portail : s’inscrire ou se connecter avec un fournisseur externe sous une adresse que personne n’a crée un contact. Lorsque le portail est destiné aux personnes que vous détenez déjà dans Dataverse — clients, membres, partenaires — désactivez l’inscription, et elles seules peuvent se connecter :
builder.Services.Configure<AuthClientOptions>(options => options.SignUpEnabled = false);
La page d’inscription et ses liens disparaissent, et le serveur refuse de créer un utilisateur de la manière dont on le demande. Une connexion externe trouve toujours son contact comme décrit ci-dessus et y fait un lien ; un contact qui ne correspond à personne est refusé au lieu de créer un contact. Les comptes locaux ne sont pas affectés pour les personnes qui en ont déjà un.
Note
Un refus de connexion n’est expliqué à l’écran que lorsque le prestataire a vérifié cette adresse exacte, car alors la personne qui se connecte a déjà prouvé qu’elle en est propriétaire. Pour toute autre adresse, le portail répond exactement comme pour une adresse ayant un contact — « vérifier votre e-mail » — et envoie l’adresse par courrier avec un court préavis indiquant qu’elle n’est pas enregistrée. En informer directement le visiteur, n’importe qui devrait taper des adresses dans le formulaire pour savoir qui possède un compte.
L’avis est envoyé via l’expéditeur de l’email du portail. Un hôte qui le remplace par sa propre pile d’emails doit également enregistrer un
ISignUpRefusalEmailSender; sans un d’eux, le refus est enregistré et aucun avis n’est envoyé.
Classes de base intégrées
Le cadre fournit des classes de base pour simplifier les schémas de sécurité courants :
AnonymousPermissionHandler— accorde un tableau à tous, connecté ou non, y compris à chaque colonne. C’est le seul endroit où le défaut d’opt-in est levé, car il en découle la décision de publier le tableau. OutrepasserGetPermissionsAsyncpour publier moins.
Référence API
AuthorizePortalRoleAttribute Classe
Propriétés
Nom | Type | Par défaut | Description |
|---|---|---|---|
AuthenticationSchemes | string? | ||
Policy | string? | ||
RoleNames | IReadOnlyList<string> | Les noms de rôles que cet attribut requiert, chacun d’eux le satisfait. | |
Roles | string? | ||
TypeId | Object |
AuthenticationSchemesPolicyRoleNamesRolesTypeIdMéthodes
Nom | Paramètres | Type | Description |
|---|---|---|---|
IsDefaultAttribute | bool | ||
Match | Object obj | bool |
IsDefaultAttributeMatchITablePermissionProvider Interface
Propriétés
Nom | Type | Par défaut | Description |
|---|---|---|---|
Table | string | Nom logique du tableau auquel ce fournisseur s’applique. Correspondu exactement, donc ça doit être le Nom logique du Dataverse en minuscules. |
TableMéthodes
Nom | Paramètres | Type | Description |
|---|---|---|---|
GetPermissionsAsync | UserInfo user | Task<TablePermissions> | Que user peut faire avec ce tableau, et quelles colonnes ils peuvent lire et écrire. Retour TablePermissions.None pour un utilisateur qui ne devrait pas avoir accès. TableSecurityPermission.Read si l’utilisateur peut voir l’une des tables lignes et portée de laquelle dans Services.TableQueryContext). TableSecurityPermission.Read À lui seul n’expose que l’identifiant du disque. Utilisation Reading(System.String[]) et Writing(System.String[]) de les nommer, ou TablePermissions.AllColumns pour se désinscrire de la sécurité de colonne pour ce tableau. |
GetQueryFilterAsync | TableQueryContext context | Task<TableQueryFilter> | Étendues sur lesquelles l’utilisateur peut agir dans les lignes de cette table : un filtre construit avec Query.LogicalOperator), TableQueryFilter.AllRows pour tout Rower selon les opérations autorisées, ou nullsans lignes. null — ou un filtre sans conditions — n’accorde aucune, donc Oublier de définir un tableau la refuse plutôt que de publier chaque ligne. Disons TableQueryFilter.AllRows Quand chaque dispute est ce que tu veux dire. TableQueryContext.Operation distingue. Il s’exécute aussi sous les deux modèles de sécurité, il reste donc en vigueur lors de la sécurité native Dataverse contourne Services.UserInfo). PortalRoleOptions.CompositionBehavior à RoleCompositionBehavior.Restrictive pour croiser aussi plusieurs rôles. null ne laisse aucune ligne au groupe, et TableQueryFilter.AllRows laisse aux filtres des autres fournisseurs décider. De l’autre côté groupes, un groupe accordant TableQueryFilter.AllRows élargit l’additif par défaut Union à chaque ligne — donc le filtre d’un rôle ne signifie quelque chose que lorsque les autres groupes accordent Ce filtre de table aussi. |
GetPermissionsAsyncuser peut faire avec ce tableau, et quelles colonnes ils peuvent lire et écrire. Retour TablePermissions.None pour un utilisateur qui ne devrait pas avoir accès. TableSecurityPermission.Read si l’utilisateur peut voir l’une des tables lignes et portée de laquelle dans Services.TableQueryContext). TableSecurityPermission.Read À lui seul n’expose que l’identifiant du disque. Utilisation Reading(System.String[]) et Writing(System.String[]) de les nommer, ou TablePermissions.AllColumns pour se désinscrire de la sécurité de colonne pour ce tableau. GetQueryFilterAsyncQuery.LogicalOperator), TableQueryFilter.AllRows pour tout Rower selon les opérations autorisées, ou nullsans lignes. null — ou un filtre sans conditions — n’accorde aucune, donc Oublier de définir un tableau la refuse plutôt que de publier chaque ligne. Disons TableQueryFilter.AllRows Quand chaque dispute est ce que tu veux dire. TableQueryContext.Operation distingue. Il s’exécute aussi sous les deux modèles de sécurité, il reste donc en vigueur lors de la sécurité native Dataverse contourne Services.UserInfo). PortalRoleOptions.CompositionBehavior à RoleCompositionBehavior.Restrictive pour croiser aussi plusieurs rôles. null ne laisse aucune ligne au groupe, et TableQueryFilter.AllRows laisse aux filtres des autres fournisseurs décider. De l’autre côté groupes, un groupe accordant TableQueryFilter.AllRows élargit l’additif par défaut Union à chaque ligne — donc le filtre d’un rôle ne signifie quelque chose que lorsque les autres groupes accordent Ce filtre de table aussi. TablePermissions Classe
Propriétés
Nom | Type | Par défaut | Description |
|---|---|---|---|
Columns | TableColumnPermissions | Les colonnes que l’utilisateur peut lire et écrire. TableColumnPermissions.None sauf si Quelque chose nommé : l’accès aux colonnes est accordé, jamais supposé. | |
Operations | TableSecurityPermission | Les opérations que l’utilisateur peut effectuer sur la table, en tant que valeur de drapeau. C’est un niveau de table gate, pas un filtre de ligne — incluez-les TableSecurityPermission.Read si l’utilisateur le peut voir n’importe quelle ligne du tableau, et faire passer par lesquelles Services.TableQueryContext). |
ColumnsTableColumnPermissions.None sauf si Quelque chose nommé : l’accès aux colonnes est accordé, jamais supposé.OperationsTableSecurityPermission.Read si l’utilisateur le peut voir n’importe quelle ligne du tableau, et faire passer par lesquelles Services.TableQueryContext).Méthodes
Nom | Paramètres | Type | Description |
|---|---|---|---|
AllColumns | TablePermissions | Refuse totalement la sécurité au niveau de la colonne — chaque colonne lisible et écrivable. Le Comportement pré-sécurité de la colonne, explicité plutôt qu’hérité. | |
Allow | TableSecurityPermission operations | TablePermissions | Cela ajoute operations à ce que cette valeur apporte déjà. |
Allowing | TableSecurityPermission operations | TablePermissions | Concessions operations et pas de colonnes. Chaîne Reading(System.String[]), Writing(System.String[]) Ou TablePermissions.ReadingAllColumns pour ouvrir des colonnes. |
Reading | string[] columns | TablePermissions | Autorise la lecture des colonnes nommées, en plus de celles déjà autorisées. Insensible aux majuscules, Donc l’orthographe du portail créateur fonctionne aussi bien que le nom logique. |
ReadingAllColumns | TablePermissions | Refuse la sécurité de lecture au niveau de la colonne pour ce tableau : chaque colonne, y compris celles ajoutées à la table plus tard. Délibérément explicite — le cadre ne le supposera pas. | |
ReadingAllColumnsExcept | string[] columns | TablePermissions | Refuse la sécurité de lecture au niveau de la colonne sauf pour les colonnes nommées — chaque colonne sauf ceux-ci, y compris ceux ajoutés plus tard. Ne s’ouvre pas pour tout nouveau, donc je préfère Reading(System.String[]) sur des tables contenant des données sensibles. |
ReadingAndWriting | string[] columns | TablePermissions | Permet à la fois de lire et d’écrire les colonnes nommées. |
WithColumns | TableColumnPermissions columns | TablePermissions | Remplace les règles de colonne en gros, pour un fournisseur qui préférerait construire un Services.TableColumnPermissions lui-même que de chaîner les méthodes fluides. |
Withholding | WithheldColumnBehavior behavior | TablePermissions | Comment les colonnes retenues de ce tableau reviennent — omises ou présentes avec une valeur masquée. Outrepasse le site SecurityOptions.WithheldColumnBehavior pour ce tableau. |
Writing | string[] columns | TablePermissions | Permet d’écrire les colonnes nommées — lorsqu’un enregistrement est créé et lorsqu’il est mis à jour — dans en plus de celles déjà permises. Écrire une chronique ne signifie pas la lire — nomme-la Reading(System.String[]) Et si l’utilisateur devait voir ce qu’il a écrit. |
WritingAllColumns | TablePermissions | Refuse la sécurité d’écriture au niveau de la colonne pour ce tableau : l’utilisateur peut écrire n’importe quelle colonne, lors de la création et lors de la mise à jour. Remplace les deux ensembles d’écriture, donc appelez WritingOnCreate(System.String[]) / WritingOnUpdate(System.String[]) après, pas avant. | |
WritingAllColumnsExcept | string[] columns | TablePermissions | Refuse la sécurité d’écriture au niveau de la colonne sauf pour les colonnes nommées, qui restent en lecture seule. Remplace les deux ensembles d’écriture, donc call WritingOnCreate(System.String[]) / WritingOnUpdate(System.String[]) Après, pas avant. |
WritingOnCreate | string[] columns | TablePermissions | Permet de définir les colonnes nommées lors de la création d’un enregistrement, et seulement alors : une fois qu’il existe ils sont en lecture seule, même si un appel antérieur — Writing(System.String[]), TablePermissions.WritingAllColumns — les rendait écrivables. La priorité d’une affaire, choisie quand elle l’est enregistré et corrigé à partir de ce moment. Appelez-le après tout Writing(System.String[]), cela n’implique pas de lecture. |
WritingOnUpdate | string[] columns | TablePermissions | Permet de modifier les colonnes nommées sur un enregistrement existant, mais sans les définir quand il est créés, même si un appel antérieur les rendait écrivables — des notes de résolution qui ne signifient que quelque chose une fois que le cas existe. Appelez-le après tout Writing(System.String[]), cela n’implique pas de lecture. |
AllColumnsAllowoperations à ce que cette valeur apporte déjà.Allowingoperations et pas de colonnes. Chaîne Reading(System.String[]), Writing(System.String[]) Ou TablePermissions.ReadingAllColumns pour ouvrir des colonnes.ReadingReadingAllColumnsReadingAllColumnsExceptReading(System.String[]) sur des tables contenant des données sensibles.ReadingAndWritingWithColumnsServices.TableColumnPermissions lui-même que de chaîner les méthodes fluides.WithholdingSecurityOptions.WithheldColumnBehavior pour ce tableau.WritingReading(System.String[]) Et si l’utilisateur devait voir ce qu’il a écrit.WritingAllColumnsWritingOnCreate(System.String[]) / WritingOnUpdate(System.String[]) après, pas avant.WritingAllColumnsExceptWritingOnCreate(System.String[]) / WritingOnUpdate(System.String[]) Après, pas avant.WritingOnCreateWriting(System.String[]), TablePermissions.WritingAllColumns — les rendait écrivables. La priorité d’une affaire, choisie quand elle l’est enregistré et corrigé à partir de ce moment. Appelez-le après tout Writing(System.String[]), cela n’implique pas de lecture.WritingOnUpdateWriting(System.String[]), cela n’implique pas de lecture.AuthClientOptions Classe
Propriétés
Nom | Type | Par défaut | Description |
|---|---|---|---|
LocalAccountsEnabled | bool | True | Que le portail accepte les comptes locaux de nom d’utilisateur/mot de passe ? Par |
SignUpEnabled | bool | True | Si la connexion peut créer un utilisateur du portail. Par |
LocalAccountsEnabledSignUpEnabled