Tischsicherheit
Power Portals Pro sichert Daten auf drei Ebenen: Welche Tabellen ein Benutzer berühren kann, welche Spalten dieser Tabellen er sehen und ändern kann, und welche Zeilen überhaupt zurückkommen. Alle drei werden in einer Registrierung pro Tabelle festgelegt. Beschreiben Sie diese Registrierung mit AddTableAccess — es ist der Weg, eine Tabelle zu erteilen, und was die Projektvorlagen für jede Tabelle verwenden, die sie registrieren. Wenn eine Regel nicht beschreibbar ist, können dieselben drei Antworten stattdessen als Klasse ITablePermissionProvider geschrieben werden; beide erzeugen dasselbe und setzen sich auf dieselbe Weise zusammen. Nichts wird gewährt, bis eine von ihnen dies sagt: Eine Tabelle, die keine Registrierung abdeckt, ist unerreichbar, was der fehlerschließende Standard ist.
Tipp
Sie müssen dies nicht von einer leeren Seite schreiben. Generierte Projekte liefern eine
secure-a-tableFähigkeit: Bitten Sie Ihren Codieragenten, eine Tabelle zu sichern, und es arbeitet sich durch, welche Tabelle, für wen sie ist, für welche Zeilen, welche Operationen und welche Spalten, dann schreibt er die Registrierung und führt die entsprechenden Änderungen in Dataverse über den Power Portals Pro MCP-Server vor – erstellt die Portalrolle, wenn der Zugriff rollengebunden ist, und gewährt dem Anwendungsbenutzer die Privilegien, die die Antworten implizieren. Anschließend meldet jede Tabellecheck_setup, an die Ihre Seiten binden, für die nichts Zugriff gewährt, sowie jede Registrierung, die die Spaltensicherheit abhebt. Siehe Starten → MCP-Server.
Alle, oder nur einige Leute
Jede Registrierung gibt an, für wen sie ist, und es gibt zwei Antworten. Registrieren Sie sie im Portal, und sie gilt für ein Publikum, das Ihre Codenamen nennen – alle oder jeden angemeldeten Nutzer. Registrieren Sie sie darin AddPortalRole , und sie gilt für die Inhaber dieser Rolle, was eine Datenentscheidung ist, die ein Administrator in Dataverse trifft. Der Zugriff selbst wird in beiden Fällen gleich beschrieben.
| Registrierung | Gilt für | Um zu ändern, wer es bekommt. |
|---|---|---|
AddTableAccess("account", t => t.ForSignedInUsers()…) |
jeden, den das Publikum abdeckt, auf jede Anfrage | Code bearbeiten und neu deployen |
AddPortalRole("Credit Manager", role => role.WithTableAccess("account", …)) |
Kontakte, die diese Portal-Rolle innehaben, und sonst niemand. | die Rolle in Dataverse zuweisen oder entziehen – keine Bereitstellung |
Eine Tabelle kann einmal für jede portalweite Zielgruppe registriert werden – ForEveryone() für das, was jeder erreichen kann, für das, ForSignedInUsers() was das Anmelden hinzufügt – und die beiden summieren sich. Ein Besucher bekommt die erste; ein angemeldeter Nutzer bekommt beide, wobei jede Operation auf die Zeilen der Registrierung beschränkt ist, die sie gewährt. Mit den beiden untenstehenden Registrierungen liest jeder jeden Artikel, und ein angemeldeter Autor kann ebenfalls einen schreiben und seinen eigenen bearbeiten:
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"));
Rollen addieren; sie ziehen niemals ab. Der Zugriff eines Nutzers sind die portalweiten Registrierungen plus jede Rolle, die er innehat, vereint – also ist die portalweite die Etage, auf der alle stehen, und eine Rolle kann sie nur erhöhen. Eine Rolle kann eine Spalte oder Zeile nicht zurücknehmen, die die portalweite Registrierung gewährt hat; um den meisten Leuten etwas vorzuenthalten, gewähre es in einer Rolle statt portalweit. (Setzt SecurityOptions.Roles.CompositionBehavior stattdessen auf Restrictive "wenn alle Gruppen zustimmen.)
Also: Wenn jeder Nutzer dieser Art Zugang haben sollte, registrieren Sie es gegen das Portal. Wenn nur einige – und welche Person eine Frage ist, deren Antwort ohne Freigabe geändert wird – sie in eine Rolle einordnen. Eine Rollenregistrierung gewährt nichts, bis jemand einen Portal-Rolleneintrag mit diesem Namen erstellt und zuweist, weshalb man in einem neuen Projekt unschädlich sitzen kann.
Beschreibung des Zugangs zu einem Tisch
Die meisten Tabellen wollen dieselben wenigen Regeln: Für wen die Tabelle gedacht ist, welche ihrer Zeilen sie erreichen, was sie tun dürfen und welche Spalten sie sehen. Alle AddTableAccess vier in einer Registrierung stehen und erzeugen eine gewöhnliche ITablePermissionProvider Meldung – nichts weiter nachgelagert weiß den Unterschied. Was es kauft, ist, dass die drei Entscheidungen, die sich einigen müssen (der Filter, die Erstellen und die beschreibbaren Spalten), einmal statt dreimal getroffen werden.
builder.Services.AddTableAccess("incident", t => t
.ForSignedInUsers()
.RowsOwnedByContact("customerid")
.Allow(TableSecurityPermission.Read | TableSecurityPermission.Create)
.Reading("ticketnumber", "statuscode", "createdon")
.Writing("title", "description"));
Ein Publikum, ein Zeilenumfang und Allow(…) alles sind erforderlich. Keines davon hat einen Standard, denn jeder plausible Standard ist eine Möglichkeit, Daten zu veröffentlichen, indem man etwas weglässt – und eine Registrierung, die einen Fehler macht, scheitert beim Start und nicht in der Produktion.
Reihenteleskope
Der Scope filtert jede Abfrage für die Tabelle – Raster, Lookups, Subgrids, Diagramme und den Abruf vor einem Speicher- oder Löschen. Die benannten Scopes decken die Formen ab, die Portale ständig benötigen:
| Welche Reihen | Umfang | Auf Create |
|---|---|---|
| Jede Reihe | AllRows() |
Nichts festgesetzt |
| Die eigenen Datensätze des Nutzers | RowsOwnedByContact("customerid") |
Der Lookup ist auf sie eingestellt |
| Die Unterlagen ihrer Kollegen, auf demselben Konto | RowsOfTheirAccount("customerid") |
Die Suche ist auf ihr Konto eingestellt |
| Aufzeichnungen, die an einem Elternteil hängen, die sie erreichen können | RowsViaParent("salesorderid", "salesorder", p => …) |
Nichts festgelegt – der eigene Zugang der Eltern entscheidet |
| Aufzeichnungen, die auf mehr als eine Weise auf sie hinweisen | RowsMatchingAny(r => r.RowsOwnedByContact("customerid"), r => r.RowsOfTheirAccount("customerid")) |
Was der erste Eigentumsbereich auflistete Sets |
| Ihre eigene Kontaktreihe | OwnRecordOnly() |
Kein Erstellen |
Die Eigentumsbereiche legen die Spalte selbst fest, wenn ein Datensatz erstellt wird, nach den Berechtigungsprüfungen: Der Benutzer benötigt keinen Schreibzugriff darauf und kein Append To auf der Tabelle, auf die er zeigt, der Wert stammt aus der Sitzung und nicht aus der Anfrage, und ein späterer Speicher, der ihn ändern würde, wird abgelehnt. Das ist es, was einen Datensatz, den jemand erstellt, im Filter speichert, dem er unterliegt. Verwenden OwnedBy('column') Sie, wenn die besitzende Spalte nicht diejenige ist, nach der die Zeilen gefiltert sind, und WithoutStamping() wenn ein Plugin sie bereits setzt.
Beim Lesen und Ändern unterscheiden sich
One Scope beantwortet jede Operation, es sei denn, du sagst etwas anderes. RowsForWriting schränkt die Zeilen ein, die ein Benutzer ändern kann, ohne die Zeilen einzugrenzen – "Jeder liest jeden Kontakt, jeder bearbeitet nur seine eigene" ist diese Unterscheidung und nichts anderes – und RowsForDeleting schränkt das Zerstören noch weiter ein, weil die Erlaubnis, einen Datensatz zu korrigieren, nicht dasselbe ist wie ihn zu vernichten. Jeder fällt auf die darüberliegende zurück, sodass eine Registrierung, die keinen Unterschied macht, keine zieht. Erstellt bleiben unbeeinträchtigt: Es gibt noch keine Zeile zum Filtern.
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());
Wenn die benannten Zielfernrohre nicht passen
RowsMatching nimmt einen eigenen Filter, ohne die Registrierung zu verlassen. Gib ihm FetchXML – ein <filter> Element oder ein Ganze, <fetch> dessen Filter und Joins kopiert werden, mit {currentUserId} für den angemeldeten Kontakt – oder einen Callback, der pro Anfrage ausgeführt wird und entweder das FetchXML zurückgibt, das aus der Antwort deiner Dienste zusammengesetzt ist, oder einen Filter, den er gebaut hat. Beides erhält dasselbe TableQueryContext wie ein Anbieter, sodass er weiß, wer fragt und ob die Zeilen gelesen, aktualisiert oder gelöscht werden. Nichts zurückzugeben gewährt keine Zeilen: TableQueryFilter.AllRows so sagt man jede Zeile.
.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;
})
Beschreibung des Zugangs zu einer Rolle
Innerhalb AddPortalRole der Rolle befindet sich das Publikum, also nennt die Registrierung keine anderen. Rollen addieren und ziehen niemals ab: Der Zugriff eines Nutzers sind die portalweiten Registrierungen plus jede Rolle, die er innehat, gewerkschaftlich organisiert. Die Portalrollen selbst – was sie sind, wie sie zugewiesen werden und wie sie zusammengesetzt sind – sind der nächste Abschnitt.
builder.Services.AddPortalRole("Case Manager", role => role
.WithTableAccess("incident", t => t
.RowsOfTheirAccount("customerid")
.Allow(TableSecurityPermission.Read | TableSecurityPermission.Write)
.Reading("ticketnumber", "statuscode")
.Writing("title", "description")));
Anmerkung
Eine Anbieterklasse bleibt der Weg, alles auszudrücken, was eine Beschreibung nicht ausdrücken kann, und sie setzt sich identisch zusammen –
AddTablePermissionProvider<T>undWithPermissions<T>bleibt unverändert. Die generierten Projekte liefern keine weitere aus: Jede Tabelle, die sie registriert, wird beschrieben, einschließlichcontactder Rolle des Kreditmanagers.
Portalrollen
Eine Portalrolle ist die bevorzugte Möglichkeit, ein Portal zu sichern. Sie bindet Berechtigungsanbieter an einen Namen, den Administratoren Kontakte in Dataverse zuweisen, sodass die Anbieter nur für die Personen gelten, die diese Rolle innehaben – und wer sie innehat, ist Daten, kein Code. Ein direkt Program.cs registrierter Anbieter gilt im Vergleich für jeden Nutzer.
Die Aufteilung ist absichtlich. Dataverse besitzt die Namen und die Zuweisungen, die ein Administrator in der modellgesteuerten App verwaltet. Dein Code besitzt, was jeder Name bedeutet. Das Gewähren oder Widerrufen des Zugriffs ist dann eine Datenänderung: kein Rebuild, kein Redeploy.
1. Erstelle die Rolle und verweise sie
Erstelle einen Portal-Rolleneintrag und weise ihn den Kontakten zu, die ihn haben sollten. Der Rolleneintrag enthält nichts außer einem Namen und einer Beschreibung – in Dataverse existieren keine Berechtigungsdaten.
2. Die Rolle an ihre Anbieter zu binden
Gleiche den Namen des Rollendatensatzes ab und liste auf, was er vergibt. Die Anbieter sind für dich registriert, also registrierst du sie nicht selbst.
builder.Services.AddPortalRole("Dealer", role => role
.WithPermissions<DealerAccountPermissionProvider>()
.WithPermissions<DealerOpportunityPermissionProvider>());
Dieser letzte Punkt ist wichtig: Registrieren Sie niemals auch den Anbieter einer Rolle bei AddTablePermissionProvider<T>(). Das legt sie in die globale Menge und gewährt sie allen, wodurch die Rolle stillschweigend verworfen wird. Reservieren AddTablePermissionProvider<T>() Sie Regeln für Regeln, die wirklich für jeden Nutzer gelten.
Wie sich Rollen kombinieren
Die Berechtigungen eines Nutzers stammen von den global registrierten Anbietern plus jeder Rolle, die er innehat, und diese Gruppen fügen sich wie folgt zusammen:
- Rollenübergreifend – additiv. Wenn eine Rolle eine Operation gewährt, hat der Nutzer sie. Mehr Rollen zu besitzen kann nur mehr Zugang geben, also kann eine Rolle nie etwas wegnehmen. Stelle
SecurityOptions.Roles.CompositionBehaviorstattdessen ein,Restrictivewenn du möchtest, dass sich Rollen gegenseitig eingrenzen. - Innerhalb einer Rolle – unverändert. Mehrere Anbieter in derselben Rolle fallen unter
SecurityOptions.MultiplePermissionHandlersBehavior, genau wie global registrierte Anbieter es immer tun. - Auch die Spalten summieren sich. Da der Spaltenzugriff opt-in ist, kann eine Rolle, die eine Spalte nie erwähnt, nicht erweitern, was eine andere Rolle veröffentlicht hat – aber eine Rolle, die eine Spalte benannt , gewährt dies, weil die Benennung eine bewusste Aktion war. Wechselt
RoleCompositionBehaviorman zuRestrictiveund schneiden sich stattdessen sowohl Operationen als auch Spalten.
Tipp
Bei additiver Komposition verneint nichts – etwas bleibt nur geschlossen, solange keine Gruppe es öffnet. Ein global registrierter Anbieter ist die richtige Heimat für eine Regel, die für jeden Nutzer gilt, weil sie immer im Fold ist, aber kein Veto: Eine Rolle, die die Operation gewährt oder die Spalte benennt, öffnet sie wieder. Um etwas geschlossen zu halten, welche Rollen ein Nutzer auch immer innehat, gewähren Sie es in keiner Rolle – oder setzen
SecurityOptions.Roles.CompositionBehaviorSie aufRestrictive, wo jede Gruppe zustimmen muss.
Rollen in Seiten und Navigation
Rollennamen gelangen als Standard-ASP.NET Rollenbehauptungen in den Browser, sodass Seitenautorisierung und bedingte Benutzeroberfläche ohne zusätzliche Funktionen funktionieren:
@attribute [Authorize(Roles = "Dealer")]
<AuthorizeView Roles="Dealer">
<p>Dealer-only content</p>
</AuthorizeView>
Der React-Stack liest dieselben Namen aus auth.user.rolesund <AuthorizedView roles="Dealer"> sperrt Inhalte auf die gleiche Weise. Interne (Systembenutzer-)Anmeldungen tragen stattdessen ihre Dataverse-Sicherheitsrollennamen hierher, sodass eine Überprüfung beide Arten von Benutzern abdeckt.
Genehmigung für die richtige Art von Rolle
Eine Portalrolle und eine Dataverse-Sicherheitsrolle können einen Namen teilen und [Authorize(Roles = "…")] können sie nicht voneinander unterscheiden – das entspricht der Untyped-Rollenbehauptung, die beide trägt. Zwei Attribute stellen die engere Frage:
@attribute [AuthorizePortalRole("Dealer")]
// Beide Rollen werden aufgenommen.
[AuthorizePortalRole("Dealer", "Distributor")]
public class PricingController : Controller { }
// Eine Portalrolle namens "Systemadministrator" erfüllt dies nicht.
[AuthorizeSecurityRole("System Administrator")]
public class ImportController : Controller { }
Rollennamen werden wortwörtlich angegeben: Kommas, Kolon und Anführungszeichen sind alle in Ordnung, weil der Name nie aus einer Liste herausgeschnitten wird. Das Stapeln zweier Attribute erfordert beide, wie es gestapelte [Authorize] Attribute immer getan haben; die Auflistung zweier Namen in einem Attribut erfordert beides. Der gleiche Check ist im Code User.IsInPortalRole("Dealer") als und User.IsInSecurityRole("Sales Manager")verfügbar.
Die gleiche Unterscheidung im Blazor-Markup. <AuthorizeView Roles="…"> liest die untypisierte Rollenbehauptung und trägt daher dieselbe Mehrdeutigkeit wie das einfache Attribut; <PortalRoleView> fragt nach einer Art. Gib / PortalRole SecurityRole für einen einzelnen Namen oder PortalRoles / SecurityRoles für mehrere – eine Sammlung statt eines kommagetrennten Strings, da Dataverse-Namen routinemäßig Kommas enthalten:
<PortalRoleView PortalRole="Dealer">
<a href="/pricing">Dealer pricing</a>
</PortalRoleView>
@* Eine Portalrolle namens "Systemadministrator" öffnet dies nicht. *@
<PortalRoleView SecurityRole="System Administrator">
<BulkImport />
</PortalRoleView>
React nimmt <AuthorizedView> dieselben zwei Gates, neben dem bestehenden roles . Mehrere Namen in einem Gate anzugeben bedeutet jeden von ihnen; zwei Gates zu liefern bedeutet beide. Die dahinterstehenden Prädikate — hasPortalRole, hasSecurityRole, — hasRole werden für imperative Prüfungen exportiert:
<AuthorizedView portalRoles="Dealer">
<Link to="/pricing">Dealer pricing</Link>
</AuthorizedView>
<AuthorizedView securityRoles="System Administrator">
<BulkImport />
</AuthorizedView>
Best Practice – an der Quelle filtern
Greifen Sie zu diesen, wann immer das zu schützende Objekt wirklich administrativ ist. Eine Portalrolle zu erstellen ist bewusst niedriger, als eine Dataverse-Sicherheitsrolle zu erhalten, daher erlaubt eine Kontrolle, die beide akzeptiert, einem Administrator Zugang zu der geschützten Dataverse-Rolle zu gewähren, indem er einfach eine Portalrolle nach ihr benannt.
Anmerkung
Rollen sind nicht kostenlos. Jede Rolle, die ein Nutzer hält, wird zweimal in das Auth-Cookie geschrieben – einmal als Standard-Rollenbehauptung, also
[Authorize(Roles)]funktioniert, und einmal als die artspezifische Behauptung, die die oben genannten Überprüfungen möglich macht – was etwa 160 Bytes pro Rolle beträgt. Etwa fünfzehn Rollen pro Nutzer passen immer noch in einen einzigen Cookie; darüber hinaus teilt sich der Cookie in Chunks, und das Erste, was nachgibt, ist meist das Header-Limit eines Reverse-Proxys, das eine undurchsichtige 400 angibt, anstatt dass Rollen erwähnt werden. Bevorzuge einige breite Rollen gegenüber vielen engen.
Neuen Nutzern eine Startrolle geben
Nenne die Rollen, mit denen jedes neue Konto beginnen sollte, und die Rolle, die für ausgeloggte Besucher gilt:
builder.Services.Configure<SecurityOptions>(options =>
{
options.Roles.DefaultRoleNames = ["Customer"];
options.Roles.AnonymousRoleName = "Anonymous";
});
Oder entscheide es in Dataverse, ohne eine Veröffentlichung. Die Portal-Rollenkonfigurationstabelle koppelt eine Rolle mit einem Zuweisungsmodus, und ein Administrator kann ändern, wer was aus der modellgesteuerten App erhält:
- Keine – konfiguriert, aber ruhend. Die Reihe gewährt nichts, bis jemand den Modus ändert.
- Automatisch neuen Benutzern zuweisen – jedes Konto erhält diese Rolle, sobald sie erstellt wird, vorausgesetzt, sowohl die Konfigurationszeile als auch die von ihr benannte Rolle sind aktiv. Das Deaktivieren einer der beiden Rollen stoppt sie: Eine Konfigurationszeile ist eine stehende Anweisung, eine Rolle zu vergeben, und eine deaktivierte Rolle gewährt nichts, sodass eine aktive Zeile, die eine inaktive Rolle benennt, eine Anweisung ohne Hintergrund ist. Die Reaktivierung setzt sie fort.
- Neuen Benutzern von Kriterien zuweisen – reserviert für das Matching mit den eigenen Daten des neuen Nutzers. Die Kriterien sind noch nicht bewertet, daher weist eine Zeile in diesem Modus die Rolle niemandem statt allen zu, und das Portal meldet eine Warnung mit dem Namen, damit es nicht als Regel gelesen wird, die läuft und niemandem entspricht.
Wenn die Rollen von der Person abhängen – deren E-Mail-Domain, der Anbieter, bei dem er sich angemeldet hat, eine Einladung – registrieren Sie sich IPortalRoleAssignmentProvider stattdessen. Jeder registrierte Anbieter wird konsultiert, sodass Ihr Provider zur konfigurierten Liste hinzugefügt wird, anstatt sie zu ersetzen. So oder so wird die Zuweisung einmal durchgeführt, wenn das Konto erstellt wird; sie wird beim Anmelden nie erneut angewendet, sodass eine Rolle, die ein Administrator entfernt, entfernt bleibt.
Anmerkung
Ein Nutzer nimmt eine neu zugewiesene Rolle durch Ab- und Wiedereinloggen auf – das Anmelden liest seine Rollen erneut. Rollenzuweisungen werden ansonsten pro Benutzer für 24 Stunden zwischengespeichert, um also eine Änderung auf jemanden anzuwenden, der bereits angemeldet ist, den Portal-Rollen-Cache im Standortverwaltungsbereich zu löschen: Ihre Sitzung erkennt ihn bei der nächsten Aktualisierung innerhalb von zehn Minuten wieder auf, und eine entfernte Rolle funktioniert dann nicht mehr, sobald eine hinzugefügte startet. Die Portal-Rollen-Konfigurationstabelle wird für fünfzehn Minuten separat zwischengespeichert, sodass eine dort hinzugefügte Zeile die nächste Registrierung von selbst erreicht – löschen Sie den "PortalRoleConfiguration"-Cache, um sie sofort zu ermöglichen. Das Deaktivieren einer Konfigurationszeile verhindert, dass sie ihre Rolle vergibt, genauso wie das Deaktivieren einer Rolle sie widerruft.
Stattdessen einen Anbieter schreiben
Greifen Sie zu einer Klasse, wenn eine Beschreibung nicht sagen kann, was Sie meinen – eine Regel, die von etwas abhängt, das nur Ihr Code kennt, oder eine, die den Datensatz während des Flugs überprüfen muss. ITablePermissionProvider Ist in jedem Fall das, was jede Registrierung wird: Ein Anbieter steuert eine Tabelle, beantwortet, was ein Nutzer damit machen darf, welche seiner Spalten er lesen und schreiben darf, und – über den untenstehenden Abfragefilter – welche seiner Zeilen er überhaupt sehen kann. Eine beschriebene und eine geschriebene Registrierung bilden identisch, sodass ein Portal beide verwenden kann, und eine Tabelle kann von der einen gewährt und von der anderen erweitert werden.
Die Schnittstelle hat ein erforderliches Mitglied. GetPermissionsAsync Erhält ein UserInfo Formular, das die ID des Benutzers, seine Portalrollen und seine Dataverse-Sicherheitsrollen enthält, und liefert eine TablePermissions Beschreibung, die alles beschreibt, was der Nutzer erhält:
public class AccountPermissionProvider : ITablePermissionProvider
{
public string Table => "account";
public Task<TablePermissions> GetPermissionsAsync(UserInfo user)
{
if (!user.IsAuthenticated)
return Task.FromResult(TablePermissions.None);
// Die Spalten sind opt-in: Nur das, was hier genannt wird, kommt zurück.
return Task.FromResult(TablePermissions
.Allowing(TableSecurityPermission.Read | TableSecurityPermission.Write)
.Reading("name", "telephone1", "websiteurl", "creditlimit")
.Writing("telephone1", "websiteurl"));
}
}
Registrierung eines Anbieters
Ein auf diese Weise registrierter Anbieter gilt für jeden Nutzer, ob angemeldet oder nicht:
builder.Services.AddTablePermissionProvider<AccountPermissionProvider>();
Das ist auch die einzige Möglichkeit, allen etwas wegzunehmen: Rollen setzen standardmäßig additiv zusammen, sodass eine Rolle Zugriff hinzufügen, aber nie entfernen kann. Portalweite Einschränkungen gehören zu einem global registrierten Anbieter, der immer Teil des Pakets ist.
Betrieb
TablePermissions.Operationsist ein TableSecurityPermission Flaggenwert — Read, , Create, WriteDelete, und Append AppendTo. Zwei Dinge sind darüber bekannt:
- Es handelt sich um ein Tabellen-Gatter, kein Zeilenfilter. Wenn
Readder Benutzer eine der Zeilen der Tabelle sehen darf, dann schränke ein, welche inGetQueryFilterAsync. - Eine Tabelle ohne Anbieter ist unerreichbar. Das ist der Standardwert "fail-closed" und gilt auch für Spalten: Ein Anbieter, der gewährt
Read, aber keine Spalten nennt, liefert Datensätze zurück, die nur ihre ID enthalten.
Anmerkung
Es gibt absichtlich keinen "Inspiziere jede Zeile nach dem Laden"-Hook. So zu filtern zerstört das Ausgreifen – eine Seite mit 50 bringt weniger zurück – ergibt die Gesamtzählung falsch und bezahlt Dataverse, um Zeilen abzurufen, die es dann wegwirft. Das Scoping der Abfrage hält stattdessen das Ausziehen und zählt durch Konstruktion korrekt.
Best Practice – an der Quelle filtern
Daten in der Abfrage scopen, nicht danach. Das Framework bietet absichtlich keinen Hook, der jede Zeile überprüft, sobald sie abgerufen wurde: Das Filtern auf diese Weise unterbricht die Ausrichtung (eine Seite mit 50 kommt zu kurz an), macht die Gesamtzählung falsch und bezahlt Dataverse dafür, Zeilen abzurufen und dann zu verwerfen.
GetQueryFilterAsyncFügt die Bedingungen vor der Abfrage hinzu, sodass Paging- und Zählzahlen durch die Konstruktion korrekt bleiben und die Zeilen, die ein Benutzer möglicherweise nicht sieht, überhaupt nicht abgerufen werden.
Sicherheit auf Reihenebene
GetQueryFilterAsync gibt einen Filter zurück, den das Framework der Abfrage hinzugefügt hat, bevor Dataverse ihn ausführt, sodass Zeilen, die der Benutzer möglicherweise nicht sieht, nie abgerufen werden. Er gilt für jede Abfrage, die das Portal für die Tabelle ausstellt – Raster, Nachschlage, Subgrids, Diagramme und Einzeldatensatz-Loads – und läuft unter beiden Sicherheitsmodellen, sodass er auch dort gilt, wo Dataverse-native Sicherheit das Operationsgate umgeht.
Baue den Filter mit context.CreateFilter() und gib ihn zurück; zurückgeben null bedeutet keine Zeilenbeschränkung. Der Kontext trägt den aktuellen Benutzer als , EntityReferencedie Rollen, die er innehat, und wofür die Zeilen gleich verwendet werden sollen. Bedingungen werden über dieselbe FetchXMLBuilder API hinzugefügt, wie überall sonst:
FetchXMLBuilder
Dieser Anbieter gewährt drei Spalten und contact beschränkt jede Kontaktanfrage auf den eigenen Datensatz des angemeldeten Nutzers:
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);
}
}
Lesen, aktualisieren und löschen
TableQueryContext.Operation erklärt, warum die Reihen abgerufen werden, damit ein Anbieter sie für jede unterschiedlich bewerten kann:
Read— eine Raster-, Suche-, Untergitter-, Diagramm- oder Datensatzlast. Die verbleibenden Zeilen sind die Zeilen, die der Benutzer sehen kann.Update— den Pre-Flight-Abruf vor einem Speicherstand. Die verbleibenden Zeilen sind die Zeilen, die der Benutzer ändern kann, was meist eine enger Menge ist. "Sehen Sie jedes Konto, bearbeiten nur meins" ist diese Unterscheidung und nichts anderes.Delete– der Pre-Flight-Abruf vor einem Löschen. In den meisten Portalen ist es wieder enger geworden: Einen Datensatz korrigieren zu dürfen ist nicht dasselbe, wie ihn zerstören zu dürfen.
public Task<TableQueryFilter?> GetQueryFilterAsync(TableQueryContext context)
{
// Jeder liest alles; nur die Schreibe sind auf den Besitzer zugeschnitten.
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);
}
Wie mehrere Filter kombiniert werden
Eine Tabelle kann mehr als einen Anbieter haben – einen global registrierten plus einen aus jeder Rolle, die der Nutzer innehat. Das Framework entscheidet, wie sich ihre Filter kombinieren, sodass ein Anbieter nie wissen muss, was die anderen gemacht haben:
- Innerhalb einer Gruppe überschneiden sie sich. Zwei global registrierte Anbieter oder zwei an dieselbe Rolle gebundene Anbieter sollen sich einigen, daher muss eine Reihe beide erfüllen.
- Über die Rollen hinweg vereinen sie sich. Wenn du eine andere Rolle hältst, werden die Zeilen erweitert, die du siehst, und passt so an, wie die Operationen komponiert sind. Wechsle
RoleCompositionBehaviorzuRestrictiveund sie schneiden sich stattdessen. - Joins werden betreut. Ein Alias, der mit einem von der Abfrage bereits verwendeten Alias kollidiert, wird umbenannt und die Filterreferenzen folgen ihm; wenn Filter vereinigt werden, wird ein innerer Join auf äußere Links gelockert, sodass der Join einer Rolle die Zeilen einer anderen Rolle nicht ausschließen kann; und ein Join, der denselben Datensatz mehrmals zurückgeben kann, hat diese Duplikate kollabiert.
Anmerkung
Zeilen versagen geschlossen, wie Spalten: Ein Provider, der zurückgibt
null– oder ein Filter ohne Bedingungen – gewährt keine Zeilen, und innerhalb einer Gruppe leert er die Gruppe. Jede Zeile muss mitTableQueryFilter.AllRowsabgefragt werden. Dort liegt die eine Asymmetrie: Die Vereinigung jeder Zeile mit irgendetwas ist jede Zeile, also wenn eine Gruppe, die der Tabelle AntwortenAllRowsgibt, kommt die Tabelle ungefiltert zurück, egal wie sorgfältig die anderen Rollen sie begrenzt haben. Das bedeutet additive Komposition, aber es ist die einzige Möglichkeit, wie der Filter einer Rolle so aussehen kann, als würde er ignoriert.
Filtern durch eine verwandte Tabelle
AddLink fügt eine Tabelle ein, damit der Filter seine Spalten testen kann. Setzen Sie den Test im Filter statt auf den Link – Bedingungen im Filter funktionieren weiterhin, wenn das Framework das Join nach außen lockert, und Bedingungen im Link würden nicht:
public Task<TableQueryFilter?> GetQueryFilterAsync(TableQueryContext context)
{
var filter = context.CreateFilter(LogicalOperator.Or);
// Das Alias wird eindeutig gemacht, wenn die Abfrage es bereits verwendet.
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);
}
Anmerkung
Der obige Join reicht bis zu vielen Zeilen, sodass Dataverse das Konto einmal pro Mitgliedschaft zurückgibt – ein Raster mit wiederholten Zeilen und eine Gesamtsumme, die sie zählt. Das Framework erkennt diese Form und markiert die Abfrage
distinct, projiziert den Primärschlüssel der Tabelle, sodass die Deduplizierung nur die Zeilen entfernen kann, die der hinzugefügte Join hinzugefügt hat. Ein Join, der stattdessen nach oben geht und mit dem eigenen Primärschlüssel der zugehörigen Tabelle verbunden ist, entspricht höchstens einer Zeile und bleibt genau so, wie er war. So oder so schreibst du den Filter und sonst nichts – aber es ist wichtig zu wissen, dass ein Join nicht kostenlos ist und dass eine Regel, die du als flache Bedingungen in den eigenen Spalten der Tabelle ausdrücken kannst, die günstigere Form ist.
Tipp
Wenn eine Tabelle pro Rolle einen anderen Umfang benötigt, kannst du immer noch einen Anbieter schreiben und darauf
context.User.PortalRolesverzweigen – aber das musst du nicht mehr. Zwei Rollen können jeweils ihren eigenen Anbieter haben und das Framework vereint ihre Filter für dich.
Wer besitzt einen neuen Rekord
Ein Zeilenfilter steuert Scopes, Reads, Updates und Löschungen, aber ein Create hat keine Zeile zum Filtern. Eine Tabelle, die auf "Zeilen, die ich besitze" beschränkt ist, würde es einem Benutzer immer noch erlauben, eine Zeile zu erstellen, die jemand anderem gehört – oder niemandem gehört, die der Ersteller dann nicht sehen kann. Deklariere die Spalte, in der steht, wem ein Datensatz gehört, und das Framework setzt sie:
// Ein Konto gehört dem Kontakt, der es erstellt.
builder.Services.AddContactOwnership("account", "primarycontactid");
- Beim Erstellen wird die Spalte nach der Berechtigungsprüfung auf den angemeldeten Kontakt gesetzt, und jeder von der Anfrage gesendete Wert wird verworfen. Der Benutzer benötigt keinen Schreibzugriff auf die Spalte und kein
AppendToEin:contactEinen Datensatz mit sich selbst zu verknüpfen, wird ihm nicht anvertraut. - Beim Update wird eine Anfrage, die den Besitzer ändern würde, abgelehnt, und eine, die den gespeicherten Besitzer wiederholt, wird ignoriert.
- Interne Benutzer (Systemuser-Anmeldungen) sind ausgenommen: Dataverse erzwingt deren Sicherheit, und die Neuzuweisung eines Datensatzes gehört zur Verwaltung dazu.
Koppele es mit einem Zeilenfilter in derselben Spalte, sodass die Datensätze, die ein Benutzer erstellt, die einsehen können. Für Eigentum, das nicht nur der erstellende Kontakt ist – zum Beispiel das Konto des Erstellers – implementiere ITableRecordOwnershipProvider und registriere es bei AddTableRecordOwnershipProvider<T>(). Setze keinen Besitzer in ein: ITableRecordInterceptor Interceptoren laufen vor den Berechtigungsprüfungen, sodass das, was sie setzen, als eigener Schreib des Nutzers bewertet wird.
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)
{
// Lesen Sie es als Anwendungsnutzer: Zu welcher Firma jemand gehört, ist nicht die Anforderung zu sagen.
var contact = await organizationServiceProvider.GetOrganizationService()
.RetrieveAsync("contact", context.CurrentUser.Id, new ColumnSet("parentcustomerid"), context.CancellationToken);
var company = contact.GetAttributeValue<EntityReference>("parentcustomerid");
// Reichen Sie die Klage bei deren Firma ein oder unter ihnen, wenn sie keine haben.
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>();
Das Framework ruft es bei jeder Erstellung dieser Tabelle nach den Berechtigungsprüfungen auf und setzt jede Spalte Columns in das, was es zurückgibt – verwirft alles, was die Anfrage gesendet hat, und lehnt ein späteres Update ab, das sie ändern würde, genau wie bei OwnedBy. Koppele es mit einem Zeilenfilter, der dem entspricht, was er setzt, sodass die Personen, unter denen ein Datensatz abgelegt wird, diejenigen sind, die ihn sehen können – hier RowsMatchingAny(r => r.RowsOfTheirAccount("customerid"), r => r.RowsOwnedByContact("customerid")), sodass WithoutStamping() die Registrierung die Spalte diesem Anbieter überlässt: Zwei, die eine Spalte anders setzen, scheitern beim Speichern.
Sicherheit auf Säulenebene
Die Spaltensicherheit ist opt-in. Ein Anbieter gewährt die Spalten, die er offenlegen möchte, und alles andere – einschließlich einer Spalte, die im nächsten Jahr zur Tabelle hinzugefügt wird – bleibt zurückgehalten, bis benannt wird. Die primäre ID des Datensatzes ist immer lesbar, da ein Datensatz, den das Framework nicht identifizieren kann, für niemanden nützlich ist.
Reads und Writes sind unabhängige Mengen: Eine Spalte kann lesbar, aber nicht beschreibbar sein (der Fall "Zeig das Kreditlimit, lass sie es nicht ändern"), oder beschreibbar, aber nicht lesbar. Benennen Sie sie mit Reading, Writing und ReadingAndWriting, oder wählen Sie eine Dimension vollständig mit ReadingAllColumns / WritingAllColumnsab:
public Task<TablePermissions> GetPermissionsAsync(UserInfo user)
{
// Jede Spalte lesbar; zwei davon nicht beschreibbar.
return Task.FromResult(TablePermissions
.Allowing(TableSecurityPermission.Read | TableSecurityPermission.Write)
.ReadingAllColumns()
.WritingAllColumnsExcept("creditlimit", "creditonhold"));
}
Anmerkung
ReadingAllColumns()ist absichtlich etwas, das man tippen muss. Es ist für alles, was später hinzugefügt wird, nicht geöffnet, was nur dann der richtige Handel ist, wenn der Tisch wirklich öffentlich ist – also wird das System es nicht für dich übernehmen.
Das Schreiben kann sich auch zwischen dem Erstellen eines Datensatzes und dessen Änderung unterscheiden. Writing Deckt beides ab. WritingOnCreate Ermöglicht es dem Benutzer, eine Spalte beim Erstellen des Datensatzes festzulegen und sie von da an nur lesegeschützt zu machen – die Priorität eines Falls, die beim Protokollieren gewählt wird. WritingOnUpdate ist das Gegenteil: auf einem bestehenden Datensatz änderbar, aber nicht von Anfang an einstellbar. Beide machen die Spalte ebenfalls lesbar, wie Writing, und beide existieren auf TablePermissions für einen eigenen Anbieter. Sie spiegeln die separaten Berechtigungen zum Erstellen und Aktualisieren wider, die Dataverse eigene Feldsicherheit gewährt:
builder.Services.AddTableAccess("incident", t => t
.ForSignedInUsers()
.RowsOwnedByContact("customerid")
.Allow(TableSecurityPermission.Read | TableSecurityPermission.Create | TableSecurityPermission.Write)
.Reading("ticketnumber", "statuscode")
.Writing("title", "description") // Gesetzt bei Erstellung, danach änderbar
.WritingOnCreate("prioritycode") // Gesetzt bei der Erstellung, nur nach der Lesebestimmung
.WritingOnUpdate("resolution")); // danach veränderbar, nicht am Anfang festgelegt
Das Framework setzt das Ergebnis auf jedem Pfad durch, bei dem Daten die Servergrenze überschreiten:
- Reads – eine zurückgehaltene Spalte wird entweder vollständig aus dem Datensatz weggelassen oder mit maskiertem Wert zurückgegeben und aus Tabellenmetadaten und Rasterspalten verborgen, sodass ein Formular einen Editor nie an einen Wert bindet, der niemals ankommt.
- Abfragen – ein Lesen wird niemals wegen Spaltenberechtigungen abgelehnt. Ansichten werden routinemäßig über Zielgruppen verteilt, und dass ein Nutzer eine Spalte sieht, die ein anderer nicht sehen kann, darf nicht zu einem Fehler werden, der Sie zwingt, eine separate Ansicht pro Publikum beizubehalten. Jeder Verweis auf eine zurückgehaltene Spalte wird stattdessen weggelassen: Projektionen, Sortierungen, Filterbedingungen (zusammen mit dem Filter, sobald sie leer ist) und Aggregate oder Gruppen-Teile. Das Weglassen einer Bedingung erweitert die Ergebnismenge, aber nur über Zeilen, die der Nutzer bereits lesen kann – die Zeilensichtbarkeit erfolgt durch die Abfragefilter der Anbieter, die nach diesem Durchgang hinzugefügt werden und nie von ihm berührt werden.
- Writes – im Gegensatz zu Reads wird ein Create oder Update, das eine Spalte ändert, die der Nutzer nicht schreiben darf, abgelehnt und nicht stillschweigend gekürzt: Ein abgelehnter Speicherstand ist die einzige Möglichkeit, dem Nutzer zu sagen, dass die Änderung nicht angekommen ist, während ein eingegrenzter Read unsichtbar und harmlos ist. Ein Create wird anhand dessen bewertet, was beim Erstellen gesetzt werden kann, und ein Update gegen das, was geändert werden kann – sowohl durch einen einzelnen Speicherstand als auch durch einen Batch.
Anmerkung
Die primäre ID-Spalte einer Tabelle wird niemals zurückgehalten, egal was das Leseset angibt. Die ID ist Identität statt Daten – sie befindet sich bereits in der URL, die der Anrufer verwendet hat, um den Datensatz zu erreichen – und Zeilenschlüssel, Edit-Links, Subgrid-Beziehungen und Löschen lassen sich ohne sie auseinanderfallen. Eine Zulassungsliste, die die Spalten nennt, die eine Seite benötigt, und die ID vergisst, würde das Portal auseinandernehmen und nichts schützen.
Wie sich die Komponenten verhalten
Die Framework-Komponenten berücksichtigen eigenständig Spaltenberechtigungen – man verkabelt nichts. Eine Spalte, die der Benutzer nicht schreiben darf, wird überall dort gelesen und deaktiviert: Jeder Editor erkennt sie über seine gemeinsame Schreib-nur-Resolution, und das Raster gibt der Zelle keinen Inline-Editor, sodass sie auf einfachen Anzeigetext zurückfällt. Ein vom Verbraucher bereitgestellter readOnly="false" Spalte überschreibt das nicht – der Server würde den Speicherstand so oder so ablehnen.
Eine Spalte, die der Benutzer nicht lesen darf – eine maskierte – wird in beiden Stacks statt eines Editors durchgerendert <ColumnDisplayValue> . Das ist nicht nur eine Nettigkeit: Eine maskierte Spalte hat überhaupt keinen Wert, nur die Maske als ihren formatierten Wert , sodass ein an sie gebundener Editor leer erscheinen würde; ColumnDisplayValue liest den formatierten Wert und zeigt somit den Platzhalter an, egal welchen Typ die Spalte hat. Spalten, die durch Auslassung zurückgehalten werden, erreichen überhaupt keine Komponente – sie sind nicht im Datensatz und die Metadaten der Tabelle listen sie nicht auf.
Was der Kunde erhält
Spaltenberechtigungen erscheinen als Abbildung ColumnPermissions – logischer Name der Spalte auf ein ColumnSecurityPermission Flagpaar – für die Spalten, deren Zugriff eingeschränkt ist, was Write bedeutet, dass die Spalte bei einem bestehenden Datensatz geändert werden kann. Alles, was fehlt, hat vollen Zugriff, sodass eine uneingeschränkte Tabelle gar nichts enthält. Ein von sich aus abgerufener Datensatz trägt die Karte; gemeinsam abgerufene Datensätze teilen sich eine Karte, die einmal auf der Antwort gesendet wird, da die Berechtigungen dem Benutzer und der Tabelle gehören und nicht einer Zeile – beide Stack-Clients legen sie auf jeden Datensatz zurück, sodass das Lesen der Berechtigungen eines Datensatzes in beide Richtungen gleich funktioniert. Grid-Antworten melden sie auch pro Spalte auf ResolvedColumn.Permissions. Ein erstellter Datensatz hat noch keine eigene Karte, daher gibt NewRecordColumnPermissions auch der Endpunkt der Tabellenberechtigungen zurück – die gleiche Form, gemessen an dem, was beim Erstellen gesetzt wird – und beide Stacks RecordContext legen ihn auf einen neuen Datensatz für dich. Spalten, die durch Auslassung zurückgehalten wurden, fehlen absichtlich auf jeder Karte – das Weglassen existiert, sodass nichts in der Antwort die Spalte offenbart, und wenn man sie dort nennt, würde das rückgängig machen. Lies es selbst durch record.CanReadColumn(column) / record.CanWriteColumn(column) in Blazor oder die Matching canReadColumn / canWriteColumn Helfer aus @powerportalspro/core React.
Weglassen oder Maskieren
Standardmäßig wird eine zurückgehaltene Spalte maskiert: Der Datensatz trägt keinen Wert dafür und SecurityOptions.WithheldColumnMask (***** standardmäßig) als seinen formatierten Wert, sodass ein Formular oder Raster das Feld weiterhin rendert und deutlich macht, dass der Wert zurückgehalten und nicht leer ist. Die Maske ist gleich, egal ob die Spalte einen Wert hat oder nicht, also verbirgt sie auch diesen. Stattdessen wird SecurityOptions.WithheldColumnBehavior Omit die Spalte komplett aus dem Datensatz entfernt, als hätte die Abfrage nie danach gefragt, sodass nichts in der Antwort zeigt, dass die Spalte existiert – die richtige Wahl, wenn die Existenz der Spalte sensibel ist. Das Zurückhalten einer Spalte enthält auch jede Spalte, die sie erneut veröffentlicht – den Begleiter eines Lookups, der Basiswährungs-Zwilling einer Geldspalte –, sodass eine Ablehnungsliste, die eine Spalte nennt, nicht durch eine andere umgangen werden kann. Eine einzelne Tabelle kann die siteweite Wahl über TableColumnPermissions.WithheldColumnBehaviorüberschreiben.
builder.Services.Configure<SecurityOptions>(options =>
{
options.WithheldColumnBehavior = WithheldColumnBehavior.Mask;
options.WithheldColumnMask = "*****";
});
Anmerkung
Paaren Sie eine maskierte Spalte mit einer Schreib-Ablehnung. Eine maskierte Spalte hat keinen Wert im Datensatz, sodass ein Aufrufer, der einen gesamten geladenen Datensatz zurückpostet, nichts dafür sendet – aber das Framework nimmt eine Speicher-Payload als Scheinwert an, sodass eine Anfrage, die die Spalte setzt, einschließlich der Maske, den gespeicherten Wert überschreibt, wenn die Spalte schreibbar ist. Die Schreib-Ablehnung ist es, die sie ablehnt.
Kolumnen, die eine weitere Kolumne neu veröffentlichen
Das Zurückhalten einer Spalte enthält auch jede Spalte, die denselben Wert unter einem zweiten Namen trägt. Dataverse erstellt diese für Sie: eine Abfrage …name und …yominame Partner, das Label einer Wahl und ein Basiswährungs-Zwilling für jede Währungsspalte – creditlimit_base neben creditlimit. Es handelt sich um gewöhnliche Spalten, daher sagt eine Denylist-Benennung creditlimit nichts über creditlimit_baseaus, und ohne diese würde ein maskierter Betrag direkt durch den Zwilling zurückkommen. Der Link stammt aus den eigenen Metadaten der Plattform und nicht aus einer Namensregel, daher gilt er auch für Ihre eigenen Spalten und gilt bei Lesungen, bei einer Abfrage, die den Begleiter explizit benennt, in Tabellenmetadaten, in Rasterspalten und bei Speicherständen.
Best Practice – an der Quelle filtern
Ein Fall, den das Framework für Sie nicht sieht: eine berechnete oder zusammenrollbare Spalte sowie die zusammengesetzten Spalten der Plattform —
fullname, ,yomifullnameundaddress1_compositederen Schwester. Dataverse gibt nicht an, welche Spalten sie füttern, daher heißt Zurückhaltunglastnamenichtfullnameauch , und eine berechnete Spalte über einer von Ihnen abgelehnten Spalte zeigt trotzdem ihr Ergebnis an. Nennen Sie sie selbst im Leseset, wenn ihre Eingaben empfindlich sind. Eine Zulassungsliste (ColumnPermissions.Allow(…)) umgeht das Problem komplett, da nichts lesbar ist, bis Sie es benennen – was die bessere Standardeinstellung für eine Tabelle ist, die alles enthält, was es wert ist, zurückgehalten zu werden.
Anmerkung
Wenn mehrere Anbieter eine Tabelle abdecken, setzen sich ihre Spaltenmengen genauso zusammen wie ihre Operationen: additiv über die Rollen hinweg und unter
MultiplePermissionHandlersBehaviorinnerhalb einer Gruppe. Ein Anbieter, der nur existiert, um Zeilen zu scopen und nichts über Spalten sagt, wird aus der Spaltenfaltung vollständig ausgelassen, sodass er weder das erweitert noch eingrenzt, was die anderen gewährten.
Dataverse-Native Security (Vorschau)
Das empfohlene Sicherheitsmodell ermöglicht es Dataverse selbst, die Sicherheit für Ihre Portalnutzer durchzusetzen, basierend auf der einheitlichen Dataverse-Sicherheitsfunktion von Power Pages. Anstatt Berechtigungshandler im Code zu schreiben, verwalten Sie den Zugriff mit Power Pages-Webrollen und Tabellenberechtigungen, die die Plattform in Dataverse-Sicherheitsrollen umwandelt, die Dataverse bei jeder Anfrage durchsetzt. Das Modell befindet sich derzeit in der Vorschau – sowohl Microsofts Funktion als auch Power Portals Pros Unterstützung dafür – und Power Portals Pro wird Microsofts Veröffentlichungsplan bis zur allgemeinen Verfügbarkeit folgen.
Wenn aktiviert, stellt PowerPortalsPro automatisch ein systemverwaltetes (C2) systemuser für jeden Portalkontakt bereit – bei der Registrierung für neue Kontakte und faul bei der Anmeldung für bestehende – zusammen mit dem powerpagesusermapping Datensatz, der den Kontakt, den Systemnutzer und Ihre Power Pages-Website verknüpft. Jeder Dataverse-Aufruf für die Sitzung imitiert dann diesen Systembenutzer, sodass die Sicherheitsrollen, die Power Pages mit deinen Webrollen und Tabellenberechtigungen synchronisiert, genau bestimmen, was der Nutzer sehen und tun kann. Die Sitzung trägt ein PortalUserType Off ContactStubUser , sodass Ihre Benutzeroberfläche diese bereitgestellten Benutzer immer von echten internen Benutzern unterscheiden kann.
Anmerkung
Um dieses Sicherheitsmodell zu verwenden, müssen Sie eine Power Pages-Website in Ihrer Dataverse-Umgebung erstellen – diese installiert die Microsoft-Framework-Komponenten und ermöglicht es Ihnen, die unified-security-Funktion zu aktivieren. Sie benötigen keinen bezahlten Power Pages-Plan und können die Website direkt nach der Installation und Aktivierung der Funktion löschen. Der von von belegte
PowerPagesSiteIdPower Pages-Site-Eintrag (powerpagesite) muss in der Umgebung vorhanden sein, da Benutzerzuordnungen und Synchronisation mit Sicherheitsrollen gegen ihn aufgelöst werden.
Aktivierung von Dataverse-Native-Sicherheit
Aktivieren Sie das System SecurityOptions auf Ihrem Server Program.csund geben Sie die ID Ihres Power Pages-Website-Datensatzes an:
builder.Services.Configure<SecurityOptions>(options =>
{
options.DataverseSecurity.ProvisionSystemUsers = true;
options.DataverseSecurity.PowerPagesSiteId = new Guid("<your powerpagesite id>");
});
Die Konfiguration wird beim Anwendungsstart überprüft: Wenn ProvisionSystemUsers aktiviert ist, PowerPagesSiteId muss gesetzt und auf einen bestehenden powerpagesite Datensatz aufgelöst werden, andernfalls startet die Anwendung nicht mit der Leitung.
Gewähren Sie Zugriff genau so wie Power Pages: Konfigurieren Sie Webrollen und Tabellenberechtigungen – in der Power Pages Management-App oder auf der Webrollen-Seite Ihres Portals in der Power Portals Pro Enhanced Authorization-Lösung – und weisen Sie Webrollen Kontakten zu (über die Management-App, programmatisch oder automatisch für neue Nutzer mit Regeln auf derselben Seite). Die Plattform synchronisiert diese in automatisch generierte Sicherheitsrollen (benannt PowerPages_*) und weist sie automatisch zu, wenn sich die Webrollenmitgliedschaft ändert – da die Konfiguration vollständig in Power Pages liegt, kann Ihr Portal problemlos zu oder von einer echten Power Pages-Seite wechseln. Beachten Sie, dass Rollen- und Privilegienänderungen von wenigen Sekunden bis zu ein paar Minuten dauern können, bis sie durch die Dataverse-Caches verbreitet werden, bevor sie wirksam werden.
Folgen Sie der Schritt-für-Schritt-Einrichtungsanleitung
Eine externe Anmeldung mit einem Kontakt abgleichen
Wenn sich jemand bei Microsoft, Google oder einem anderen externen Anbieter anmeldet, muss das Portal herausfinden, zu welchem bestehenden Kontakt – falls überhaupt – die Anmeldung gehört. Dies wird in zwei Schritten gelöst:
- Eine bestehende Verbindung. Wenn das Konto sich schon einmal angemeldet hat, verknüpft eine
adx_externalidentityZeile es bereits mit einem Kontakt. Das ist eine explizite, gespeicherte Aussage "Dieses Konto ist diese Person", sodass sie immer gewinnt und nie von den untenstehenden Einstellungen beeinflusst wird. - Das E-Mail-Rückfallback. Da es noch keinen Link gibt, gleicht das Portal die vom Anbieter angegebene E-Mail-Adresse mit den Spalten des Kontakts ab. Dies ist der Schritt, den Sie konfigurieren können – und der Schritt, der entscheidet, ob ein zurückkehrender Kunde seinem bestehenden Kontakt beitritt oder eine Kopie erhält.
Standardmäßig werden zwei Spalten durchsucht, zuerst der Benutzername des Portals. Kontakte, die über das Portal erstellt werden, tragen adx_identity_username, während Kontakte, die vom Personal importiert oder eingegeben wurden, meist nur emailaddress1 – die Suche in beiden ermöglicht es einem bestehenden Kunden, sich einzuloggen und auf seinem eigenen Datensatz zu landen, statt auf einem neuen:
builder.Services.Configure<SecurityOptions>(options =>
{
// The default: portal username first, then the primary email column.
options.ContactMatching.ExternalLoginEmailColumns =
[
ContactEmailColumn.IdentityUsername,
ContactEmailColumn.EmailAddress1,
];
});
Die Reihenfolge ist wichtig: Es ist der Vorrang, nicht nur eine Liste. Die Spalten werden zusammen in einer einzigen Abfrage durchsucht, und die erste Spalte in Ihrer Liste, die zu etwas passt, entscheidet über den Gewinner – wenn also ein Kontakt die Adresse als Portal-Benutzername und ein anderer als emailaddress1Portal-Benutzername angibt, gewinnt der Benutzername-Treffer unter der Standardreihenfolge.
Führe weniger Spalten auf, um das Matching zu verengen: [ContactEmailColumn.IdentityUsername] beschränkt sie auf vom Portal erstellte Konten, und eine leere Liste schaltet den E-Mail-Fallback komplett aus, sodass ein bestehender externer Identitätslink die einzige Möglichkeit bleibt, wie ein Konto zu einem Kontakt aufgelöst werden kann. Füge hinzu ContactEmailColumn.EmailAddress2 und ContactEmailColumn.EmailAddress3 um es zu erweitern – sie werden absichtlich aus dem Standard weggelassen, aus dem unten genannten Grund.
Best Practice – an der Quelle filtern
Jede Spalte, die Sie angeben, ermöglicht es derjenigen, die eine E-Mail-Adresse kontrolliert, als Kontakt erkannt zu werden, der sie hält. Listen Sie nur Spalten auf, die Ihr Dateneingabeprozess als Eigentum des Kontakts selbst behandelt – wenn
emailaddress2Ihr Team die Adresse eines Assistenten oder einen gemeinsamen Firmenposteingang eingibt, kann sich durch die Listung jeder, der diesen Posteingang liest, als dieser Kontakt einloggen.
Best Practice – an der Quelle filtern
Das Matching macht die E-Mail-Adresse zu einer Identifikator, nicht nur zu einem Label – es entscheidet, welcher bestehende Kontakt ein Anmelder wird. Das Portal überspringt die Bestätigungs-E-Mail nur, wenn der Identitätsanbieter diese genaue Adresse tatsächlich verifiziert hat. Wenn die Adresse über die vom Anbieter angegebene eingegeben wurde oder der Anbieter sie nie verifiziert hat, wird ein Bestätigungslink gesendet und der externe Login wird erst mit dem Kontakt verknüpft, bis dieser Link gefolgt ist.
Welche Anmeldungen als verifiziert gelten, sind keine Eigenschaft des Anbieters. Google überprüft jede angegebene Adresse. Microsoft nicht: Ein persönliches Microsoft-Konto hat seine Adresse überprüft, aber ein Entra-Arbeits- oder Schulkonto liefert alles, was im Verzeichnisattribut
verified_primary_emailoderverified_secondary_emailClaim listet sie auf oderxms_edovgibt an, dass die Domain dem eigenen Mieter des Nutzers gehört. Entra sendet diese Ansprüche nur, wenn die App-Registrierung danach fragt (siehe Entra ID Sign-In Setup); ohne diese bestätigen Arbeits- und Schulanmeldungen beim ersten Mal per E-Mail.
Anmerkung
Diese Einstellungen gelten nur für externe Anmeldungen. Lokale Benutzername/Passwort-Anmeldung, Registrierung und Passwort-Zurücksetzung stimmen absichtlich auf
adx_identity_username– der Passwort-Zurücksetzungsablauf mailt seinen Link an die eingegebene Adresse, sodass das Abgleichen der sekundären E-Mail-Spalten jemandem erlaubt, das Passwort einer anderen Person zurückzusetzen.
Nur bestehende Kontakte können sich anmelden
Standardmäßig kann jeder Portalnutzer werden: Die Registrierung oder das Anmelden bei einem externen Anbieter unter einer Adresse, die keinen Kontakt hat, schafft einen Kontakt. Wenn das Portal für Personen ist, die Sie bereits in Dataverse halten – Kunden, Mitglieder, Partner – schalten Sie die Anmeldung aus, und nur sie können sich anmelden:
builder.Services.Configure<AuthClientOptions>(options => options.SignUpEnabled = false);
Die Registrierungsseite und ihre Links verschwinden, und der Server weigert sich, einen Benutzer zu erstellen, egal wie er darum gebeten wird. Ein externer Anmelder findet trotzdem seinen oben beschriebenen Kontakt und verlinkt darauf; einer, der zu niemand passt, wird abgelehnt, anstatt einen Kontakt zu erstellen. Lokale Konten sind für Personen, die bereits einen haben, nicht betroffen.
Anmerkung
Eine verweigerte Anmeldung wird nur dann auf dem Bildschirm erklärt, wenn der Anbieter genau diese Adresse überprüft hat, denn dann hat die angemeldete Person bereits bewiesen, dass sie sie besitzt. Bei jeder anderen Adresse antwortet das Portal genauso wie bei einer Adresse mit Kontakt – "E-Mail prüfen" – und schickt der Adresse eine kurze Benachrichtigung, dass sie nicht registriert ist. Wenn man den Besucher direkt informiert, kann jeder Adressen ins Formular eingeben, um herauszufinden, wer ein Konto hat.
Die Mitteilung wird über den eigenen E-Mail-Absender des Portals verschickt. Ein Host, der sie durch einen eigenen E-Mail-Stack ersetzt, sollte ebenfalls ein
ISignUpRefusalEmailSenderregistrieren; ohne eine wird die Ablehnung protokolliert und keine Mitteilung gesendet.
Eingebaute Basisklassen
Das Framework stellt Basisklassen bereit, um gängige Sicherheitsmuster zu vereinfachen:
AnonymousPermissionHandler— gewährt allen eine Tabelle, egal ob sie angemeldet ist oder nicht, einschließlich jeder Spalte. Es ist der einzige Ort, an dem die Opt-in-Standardregelung aufgehoben wird, weil daraus die Entscheidung zur Veröffentlichung der Tabelle liegt . ÜberschreibenGetPermissionsAsyncSie das Veröffentlichen, um weniger zu veröffentlichen.
API-Referenz
AuthorizePortalRoleAttribute Baureihe
Eigenschaften
Name | Typ | Default | Beschreibung |
|---|---|---|---|
AuthenticationSchemes | string? | ||
Policy | string? | ||
RoleNames | IReadOnlyList<string> | Die Rollennamen, die dieses Attribut erfordert, und jeder davon erfüllt ihn. | |
Roles | string? | ||
TypeId | Object |
AuthenticationSchemesPolicyRoleNamesRolesTypeIdMethoden
Name | Parameter | Typ | Beschreibung |
|---|---|---|---|
IsDefaultAttribute | bool | ||
Match | Object obj | bool |
IsDefaultAttributeMatchITablePermissionProvider Interface
Eigenschaften
Name | Typ | Default | Beschreibung |
|---|---|---|---|
Table | string | Logischer Name der Tabelle, auf die sich dieser Anbieter bezieht. Genau abgestimmt, also muss es die Dataverse logischer Name in Kleinbuchstaben. |
TableMethoden
Name | Parameter | Typ | Beschreibung |
|---|---|---|---|
GetPermissionsAsync | UserInfo user | Task<TablePermissions> | Was user könnte mit dieser Tabelle geschehen, und welche ihrer Spalten sie lesen dürfen und schreibe. Zurück TablePermissions.None für einen Benutzer, der eigentlich keinen Zugriff haben sollte. TableSecurityPermission.Read ob der Benutzer eine der Tabellen sehen kann Zeilen und Scope, welche in Services.TableQueryContext). TableSecurityPermission.Read Allein gibt nur die ID des Datensatzes frei. Verwendung Reading(System.String[]) und Writing(System.String[]) sie zu benennen, oder TablePermissions.AllColumns Um für diese Tabelle auf die Spaltensicherheit zu verzichten. |
GetQueryFilterAsync | TableQueryContext context | Task<TableQueryFilter> | Bereiche, auf welche Zeilen dieser Tabelle der Benutzer reagieren kann: ein Filter, der mit erstellt wurde Query.LogicalOperator), TableQueryFilter.AllRows für jeden Reihen die Operationen erlauben, oder null keine Reihen. null — oder ein Filter ohne Bedingungen — gewährt keine, also Wenn man vergisst, eine Tabelle zu scopen, lehnt sie ab, anstatt jede Zeile zu veröffentlichen. Zum Beispiel TableQueryFilter.AllRows Wenn jede Reihe das ist, was du meinst. TableQueryContext.Operation . Es läuft außerdem unter beiden Sicherheitsmodellen, sodass sie bei Dataverse-nativer Sicherheit weiterhin in Kraft bleibt umgeht Services.UserInfo). PortalRoleOptions.CompositionBehavior Sie auf RoleCompositionBehavior.Restrictive Um sich auch über verschiedene Rollen hinweg zu überschneiden. null Anbieter der Gruppe keine Zeilen, und TableQueryFilter.AllRows Überlässt es den Filtern der anderen Anbieter, selbst zu entscheiden. Across Gruppen erweitert eine Gruppe gewährt TableQueryFilter.AllRows die Standardadditivität Union zu jeder Zeile – daher bedeutet der Filter einer Rolle nur etwas, wenn die anderen Gruppen gewähren Auch dieser Tischfilter. |
GetPermissionsAsyncuser könnte mit dieser Tabelle geschehen, und welche ihrer Spalten sie lesen dürfen und schreibe. Zurück TablePermissions.None für einen Benutzer, der eigentlich keinen Zugriff haben sollte. TableSecurityPermission.Read ob der Benutzer eine der Tabellen sehen kann Zeilen und Scope, welche in Services.TableQueryContext). TableSecurityPermission.Read Allein gibt nur die ID des Datensatzes frei. Verwendung Reading(System.String[]) und Writing(System.String[]) sie zu benennen, oder TablePermissions.AllColumns Um für diese Tabelle auf die Spaltensicherheit zu verzichten. GetQueryFilterAsyncQuery.LogicalOperator), TableQueryFilter.AllRows für jeden Reihen die Operationen erlauben, oder null keine Reihen. null — oder ein Filter ohne Bedingungen — gewährt keine, also Wenn man vergisst, eine Tabelle zu scopen, lehnt sie ab, anstatt jede Zeile zu veröffentlichen. Zum Beispiel TableQueryFilter.AllRows Wenn jede Reihe das ist, was du meinst. TableQueryContext.Operation . Es läuft außerdem unter beiden Sicherheitsmodellen, sodass sie bei Dataverse-nativer Sicherheit weiterhin in Kraft bleibt umgeht Services.UserInfo). PortalRoleOptions.CompositionBehavior Sie auf RoleCompositionBehavior.Restrictive Um sich auch über verschiedene Rollen hinweg zu überschneiden. null Anbieter der Gruppe keine Zeilen, und TableQueryFilter.AllRows Überlässt es den Filtern der anderen Anbieter, selbst zu entscheiden. Across Gruppen erweitert eine Gruppe gewährt TableQueryFilter.AllRows die Standardadditivität Union zu jeder Zeile – daher bedeutet der Filter einer Rolle nur etwas, wenn die anderen Gruppen gewähren Auch dieser Tischfilter. TablePermissions Baureihe
Eigenschaften
Name | Typ | Default | Beschreibung |
|---|---|---|---|
Columns | TableColumnPermissions | Die Spalten, die der Benutzer lesen und schreiben darf. TableColumnPermissions.None Außer Etwas, das sie genannt wurde: Spaltenzugang wird gewährt, aber nie angenommen. | |
Operations | TableSecurityPermission | Die Operationen, die der Benutzer auf der Tabelle ausführen kann, als Flaggenwert. Dies ist eine Tabellenebene Gatter, kein Zeilenfilter – wenn TableSecurityPermission.Read der Benutzer es erlaubt Sehen Sie sich eine der Reihen der Tabelle an und scannen Sie welche durch Services.TableQueryContext). |
ColumnsTableColumnPermissions.None Außer Etwas, das sie genannt wurde: Spaltenzugang wird gewährt, aber nie angenommen.OperationsTableSecurityPermission.Read der Benutzer es erlaubt Sehen Sie sich eine der Reihen der Tabelle an und scannen Sie welche durch Services.TableQueryContext).Methoden
Name | Parameter | Typ | Beschreibung |
|---|---|---|---|
AllColumns | TablePermissions | Verzichtet vollständig auf die Sicherheit auf Spaltenebene – jede Spalte ist lesbar und beschreibbar. Die Verhalten vor der Säulensicherheit, das eher ausgesprochen als geerbt wurde. | |
Allow | TableSecurityPermission operations | TablePermissions | Fügt dem hinzu, operations was dieser Wert bereits bietet. |
Allowing | TableSecurityPermission operations | TablePermissions | Zuschüsse operations und keine Spalten. Kette Reading(System.String[]), Writing(System.String[]) oder TablePermissions.ReadingAllColumns um Spalten zu öffnen. |
Reading | string[] columns | TablePermissions | Erlaubt das Lesen der benannten Spalten zusätzlich zu allen bereits erlaubten. Groß- und Schreibung-unempfindlich, Die Schreibweise des Maker-Portals funktioniert also ebenso gut wie der logische Name. |
ReadingAllColumns | TablePermissions | Verzichtet auf die Lesesicherheit auf Spaltenebene für diese Tabelle: jede Spalte, einschließlich der hinzugefügten später an den Tisch. Absichtlich explizit – das Rahmenwerk wird es nicht annehmen. | |
ReadingAllColumnsExcept | string[] columns | TablePermissions | Meldet sich gegen die Spalten-Lesesicherheit ab, außer bei den benannten Spalten – jede Spalte außer jeder Spalte diese, einschließlich derjenigen, die später hinzugefügt wurden. Öffnet sich nicht für alles Neue, daher bevorzuge ich es Reading(System.String[]) auf Tabellen mit sensiblen Daten. |
ReadingAndWriting | string[] columns | TablePermissions | Ermöglicht sowohl das Lesen als auch das Schreiben der benannten Kolumnen. |
WithColumns | TableColumnPermissions columns | TablePermissions | Ersetzt die Spaltenregeln komplett, für einen Anbieter, der lieber eine Services.TableColumnPermissions sich selbst und dann die flüssigen Methoden verkettet. |
Withholding | WithheldColumnBehavior behavior | TablePermissions | Wie die zurückgehaltenen Spalten in dieser Tabelle zurückkommen – weggelassen oder mit einem verdeckten Wert präsentiert werden. Überschreibt für diese Tabelle die seitenweite SecurityOptions.WithheldColumnBehavior Übersicht. |
Writing | string[] columns | TablePermissions | Erlaubt das Schreiben der benannten Spalten – wenn ein Datensatz erstellt und aktualisiert wird – in Ergänzung zu allen bereits erlaubten. Eine Kolumne zu schreiben bedeutet nicht, sie zu lesen – nenne sie in Reading(System.String[]) auch, ob der Nutzer sehen sollte, was er geschrieben hat. |
WritingAllColumns | TablePermissions | Verzichtet auf die Schreibsicherheit auf Spaltenebene für diese Tabelle: Der Benutzer kann jede beliebige Spalte schreiben, beim Erstellen und beim Aktualisieren. Ersetzt beide Schreibmengen, also ruf WritingOnCreate(System.String[]) / WritingOnUpdate(System.String[]) Danach, nicht davor. | |
WritingAllColumnsExcept | string[] columns | TablePermissions | Meldet sich gegen die Spalten-Schreibsicherheit ab, außer bei den benannten Spalten, die weiterhin lesegeschützt bleiben. Ersetzt beide Schreibmengen, also ruf WritingOnCreate(System.String[]) / WritingOnUpdate(System.String[]) Danach, nicht vorher. |
WritingOnCreate | string[] columns | TablePermissions | Erlaubt das Setzen der benannten Spalten, wenn ein Datensatz erstellt wird, und erst dann: sobald er existiert sie sind schreibgeschützt, selbst wenn ein früherer Aufruf — Writing(System.String[]), TablePermissions.WritingAllColumns — machte sie beschreibbar. Die Priorität eines Falls, gewählt, wann er es ist Protokolliert und von da an behoben. Nennen Sie es nach einem Writing(System.String[]). |
WritingOnUpdate | string[] columns | TablePermissions | Erlaubt es, die benannten Spalten in einem bestehenden Datensatz zu ändern, sie aber nicht zu setzen, wenn es der Fall ist erstellt, selbst wenn ein früherer Anruf sie beschreibbar gemacht hat – Resolution merkt nur, dass das bedeutet etwas, wenn der Fall einmal existiert. Nennen Sie es nach jedem Writing(System.String[]). |
AllColumnsAllowoperations was dieser Wert bereits bietet.Allowingoperations und keine Spalten. Kette Reading(System.String[]), Writing(System.String[]) oder TablePermissions.ReadingAllColumns um Spalten zu öffnen.ReadingReadingAllColumnsReadingAllColumnsExceptReading(System.String[]) auf Tabellen mit sensiblen Daten.ReadingAndWritingWithColumnsServices.TableColumnPermissions sich selbst und dann die flüssigen Methoden verkettet.WithholdingSecurityOptions.WithheldColumnBehavior Übersicht.WritingReading(System.String[]) auch, ob der Nutzer sehen sollte, was er geschrieben hat.WritingAllColumnsWritingOnCreate(System.String[]) / WritingOnUpdate(System.String[]) Danach, nicht davor.WritingAllColumnsExceptWritingOnCreate(System.String[]) / WritingOnUpdate(System.String[]) Danach, nicht vorher.WritingOnCreateWriting(System.String[]), TablePermissions.WritingAllColumns — machte sie beschreibbar. Die Priorität eines Falls, gewählt, wann er es ist Protokolliert und von da an behoben. Nennen Sie es nach einem Writing(System.String[]).WritingOnUpdateWriting(System.String[]).AuthClientOptions Baureihe
Eigenschaften
Name | Typ | Default | Beschreibung |
|---|---|---|---|
LocalAccountsEnabled | bool | True | Ob das Portal lokale Benutzername-/Passwortkonten akzeptiert. Standard: |
SignUpEnabled | bool | True | Ob das Einloggen einen Portal-Nutzer erstellen kann. Standard |
LocalAccountsEnabledSignUpEnabled