Seguridad de la mesa
Power Portals Pro asegura los datos en tres niveles: qué tablas puede tocar un usuario, qué columnas de esas tablas puede ver y cambiar, y qué filas regresan en absoluto. Las tres se deciden en un registro por tabla. Describe ese registro con AddTableAccess — es la forma de conceder una tabla y lo que usan las plantillas del proyecto para cada tabla que registran. Cuando una regla está fuera de lugar, las mismas tres respuestas pueden escribirse como una ITablePermissionProvider clase; ambas producen lo mismo y componen de la misma manera. No se concede nada hasta que una de ellas lo dice: una tabla que no cubre el registro es inalcanzable, que es el default fallo-cerrado.
Propina
No tienes que escribir esto desde una página en blanco. Los proyectos generados incluyen una
secure-a-tablehabilidad: pide a tu agente de codificación que asegure una tabla y este trabaja a través de qué tabla, para quién es, qué filas, qué operaciones y qué columnas, luego escribe el registro y realiza los cambios correspondientes en Dataverse a través del servidor Power Portals Pro MCP — creando el rol del portal si el acceso está limitado a un rol, y otorgando al usuario de la aplicación los privilegios que implican las respuestas. Después,check_setupinforma cualquier tabla a la que tus páginas se vinculen y a la que nada conceda acceso, y cualquier registro que se retire de la seguridad de columnas. Consulta cómo empezar → servidor MCP.
Todos, o solo algunas personas
Cada registro dice para quién es, y hay dos respuestas. Regístralo en el portal y aplica a una audiencia tus nombres en clave — todos o todos los usuarios conectados. Regístralo dentro AddPortalRole y se aplica a los titulares de ese rol, que es una decisión de datos que toma un administrador en Dataverse. El acceso en sí se describe de la misma manera en ambos.
| Registro | Aplica a | Para cambiar quién lo recibe |
|---|---|---|
AddTableAccess("account", t => t.ForSignedInUsers()…) |
todos los que el público cubre, a petición | editar el código y volver a desplegar |
AddPortalRole("Credit Manager", role => role.WithTableAccess("account", …)) |
contactos que ostentan ese Rol de Portal, y nadie más | asignar o desasignar el rol en Dataverse — sin despliegue |
Se puede registrar una tabla una vez para cada audiencia de todo el portal — ForEveryone() para lo que cualquiera puede alcanzar, ForSignedInUsers() para qué añade iniciar sesión — y ambas suman. Un visitante recibe la primera; un usuario iniciado sesión recibe ambos, con cada operación confinada a las filas del registro que la concede. Con las dos inscripciones que aparecen a continuación, todo el mundo lee cada artículo, y un autor con inicio de sesión también puede escribir uno y editar el suyo propio:
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"));
Los roles se suman; nunca restan. El acceso de un usuario son los registros a nivel de portal más todos los roles que ocupa, sindicados — así que el de todo el portal es el suelo en el que todos se sienten, y un rol solo puede aumentarlo. Un rol no puede recuperar una columna o fila que concedió el registro en todo el portal; para retener algo a la mayoría de la gente, concederlo en un rol en lugar de en todo el portal. (Ajustado SecurityOptions.Roles.CompositionBehavior a Restrictive si necesitas que todos los grupos estén de acuerdo.)
Así que: si todos los usuarios de ese tipo deberían tener acceso, regístralo en el portal. Si tan solo algunos deberían hacerlo — y qué personas es una pregunta cuya respuesta cambia sin autorización — lo ponen en un rol. Un registro de rol no otorga nada hasta que alguien crea un registro de Rol del Portal con ese nombre y lo asigna, por eso uno puede sentarse en un proyecto nuevo sin hacer daño.
Descripción del acceso de una tabla
La mayoría de las tablas quieren las mismas pocas reglas: para quién es la tabla, a qué filas llegan, qué pueden hacer y qué columnas ven. AddTableAccess Dice los cuatro en un solo registro y produce un ordinario ITablePermissionProvider — nada más adelante sabe la diferencia. Lo que compra es que las tres decisiones que deben estar de acuerdo (el filtro, la creación y las columnas escribibles) se toman una vez en lugar de tres.
builder.Services.AddTableAccess("incident", t => t
.ForSignedInUsers()
.RowsOwnedByContact("customerid")
.Allow(TableSecurityPermission.Read | TableSecurityPermission.Create)
.Reading("ticketnumber", "statuscode", "createdon")
.Writing("title", "description"));
Una audiencia, un alcance de fila y Allow(…) todos son necesarios. Ninguno de ellos tiene un valor predeterminado, porque cada defecto plausible es una forma de publicar datos omitiendo algo — y un registro que se equivoca falla al inicio en lugar de en producción.
Oscilaciones de fila
El alcance filtra cada consulta de la tabla — cuadrículas, consultas, subrejillas, gráficos y el fetch antes de una partida guardada o eliminación. Los scopes nombrados cubren las formas que los portales siguen necesitando:
| ¿Qué filas | Alcance | En crear |
|---|---|---|
| Cada fila | AllRows() |
Nada preparado |
| Los propios registros del usuario | RowsOwnedByContact("customerid") |
la búsqueda está configurada para ellos |
| Los registros de sus colegas, en la misma cuenta | RowsOfTheirAccount("customerid") |
La consulta está configurada en su cuenta |
| Registros colgando de un padre a los que puedan contactar | RowsViaParent("salesorderid", "salesorder", p => …) |
Nada fijado — el acceso propio del padre decide |
| Registros que los señalan de más de una manera | RowsMatchingAny(r => r.RowsOwnedByContact("customerid"), r => r.RowsOfTheirAccount("customerid")) |
Lo que establece el primer ámbito de propiedad |
| Su propia fila de contactos | OwnRecordOnly() |
No Create |
Los ámbitos de propiedad establecen la columna por sí mismos cuando se crea un registro, tras las comprobaciones de permisos: el usuario no necesita acceso de escritura ni Append To en la tabla a la que apunta, el valor proviene de la sesión en lugar de la petición, y se rechaza una partida guardada posterior que lo cambie. Eso es lo que mantiene un registro que alguien crea dentro del filtro al que está sujeto. Úsalo OwnedBy('column') cuando la columna propietaria no es la que filtra las filas, y WithoutStamping() cuando un plugin ya la configura.
Cuando leer y cambiar difieren
One scope responde a cada operación a menos que digas lo contrario. RowsForWriting Reduce las filas que un usuario puede cambiar sin reducir las filas que puede ver — "Todos leen cada contacto, cada uno edita solo el suyo" es esa distinción y nada más — y RowsForDeleting reduce la destrucción aún más, porque poder corregir un registro no es lo mismo que poder destruirlo. Cada uno vuelve a la que está por encima, por lo que un registro que no hace distinción no atrae ninguno. Los creadores no se ven afectados: aún no hay fila que filtrar.
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());
Cuando los visores con nombre no encajan
RowsMatching toma un filtro propio sin salir del registro. Dale FetchXML — un <filter> elemento o un conjunto <fetch> cuyos filtros y unirse se copian, con {currentUserId} para el contacto con que inicia sesión— o una callback que se ejecute por solicitud y devuelva bien el FetchXML, compuesto por lo que responden tus servicios, o un filtro que haya construido. Ambos reciben lo mismo TableQueryContext que recibe un proveedor, así saben quién pregunta y si las filas van a ser leídas, actualizadas o eliminadas. No devolver nada no otorga filas: TableQueryFilter.AllRows es como se dice cada fila.
.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;
})
Descripción del acceso de un puesto
Dentro AddPortalRole del rol está la audiencia, así que el registro no nombra a otro. Los roles suman y nunca restan: el acceso del usuario son los registros a nivel de portal más todos los roles que ocupa, sindicatos. Los roles del portal en sí mismos — qué son, cómo se asignan y cómo componen — son la siguiente sección.
builder.Services.AddPortalRole("Case Manager", role => role
.WithTableAccess("incident", t => t
.RowsOfTheirAccount("customerid")
.Allow(TableSecurityPermission.Read | TableSecurityPermission.Write)
.Reading("ticketnumber", "statuscode")
.Writing("title", "description")));
Nota
Una clase de proveedor sigue siendo la forma de expresar cualquier cosa que una descripción no puede, y se compone de forma idéntica —
AddTablePermissionProvider<T>yWithPermissions<T>permanece sin cambios. Los proyectos generados ya no envían uno: todas las tablas que registran están descritas, incluyendocontacty el rol de Gestor de Crédito.
Portal Roles
Un rol de portal es la forma preferida de asegurar un portal. Vincula a los proveedores de permisos a un nombre que los administradores asignan a los contactos en Dataverse, de modo que los proveedores solo se aplican a las personas que ocupan ese rol — y quienes lo poseen son datos, no código. Un proveedor registrado directamente Program.cs se aplica a todos los usuarios en comparación.
La división es deliberada. Dataverse posee los nombres y las asignaciones, que un administrador gestiona en la aplicación basada en modelos. Tu código es dueño de lo que significa cada nombre. Conceder o revocar el acceso a alguien es entonces un cambio de datos: no hay reconstrucción, no hay redistribución.
1. Crear el rol y asignarlo
Crea un registro de rol en el portal y asignalo a los contactos que deberían tenerlo. El registro de rol no contiene más que un nombre y una descripción — ningún dato de permisos está presente en Dataverse.
2. Vincular el rol a sus proveedores
Coincide con el nombre del registro de puesto y lista lo que concede. Los tipos de proveedor están registrados para ti, así que no los registras tú mismo.
builder.Services.AddPortalRole("Dealer", role => role
.WithPermissions<DealerAccountPermissionProvider>()
.WithPermissions<DealerOpportunityPermissionProvider>());
Ese último punto importa: nunca registres también al proveedor de un rol con AddTablePermissionProvider<T>(). Eso lo coloca en el conjunto global y lo concede a todos, anulando silenciosamente el rol. Reserva AddTablePermissionProvider<T>() reglas que realmente se apliquen a todos los usuarios.
Cómo se combinan los roles
Los permisos de un usuario provienen de los proveedores registrados globalmente más cada rol que desempeña, y esos grupos se agrupan así:
- Entre roles — aditivo. Si algún rol concede una operación, el usuario la tiene. Tener más roles solo puede dar más acceso, así que un rol nunca puede quitar algo. Ponlo
SecurityOptions.Roles.CompositionBehaviorenRestrictivesi quieres que los roles se reduzcan entre sí. - Dentro de un mismo rol — sin cambios. Varios proveedores en el mismo rol se integran bajo
SecurityOptions.MultiplePermissionHandlersBehavior, exactamente como siempre han hecho los proveedores registrados globalmente. - Las columnas también suman. Como el acceso a columnas es opt-in, un rol que nunca menciona una columna no puede ampliar lo que otro rol publicado — pero un rol que nombra una columna sí lo concede, porque nombrarla fue un acto deliberado. Cambia
RoleCompositionBehavioraRestrictivey tanto operaciones como columnas se cruzan en su lugar.
Propina
En la composición aditiva nada se niega — algo permanece cerrado solo mientras ningún grupo lo abre. Un proveedor registrado globalmente es el hogar adecuado para una regla que se aplica a todos los usuarios, porque siempre está en el pliegue, pero no es un veto: un rol que otorga la operación, o nombra la columna, la reabre. Para mantener algo cerrado cualesquiera que sean los roles que tenga un usuario, concederlo en ningún rol — o establecer
SecurityOptions.Roles.CompositionBehaviorenRestrictive, donde todos los grupos tengan que estar de acuerdo.
Roles en las páginas y la navegación
Los nombres de roles llegan al navegador como reclamaciones estándar ASP.NET rol, por lo que la autorización de página y la interfaz condicional funcionan sin nada extra:
@attribute [Authorize(Roles = "Dealer")]
<AuthorizeView Roles="Dealer">
<p>Dealer-only content</p>
</AuthorizeView>
La pila React lee los mismos nombres de auth.user.roles, y <AuthorizedView roles="Dealer"> compuerta contenido de la misma manera. Los inicios de sesión internos (usuarios del sistema) llevan aquí sus nombres de roles de seguridad en Dataverse, así que una sola comprobación cubre ambos tipos de usuario.
Autorización para el tipo de puesto adecuado
Un rol de portal y un rol de seguridad de Dataverse pueden compartir un nombre y [Authorize(Roles = "…")] no pueden distinguirlos — coincide con la reclamación de rol no tipado, que conlleva ambos. Dos atributos plantean la pregunta más concreta:
@attribute [AuthorizePortalRole("Dealer")]
// Cualquiera de los dos roles entra.
[AuthorizePortalRole("Dealer", "Distributor")]
public class PricingController : Controller { }
// Un rol de portal llamado "Administrador de Sistemas" no cumple con esto.
[AuthorizeSecurityRole("System Administrator")]
public class ImportController : Controller { }
Los nombres de los roles van en letra por palabra: comas, dos puntos y comillas están bien, porque el nombre nunca se elimina de una lista. Apilar dos atributos requiere ambos, como siempre ha hecho los atributos apilados [Authorize] ; listar dos nombres en un atributo requiere cualquiera de los dos. La misma comprobación está disponible en código como User.IsInPortalRole("Dealer") y User.IsInSecurityRole("Sales Manager").
La misma distinción en el marcado de Blazor. <AuthorizeView Roles="…"> lee la reivindicación de rol sin tipificar y por tanto lleva la misma ambigüedad que el atributo simple; <PortalRoleView> pide un tipo. Ponle PortalRole / SecurityRole para un solo nombre, o PortalRoles / SecurityRoles para varios — una colección en lugar de una cadena separada por comas, porque los nombres de Dataverse suelen contener comas:
<PortalRoleView PortalRole="Dealer">
<a href="/pricing">Dealer pricing</a>
</PortalRoleView>
@* Un rol de portal llamado "System Administrator" no abrirá esto. *@
<PortalRoleView SecurityRole="System Administrator">
<BulkImport />
</PortalRoleView>
React <AuthorizedView> toma las mismas dos puertas, junto con la existente roles . Suministrar varios nombres en una puerta significa cualquiera de ellos; suministrar dos puertas significa ambos. Los predicados detrás de ellos — hasPortalRole, hasSecurityRole, hasRole — se exportan para comprobaciones imperativas:
<AuthorizedView portalRoles="Dealer">
<Link to="/pricing">Dealer pricing</Link>
</AuthorizedView>
<AuthorizedView securityRoles="System Administrator">
<BulkImport />
</AuthorizedView>
Buenas prácticas — filtrar en la fuente
Busca estos siempre que lo que se está protegiendo sea realmente administrativo. Crear un rol de portal es un estándar deliberadamente más bajo que recibir un rol de seguridad en Dataverse, así que un cheque que acepte permite a un administrador asignar acceso al rol que protegía Dataverse simplemente nombrando un rol de portal en su honor.
Nota
Los roles no son gratuitos. Cada uno que un usuario tiene se escribe en la cookie de autenticación dos veces — una como la reclamación estándar de rol, así
[Authorize(Roles)]funciona, y otra como la reclamación específica de tipo que hace posibles las comprobaciones anteriores — que mide unos 160 bytes por rol. Unos quince roles por usuario siguen cabiendo una sola cookie; más allá de eso, la cookie se divide en bloques, y lo primero que se cierra suele ser el límite de cabecera de un proxy inverso, que reporta un opaco 400 en lugar de mencionar roles. Prefiero unos pocos roles amplios en lugar de muchos específicos.
Dando a los nuevos usuarios un rol inicial
Nombra los roles con los que debería empezar cada cuenta nueva y el rol que se aplica a los visitantes que han cerrado sesión:
builder.Services.Configure<SecurityOptions>(options =>
{
options.Roles.DefaultRoleNames = ["Customer"];
options.Roles.AnonymousRoleName = "Anonymous";
});
O decidirlo en Dataverse, sin una liberación. La tabla de configuración de roles del portal empareja un rol con un modo de asignación, y un administrador puede cambiar quién obtiene qué de la aplicación basada en modelos:
- Ninguno — configurado pero inactivo. La fila no concede nada hasta que alguien cambia el modo.
- Asignar automáticamente a nuevos usuarios — cada cuenta recibe este rol al crearse, siempre que tanto la fila de configuración como el rol que nombra estén activos. Desactivar cualquiera de los dos lo detiene: una fila de configuración es una instrucción permanente para asignar un rol, y un rol desactivado no otorga nada, así que una fila activa que nombra un rol inactivo es una instrucción sin nada detrás. Reactivarlo lo reanuda.
- Asignar a nuevos usuarios desde criterios — reservado para coincidir con los propios datos del nuevo usuario. Los criterios aún no se han evaluado, así que una fila en este modo asigna el rol a nadie en lugar de a todos, y el portal registra una advertencia nombrándolo para que no se lea como una regla que se ejecutó y no coincidió con nadie.
Cuando los roles dependen de la persona — su dominio de correo, el proveedor con el que inició sesión, una invitación — regístrate y IPortalRoleAssignmentProvider en su lugar. Se consulta a cada proveedor registrado, así que el tuyo se suma a la lista configurada en lugar de reemplazarla. En cualquier caso, la asignación se ejecuta una vez, cuando se crea la cuenta; nunca se vuelve a aplicar al iniciar sesión, así que un rol que elimina un administrador permanece eliminado.
Nota
Un usuario adquiere un rol recién asignado cerrando sesión y volviendo a entrar — iniciar sesión relee sus roles. Las asignaciones de roles se almacenan en caché por usuario durante 24 horas, así que para aplicar un cambio a alguien que ya ha iniciado sesión, borra la caché de Roles del Portal desde el área de administración del sitio: su sesión lo recoge cuando se refresca de nuevo, en menos de diez minutos, y un rol eliminado deja de funcionar justo cuando empieza uno añadido. La tabla de Configuración de Roles del Portal se almacena en caché por separado durante quince minutos, por lo que una fila añadida allí llega al siguiente registro por sí sola — borra la caché "PortalRoleConfiguration" para que sea inmediata. Desactivar una fila de configuración impide que asigne su rol, de la misma manera que desactivar un rol lo revoca.
En su lugar, escribir a un proveedor
Busca una clase cuando una descripción no puede decir a qué te refieres — una regla que depende de algo que solo tu código conoce, o una que tenga que inspeccionar el registro en proceso. ITablePermissionProvider Es en lo que se convierte cada registro de cualquier forma: un proveedor gobierna una tabla, respondiendo a qué puede hacer un usuario con ella, a qué columnas puede leer y escribir, y — a través del filtro de consulta de abajo — cuáles de sus filas puede ver. Un registro descrito y uno escrito componen de forma idéntica, así que un portal puede usar ambos, y una tabla puede ser concedida por uno y ampliada por el otro.
La interfaz tiene un miembro obligatorio. GetPermissionsAsync recibe un UserInfo documento que lleva el ID del usuario, sus roles de portal y sus roles de seguridad en Dataverse, y devuelve un TablePermissions que describe todo lo que recibe ese usuario:
public class AccountPermissionProvider : ITablePermissionProvider
{
public string Table => "account";
public Task<TablePermissions> GetPermissionsAsync(UserInfo user)
{
if (!user.IsAuthenticated)
return Task.FromResult(TablePermissions.None);
// Las columnas son opcionales: solo lo que se menciona aquí vuelve a aparecer.
return Task.FromResult(TablePermissions
.Allowing(TableSecurityPermission.Read | TableSecurityPermission.Write)
.Reading("name", "telephone1", "websiteurl", "creditlimit")
.Writing("telephone1", "websiteurl"));
}
}
Registro de un proveedor
Un proveedor registrado de esta manera se aplica a todos los usuarios, hayan iniciado sesión o no:
builder.Services.AddTablePermissionProvider<AccountPermissionProvider>();
Esta es también la única forma de quitarle algo a todos: los roles se componen aditivamente por defecto, así que un rol puede añadir acceso pero nunca eliminarlo. Las restricciones a nivel de portal pertenecen a un proveedor registrado globalmente, que siempre forma parte del grupo.
Operaciones
TablePermissions.Operations es un TableSecurityPermission valor de bandera — Read, Create, Write, Delete, y Append AppendTo. Hay dos cosas que merece la pena saber sobre él:
- Es una puerta a nivel de tabla, no un filtro de filas. Concede
Readsi el usuario puede ver alguna de las filas de la tabla, entonces reduce cuáles están enGetQueryFilterAsync. - Una tabla sin proveedor es inalcanzable. Ese es el default fail-closed, y también se aplica a columnas: un proveedor que concede
Readpero no nombra columnas devuelve registros que solo contienen su id.
Nota
No existe deliberadamente un gancho de "inspeccionar cada fila tras cargarla". Filtrar así rompe la paginación — una página de 50 vuelta con menos — hace que los conteos totales sean incorrectos y paga a Dataverse para recuperar las filas que luego descarta. En cambio, el ámbito de la consulta mantiene la paginación y los conteos son correctos por construcción.
Buenas prácticas — filtrar en la fuente
Alcance de datos en la consulta, no después de ella. El framework deliberadamente no ofrece ningún gancho que inspeccione cada fila una vez que ha sido obtenida: filtrar así rompe la paginación (falta una página de 50), hace que el recuento total sea incorrecto y paga a Dataverse para recuperar las filas que luego descarta.
GetQueryFilterAsyncAñade las condiciones antes de que se ejecute la consulta, de modo que la paginación y los conteos permanecen correctos por construcción y las filas que un usuario puede no ver nunca se recuperan.
Seguridad a nivel de fila
GetQueryFilterAsync devuelve un filtro que el framework añade a la consulta antes de que Dataverse la ejecute, por lo que las filas que el usuario puede no ver nunca se recuperan. Se aplica a todas las consultas que el portal emite para la tabla — cuadrículas, consultas, subcuadras, gráficos y cargas de un solo registro — y se ejecuta bajo ambos modelos de seguridad, por lo que sigue vigente incluso donde la seguridad nativa de Dataverse evita la puerta de operaciones.
Construye el filtro con context.CreateFilter() y devuélvelo; devolver null significa que no hay restricción de fila. El contexto lleva al usuario actual como EntityReference, los roles que tiene y para qué se van a usar las filas. Las condiciones se añaden a través de la misma FetchXMLBuilder API que se usa en cualquier otro lugar:
FetchXMLBuilder
Este proveedor concede tres columnas de contact y asigna cada consulta de contacto al propio registro del usuario iniciado sesión:
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);
}
}
Leer, actualizar y borrar
TableQueryContext.Operation Explica por qué se van a buscar las filas, para que cada proveedor pueda asignarlas de forma diferente para cada una:
Read— una carga de cuadrilla, consulta, subcuadrícula, gráfico o registro. Las filas que quedan son las filas que el usuario puede ver.Update— la búsqueda previa al vuelo antes de una partida. Las filas que quedan son las filas que el usuario puede cambiar, que normalmente es un conjunto más reducido. "Ver todas las cuentas, editar solo la mía" es esta distinción y nada más.Delete— la búsqueda previa al vuelo antes de un borrado. Más estrecho en la mayoría de los portales: poder corregir un registro no es lo mismo que poder destruirlo.
public Task<TableQueryFilter?> GetQueryFilterAsync(TableQueryContext context)
{
// Todos leen todo; solo las escrituras se envían al propietario.
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);
}
Cómo se combinan varios filtros
Una tabla puede tener más de un proveedor — uno registrado globalmente, más uno de cada rol que desempeñe el usuario. El framework decide cómo se combinan sus filtros, por lo que un proveedor nunca tiene que saber qué hicieron los demás:
- Dentro de un mismo grupo se cruzan. Dos proveedores registrados globalmente, o dos vinculados al mismo rol, están destinados a estar de acuerdo, por lo que una fila debe satisfacer ambos.
- Entre roles se sindicalizan. Tener otro rol amplía las filas que ves, coincidiendo con cómo se componen las operaciones. Cambias
RoleCompositionBehavioraRestrictivey se cruzan en su lugar. - Las uniones se cuidan. Un alias que colisiona con uno que ya usa la consulta se renombra y las referencias del filtro lo siguen; cuando se están uniendo filtros, una unión interna se relaja hacia la externa para que la unión de un rol no pueda excluir las filas que permite otro rol; y una unión que puede devolver el mismo registro más de una vez tiene esos duplicados colapsados.
Nota
Las filas no se cierran, como las columnas: un proveedor que devuelve
null— o un filtro sin condiciones — no concede filas , y dentro de un grupo que vacía el grupo. Cada fila debe pedirse conTableQueryFilter.AllRows. Ahí es donde radica la única asimetría: la unión de cada fila con cualquier cosa es cada fila, así que si algún grupo que concede la tabla respondeAllRows, la tabla vuelve sin filtrar por muy cuidadosamente que los otros roles la hayan definido. Eso es lo que significa composición aditiva, pero es la única forma en que el filtro de un rol puede parecer que está siendo ignorado.
Filtrado a través de una tabla relacionada
AddLink une una tabla para que el filtro pueda probar sus columnas. Coloca la prueba en el filtro en lugar de en el enlace — las condiciones dentro del filtro siguen funcionando cuando el marco relaja la unión a la exterior, y las condiciones en el enlace no:
public Task<TableQueryFilter?> GetQueryFilterAsync(TableQueryContext context)
{
var filter = context.CreateFilter(LogicalOperator.Or);
// El alias se vuelve único si la consulta ya lo usa.
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);
}
Nota
La unión anterior llega hasta muchas filas, así que Dataverse devuelve la cuenta una vez por cada membresía — una cuadrícula que repite filas y un total que las cuenta. El framework reconoce esa forma y marca la consulta
distinct, proyectando la clave primaria de la tabla para que la deduplicación solo pueda eliminar las filas añadidas por la unión. Una unión que en su lugar sube , una a la propia clave primaria de la tabla relacionada, coincide como mucho con una fila y se deja exactamente como estaba. De cualquier forma, escribes el filtro y nada más — pero vale la pena saber que una unión no es gratuita, y que una regla que puedes expresar como condiciones planas en las propias columnas de la tabla es la forma más económica.
Propina
Cuando una tabla necesita un alcance diferente por rol, aún puedes escribir un proveedor y hacer una rama en
context.User.PortalRolesél, pero ya no es necesario. Dos roles pueden llevar cada uno su propio proveedor y el framework suelta sus filtros por ti.
Quién posee un nuevo disco
Un filtro de filas escala lecturas, actualizaciones y eliminaciones, pero un crear no tiene fila que filtrar. Así que una tabla con el alcance de "filas que poseo" seguiría permitiendo que un usuario cree una fila propiedad de otra persona — o de nadie, que su creador entonces no puede ver. Declara la columna que indica quién posee un registro, y el marco lo establece:
// Una cuenta pertenece al contacto que la crea.
builder.Services.AddContactOwnership("account", "primarycontactid");
- Al crear , la columna se establece como el contacto iniciado sesión tras las comprobaciones de permisos, y cualquier valor enviado por la solicitud se descarta. El usuario no necesita acceso de escritura a la columna ni
AppendToacontactla única opción: vincular un registro a uno mismo no es algo en lo que se confíe. - Al actualizar se rechaza una solicitud que cambiaría al propietario, y se ignora otra que repita el propietario almacenado.
- Los usuarios internos (inicios de sesión de systemuser) están exentos: Dataverse hace cumplir su seguridad, y reasignar un registro forma parte de su administración.
Emparejalo con un filtro de fila en la misma columna, de modo que los registros que crea un usuario sean los registros que puede ver. Para la propiedad que no es simplemente el contacto creador — por ejemplo, la cuenta del creador — implévalo ITableRecordOwnershipProvider y regístralo con AddTableRecordOwnershipProvider<T>(). No pongas un propietario en un ITableRecordInterceptor instead: los interceptores se ejecutan antes de las comprobaciones de permisos, por lo que lo que establecen se juzga como la escritura del propio usuario.
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)
{
// Lee como el usuario de la aplicación: a qué empresa pertenece alguien no es la petición para decir.
var contact = await organizationServiceProvider.GetOrganizationService()
.RetrieveAsync("contact", context.CurrentUser.Id, new ColumnSet("parentcustomerid"), context.CancellationToken);
var company = contact.GetAttributeValue<EntityReference>("parentcustomerid");
// Presenta el caso a nombre de su empresa, o a su nombre cuando no tengan ninguno.
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>();
El framework lo llama en cada creación de esa tabla, tras las comprobaciones de permisos, y establece cada columna Columns en lo que devuelve — descartando todo lo que envió la solicitud y rechazando una actualización posterior que lo cambiaría, exactamente como para OwnedBy. Emparejalo con un filtro de fila que coincida con lo que establece, así las personas bajo las que archiva un registro sean las que pueden verlo — aquí RowsMatchingAny(r => r.RowsOfTheirAccount("customerid"), r => r.RowsOwnedByContact("customerid")), con WithoutStamping() , así que el registro deja la columna a este proveedor: dos que configuran una columna de forma diferente fallan la partida.
Seguridad a nivel de columna
La seguridad de columnas es opt-in. Un proveedor concede las columnas que pretende exponer, y todo lo demás — incluida una columna que alguien añade a la tabla el año siguiente — permanece retenido hasta que se nombre. El id principal del registro siempre es legible, ya que un registro que el framework no puede identificar no sirve de nada a nadie.
Las lecturas y escrituras son conjuntos independientes: una columna puede ser legible pero no escribible (el caso "mostrar el límite de créditos, no dejar que lo cambien"), o escribible pero no legible. Nómbralos con Reading, Writing y ReadingAndWriting, o optar por no participar en una dimensión completamente con ReadingAllColumns / WritingAllColumns:
public Task<TablePermissions> GetPermissionsAsync(UserInfo user)
{
// Cada columna era legible; dos de ellas no se pueden escribir.
return Task.FromResult(TablePermissions
.Allowing(TableSecurityPermission.Read | TableSecurityPermission.Write)
.ReadingAllColumns()
.WritingAllColumnsExcept("creditlimit", "creditonhold"));
}
Nota
ReadingAllColumns()es algo que tienes que escribir deliberadamente. No se abre para nada que se añada a la tabla después, que es el intercambio correcto solo cuando la tabla es realmente pública — así que el marco no lo asumirá en tu nombre.
La escritura también puede variar entre crear un registro y cambiar uno. Writing cubre ambos. WritingOnCreate permite al usuario establecer una columna al crear el registro y lo convierte en solo lectura a partir de entonces — la prioridad de un caso, elegida al registrarse. WritingOnUpdate es al revés: cambiable en un registro existente, no establecible al principio. Ambos hacen que la columna también sea legible, como Writing, y ambos existen para TablePermissions un proveedor propio. Reflejan los permisos separados de creación y actualización que concede la seguridad de campo de Dataverse:
builder.Services.AddTableAccess("incident", t => t
.ForSignedInUsers()
.RowsOwnedByContact("customerid")
.Allow(TableSecurityPermission.Read | TableSecurityPermission.Create | TableSecurityPermission.Write)
.Reading("ticketnumber", "statuscode")
.Writing("title", "description") // se establece al crear, se puede cambiar después
.WritingOnCreate("prioritycode") // Establecido al crearlo, solo lectura después
.WritingOnUpdate("resolution")); // Cambiable después, no configurado al principio
El marco impone el resultado en cada ruta que los datos cruzan el límite del servidor:
- Lecturas — una columna retenida se omite completamente del registro o se devuelve con su valor enmascarado, y está oculta de los metadatos de las tablas y columnas de cuadrícula, por lo que un formulario nunca renderiza un editor vinculado a un valor que nunca llegará.
- Consultas — nunca se rechaza una lectura por permisos de columna. Las vistas se comparten rutinariamente entre audiencias, y que un usuario vea una columna que otro no puede no puede evitar convertirse en un error que te obligue a mantener una vista separada por audiencia. En su lugar, se elimina toda referencia a una columna retenida: proyecciones, ordenaciones, condiciones de filtro (junto con el filtro una vez vacía) y agregados o agrupaciones por grupos. Eliminar una condición amplía el conjunto de resultados, pero solo entre filas que el usuario puede leer ya — la visibilidad de filas proviene de los filtros de consulta de los proveedores, que se añaden tras esta pasada y nunca se tocan por ellos.
- Escrituras — a diferencia de las lecturas, una creación o actualización que cambia una columna que el usuario puede no escribir es rechazada en lugar de recortada silenciosamente: una partida rechazada es la única forma de avisar al usuario de que su edición no funcionó, mientras que una lectura restringida es invisible e inofensiva. Una creación se juzga según lo que puede configurarse al crear, y una actualización según lo que puede cambiarse — mediante una sola partida guardada y por un lote.
Nota
La columna principal de id de una tabla nunca se oculta, sea lo que sea que diga el conjunto de lectura. El id es identidad y no datos — ya está en la URL que el llamante usó para acceder al registro — y las claves de fila, enlaces de edición, relaciones de subcuadrícula y eliminación se deshacen sin ella. Una lista de permisos que nombre las columnas que una página necesita y olvida el id desmontaría el portal sin proteger nada.
Cómo se comportan los componentes
Los componentes del framework respetan los permisos de columna por sí solos — no conectas nada. Una columna que el usuario no puede escribir se muestra en modo solo lectura y desactivada dondequiera que aparezca: todos los editores la recogen a través de su resolución compartida de solo lectura, y la cuadrícula no da a la celda un editor en línea, así que vuelve a mostrar texto plano. Una columna suministrada readOnly="false" por el consumidor no la anula — el servidor rechazaría la partida guardada de todas formas.
Una columna que el usuario puede no leer — una enmascarada — se renderiza en <ColumnDisplayValue> lugar de un editor, en ambas pilas. Eso no es solo una cortesia: una columna enmascarada no tiene valor alguno, solo la máscara como su valor formateado , por lo que un editor vinculado a ella se renderizaría en blanco; ColumnDisplayValue lee el valor formateado y así muestra el marcador de posición sea cual sea el tipo de columna. Las columnas retenidas por omisión nunca llegan a un componente — no están en el registro y los metadatos de la tabla no las listan.
Lo que recibe el cliente
Los permisos de columna vienen como un ColumnPermissions mapa — nombre lógico de columna a un ColumnSecurityPermission par de banderas — para las columnas cuyo acceso está restringido, lo que Write significa que la columna puede cambiarse en un registro existente. Todo lo ausente tiene acceso completo, por lo que una tabla sin restricciones no contiene nada en absoluto. Un registro recuperado por sí solo lleva el mapa; los registros recuperados juntos comparten un mapa, enviado una vez sobre la respuesta, ya que los permisos pertenecen al usuario y a la tabla en lugar de a una fila — los clientes de ambas pilas lo vuelven a poner en cada registro, por lo que leer los permisos de un registro funciona igual en cualquier caso. Las respuestas en cuadrícula también las reportan por columna en ResolvedColumn.Permissions. Un registro que se está creando aún no tiene un mapa propio, por lo que el extremo de permisos de tabla también devuelve NewRecordColumnPermissions — la misma forma, juzgada por lo que se puede establecer al crear — y ambas pilas RecordContext lo ponen en un nuevo registro para ti. Las columnas retenidas por omisión están deliberadamente ausentes en todos los mapas — omitir existe para que nada en la respuesta revele la columna, y nombrarla ahí lo desharía de menos. Léelo tú mismo en record.CanReadColumn(column) Blazor, record.CanWriteColumn(column) o los emparejamientos canReadColumn / canWriteColumn ayudantes de @powerportalspro/core React.
Omitir o enmascarar
Por defecto, una columna retenida está enmascarada: el registro no tiene valor para ella y SecurityOptions.WithheldColumnMask (***** por defecto) como su valor formateado, por lo que un formulario o cuadrícula sigue mostrando el campo y deja claro que el valor está retenido en lugar de en blanco. La máscara es la misma tenga o no valor de la columna, así que también lo oculta. En cambio, configúrase SecurityOptions.WithheldColumnBehavior para Omit eliminar la columna completamente del registro, como si la consulta nunca lo hubiera pedido, por lo que nada en la respuesta revela que la columna existe — la elección correcta cuando la propia existencia de la columna es sensible. Retener una columna también retiene cualquier columna que la republique — el compañero de nombre de una consulta, el gemelo de moneda base de una columna de dinero — por lo que una lista de denegación que nombre una columna no puede ser evitada a través de otra. Una sola tabla puede anular la elección a nivel de sitio a través TableColumnPermissions.WithheldColumnBehaviorde .
builder.Services.Configure<SecurityOptions>(options =>
{
options.WithheldColumnBehavior = WithheldColumnBehavior.Mask;
options.WithheldColumnMask = "*****";
});
Nota
Empareja una columna enmascarada con una denegación de escritura. Una columna enmascarada no tiene valor en el registro, así que un llamante que publica un registro cargado completo no envía nada por él — pero el framework acepta una carga útil guardada al pie de la letra, así que una solicitud que establece la columna, incluida la máscara, escribe sobre el valor almacenado si la columna es escribible. La denegación de escritura es lo que la rechaza.
Columnas que republican otra columna
Retener una columna también retiene cualquier columna que tenga el mismo valor bajo un segundo nombre. Dataverse crea estas para ti: una consulta …name y …yominame socios, la etiqueta de una elección y un gemelo de moneda base para cada columna de moneda — creditlimit_base junto creditlimitcon . Son columnas ordinarias, así que una denominación creditlimit de lista de denegados no dice nada sobre creditlimit_base, y sin esto una cantidad enmascarada volvería directamente a través de su gemela. El enlace proviene de los propios metadatos de la plataforma en lugar de una regla de nombre, por lo que también se cumple para tus propias columnas, y se aplica en lecturas, en una consulta que nombra explícitamente al compañero, en metadatos de tabla, en columnas de cuadrícula y en guardados.
Buenas prácticas — filtrar en la fuente
Un caso que el framework no puede ver para ti: una columna calculada o de agrupación, y las columnas compuestas de la plataforma —
fullname,yomifullname,address1_compositey sus hermanas. Dataverse no informa qué columnas las alimentan, así que retenerlastnamenofullnameretiene , y una columna calculada sobre una columna que has denegado seguirá mostrando su resultado. Nómbralos tú mismo en el conjunto de lectura cuando sus entradas sean sensibles. Una lista de permisos (ColumnPermissions.Allow(…)) evita el problema por completo, ya que nada es legible hasta que lo nombras — que es el mejor valor por defecto para una tabla que contiene cualquier cosa que valga la pena retener.
Nota
Cuando varios proveedores cubren una tabla, sus conjuntos de columnas se componen igual que sus operaciones: de forma aditiva entre roles y bajo
MultiplePermissionHandlersBehaviordentro de un mismo grupo. Un proveedor que existe solo para definir filas y no dice nada sobre columnas queda completamente fuera del pliegue de columnas, por lo que no amplía ni reduce lo que los demás concedieron.
Dataverse-Native Security (Vista previa)
El modelo de seguridad recomendado para el futuro permite que Dataverse imponga la seguridad para los usuarios de tu portal, basado en la función unificada de seguridad de Power Pages. En lugar de escribir manejadores de permisos en código, gestionas el acceso con roles web y permisos de tabla de Power Pages, que la plataforma convierte en roles de seguridad de Dataverse que Dataverse aplica en cada solicitud. El modelo está actualmente en versión preliminar — tanto la función de Microsoft como el soporte de Power Portals Pro — y Power Portals Pro seguirá el calendario de lanzamientos de Microsoft hasta la disponibilidad general.
Cuando está habilitado, PowerPortalsPro provisiona automáticamente un sistema gestionado por el sistema (C2) systemuser para cada contacto del portal — al registrarse para nuevos contactos y de forma perezosa al iniciar sesión para los existentes — junto con el powerpagesusermapping registro que vincula el contacto, el usuario del sistema y tu sitio Power Pages. Cada llamada de Dataverse para la sesión se hace pasar por ese usuario del sistema, por lo que los roles de seguridad que Power Pages sincroniza desde tus permisos web de roles y tablas gobiernan exactamente lo que el usuario puede ver y hacer. La sesión lleva un PortalUserType de ContactStubUser para que tu interfaz siempre pueda distinguir a estos usuarios provisionados de los usuarios internos reales.
Nota
Para usar este modelo de seguridad debes crear un sitio web de Power Pages en tu entorno Dataverse — esto es lo que instala los componentes del framework de Microsoft y te permite activar la función de seguridad unificada. No necesitas un plan de pago de PowerPages, y puedes eliminar la web inmediatamente después de instalar y activar la función. El registro del sitio de Power Pages (
powerpagesite) al quePowerPagesSiteIdse refiere debe existir en el entorno, ya que los mapeos de usuario y la sincronización de seguridad-rol se resuelven en su contra.
Habilitación de la seguridad nativa de Dataverse
Activa el modelo desde SecurityOptions el Program.csservidor , proporcionando el id del registro de tu sitio de Power Pages:
builder.Services.Configure<SecurityOptions>(options =>
{
options.DataverseSecurity.ProvisionSystemUsers = true;
options.DataverseSecurity.PowerPagesSiteId = new Guid("<your powerpagesite id>");
});
La configuración se valida al iniciar la aplicación: cuando ProvisionSystemUsers está habilitada, PowerPagesSiteId debe establecerse y debe resolverse a un registro existente powerpagesite , de lo contrario la aplicación no arranca con guía.
Concede acceso exactamente como lo hace Power Pages: configura roles web y permisos de tablas — en la app Power Pages Management o en la página de Roles Web de tu portal desde la solución Power Portals Pro Enhanced Authorization — y asigna roles web a los contactos (a través de la app Management, de forma programática o automática para nuevos usuarios con reglas en esa misma página). La plataforma sincroniza esos roles de seguridad auto-generados (nombrados PowerPages_*) y los asigna automáticamente a medida que cambia la membresía de rol web — como la configuración está completamente en Power Pages, tu portal puede moverse hacia o desde un sitio real de Power Pages sin fricciones. Ten en cuenta que los cambios de roles y privilegios pueden tardar desde unos segundos hasta un par de minutos en propagarse a través de las cachés de Dataverse antes de hacer efecto.
Sigue la guía paso a paso de configuración
Emparejando un inicio de sesión externo con un contacto
Cuando alguien inicia sesión con Microsoft, Google u otro proveedor externo, el portal tiene que averiguar a qué contacto existente — si es que tiene alguno — ese inicio de sesión. Esto se resuelve en dos pasos:
- Un enlace existente. Si la cuenta ha iniciado sesión antes, una
adx_externalidentityfila ya la vincula a un contacto. Esa es una declaración explícita y almacenada de "esta cuenta es esta persona", así que siempre gana y nunca se ve afectada por la configuración que aparece abajo. - El respaldo del correo electrónico. Sin un enlace todavía, el portal coincide con la dirección de correo electrónico que proporcionó el proveedor con las columnas del contacto. Este es el paso que puedes configurar — y el paso que decide si un cliente recurrente se une a su contacto actual o recibe un duplicado.
Por defecto se buscan dos columnas, primero el nombre de usuario del portal. Los contactos creados a través del portal llevan adx_identity_username, mientras que los contactos importados o introducidos por el personal suelen tener solo emailaddress1 — buscar en ambos es lo que permite a un cliente existente iniciar sesión y aparecer en su propio registro en lugar de uno nuevo:
builder.Services.Configure<SecurityOptions>(options =>
{
// The default: portal username first, then the primary email column.
options.ContactMatching.ExternalLoginEmailColumns =
[
ContactEmailColumn.IdentityUsername,
ContactEmailColumn.EmailAddress1,
];
});
El orden importa: es la precedencia, no solo una lista. Las columnas se buscan juntas en una sola consulta, y la primera columna de tu lista que coincide con algo decide el ganador — así que si un contacto tiene la dirección como nombre de usuario del portal y otro la tiene en emailaddress1, la coincidencia de nombre de usuario gana bajo el orden por defecto.
Lista menos columnas para la coincidencia estrecha: [ContactEmailColumn.IdentityUsername] lo restringe a cuentas creadas por el portal, y una lista vacía desactiva por completo el respaldo del correo, dejando un enlace de identidad externa existente como la única forma en que una cuenta puede resolver a un contacto. Añade ContactEmailColumn.EmailAddress2 y ContactEmailColumn.EmailAddress3 amplía la relación — se omiten deliberadamente del predeterminado por la razón siguiente.
Buenas prácticas — filtrar en la fuente
Cada columna que listes es una forma de que quien controle una dirección de correo electrónico sea reconocido como el contacto que la posee. Solo lista las columnas que tu proceso de entrada de datos trate como pertenecientes al propio contacto — si
emailaddress2es donde tu equipo archiva la dirección de un asistente o una bandeja de entrada compartida de la empresa, listarla permite que quien lea esa bandeja inicie sesión como ese contacto.
Buenas prácticas — filtrar en la fuente
La coincidencia convierte la dirección de correo electrónico en un identificador, no solo en una etiqueta: decide en qué contacto existente se convierte un inicio de sesión. Así que el portal solo omite el correo de confirmación cuando el proveedor de identidad realmente verificó esa dirección exacta. Si la dirección fue escrita sobre la proporcionada por el proveedor, o el proveedor nunca la verificó, se envía un enlace de confirmación y el inicio de sesión externo no se asocia al contacto hasta que ese enlace se sigue.
Qué inicios de sesión cuentan como verificados no es propiedad del proveedor. Google verifica cada dirección que afirma. Microsoft no: una cuenta personal de Microsoft verificó su dirección, pero una cuenta de trabajo o escuela de Entra proporciona lo que esté en el atributo del
verified_primary_emailreclamaciónverified_secondary_emailla lista, oxms_edovdice que su dominio pertenece al propio inquilino del usuario. Entra envía esas reclamaciones solo cuando el registro de la app las solicita (véase Configuración de inicio de sesión de Entra ID); sin ellas, los inicios de sesión en el trabajo y la escuela confirman por correo electrónico la primera vez.
Nota
Estas configuraciones solo se aplican al inicio de sesión externo. El inicio de sesión local de nombre de usuario/contraseña, registro y restablecimiento de contraseña siguen coincidiendo
adx_identity_usernamede forma independiente, deliberadamente — el flujo de restablecimiento de contraseña envía su enlace a la dirección introducida, así que coincidir con columnas secundarias de correo allí permitiría restablecer la contraseña de otra persona.
Permitir que solo los contactos existentes inicien sesión
Por defecto, cualquiera puede convertirse en usuario del portal: registrarse o iniciar sesión con un proveedor externo bajo una dirección que no tiene contacto crea un contacto. Cuando el portal es para personas que ya tienes en Dataverse — clientes, miembros, socios — desactiva el registro y solo ellos pueden iniciar sesión:
builder.Services.Configure<AuthClientOptions>(options => options.SignUpEnabled = false);
La página de registro y sus enlaces desaparecen, y el servidor se niega a crear un usuario como se le pida. Un inicio de sesión externo sigue encontrando su contacto como se ha descrito arriba y enlaza a él; uno que no coincide con nadie es rechazado en lugar de crear un contacto. Las cuentas locales no se ven afectadas para quienes ya tienen uno.
Nota
Un inicio de sesión rechazado solo se explica en pantalla cuando el proveedor ha verificado esa dirección exacta, porque entonces la persona que inicia sesión ya ha demostrado que es la propietaria. Para cualquier otra dirección, el portal responde exactamente igual que para una dirección que tiene un contacto — "revisa tu correo electrónico" — y envía la dirección con un aviso breve de que no está registrada. Informarlo directamente al visitante permitiría que cualquiera escriba direcciones en el formulario para saber quién tiene una cuenta.
El aviso se envía a través del propio remitente del portal. Un host que lo sustituya por su propia pila de correo también debería registrar un
ISignUpRefusalEmailSender; sin uno, el rechazo se registra y no se envía ningún aviso.
Clases base integradas
El marco proporciona clases base para simplificar patrones de seguridad comunes:
AnonymousPermissionHandler— concede una tabla a todos, hayan iniciado sesión o no, incluyendo cada columna. Es el único lugar donde se renuncia al impago opt-in, porque derivar de él es la decisión de publicar la tabla. AnularGetPermissionsAsyncpara publicar menos.
Referencia API
AuthorizePortalRoleAttribute Clase
Propiedades
Nombre | Tipo | Default | Descripción |
|---|---|---|---|
AuthenticationSchemes | string? | ||
Policy | string? | ||
RoleNames | IReadOnlyList<string> | Los nombres de los roles que requiere este atributo, cualquiera de los cuales lo satisface. | |
Roles | string? | ||
TypeId | Object |
AuthenticationSchemesPolicyRoleNamesRolesTypeIdMétodos
Nombre | Parámetros | Tipo | Descripción |
|---|---|---|---|
IsDefaultAttribute | bool | ||
Match | Object obj | bool |
IsDefaultAttributeMatchITablePermissionProvider Interface
Propiedades
Nombre | Tipo | Default | Descripción |
|---|---|---|---|
Table | string | Nombre lógico de la tabla a la que se aplica este proveedor. Coincide exactamente, así que debe ser el Nombre lógico de Dataverse en minúsculas. |
TableMétodos
Nombre | Parámetros | Tipo | Descripción |
|---|---|---|---|
GetPermissionsAsync | UserInfo user | Task<TablePermissions> | Qué user puede hacer con esta tabla y cuál de sus columnas pueden leer y escribir. Volver TablePermissions.None para un usuario que no debería tener acceso. TableSecurityPermission.Read si el usuario puede ver alguno de los tablones filas y alcance cuáles son en Services.TableQueryContext). TableSecurityPermission.Read Alone solo expone el ID del registro. Usar Reading(System.String[]) y Writing(System.String[]) nombrarles, o TablePermissions.AllColumns para optar por no tener la seguridad de columna en esta tabla. |
GetQueryFilterAsync | TableQueryContext context | Task<TableQueryFilter> | Ámbitos en qué filas de esta tabla el usuario puede actuar: un filtro construido con Query.LogicalOperator), TableQueryFilter.AllRows para cada rema según las operaciones permitan o nullpara no tener filas. null — o un filtro sin condiciones — no concede ninguna, así que Olvidar poner el alcance de una tabla lo niega en lugar de publicar cada fila. Por ejemplo, TableQueryFilter.AllRows Cuando cada disputa es lo que quieres decir. TableQueryContext.Operation distingue. También ejecuta bajo ambos modelos de seguridad, por lo que sigue vigente cuando la seguridad nativa de Dataverse evita Services.UserInfo). PortalRoleOptions.CompositionBehavior a RoleCompositionBehavior.Restrictive Que también se cruzen entre roles. null responde no deja filas al grupo, y TableQueryFilter.AllRows deja que los filtros de los demás proveedores decidan. A lo largo Grupos, un grupo que concede TableQueryFilter.AllRows amplía el aditivo por defecto Unión a cada fila — así que el filtro de un rol solo significa algo cuando los otros grupos conceden Ese filtro de mesa también. |
GetPermissionsAsyncuser puede hacer con esta tabla y cuál de sus columnas pueden leer y escribir. Volver TablePermissions.None para un usuario que no debería tener acceso. TableSecurityPermission.Read si el usuario puede ver alguno de los tablones filas y alcance cuáles son en Services.TableQueryContext). TableSecurityPermission.Read Alone solo expone el ID del registro. Usar Reading(System.String[]) y Writing(System.String[]) nombrarles, o TablePermissions.AllColumns para optar por no tener la seguridad de columna en esta tabla. GetQueryFilterAsyncQuery.LogicalOperator), TableQueryFilter.AllRows para cada rema según las operaciones permitan o nullpara no tener filas. null — o un filtro sin condiciones — no concede ninguna, así que Olvidar poner el alcance de una tabla lo niega en lugar de publicar cada fila. Por ejemplo, TableQueryFilter.AllRows Cuando cada disputa es lo que quieres decir. TableQueryContext.Operation distingue. También ejecuta bajo ambos modelos de seguridad, por lo que sigue vigente cuando la seguridad nativa de Dataverse evita Services.UserInfo). PortalRoleOptions.CompositionBehavior a RoleCompositionBehavior.Restrictive Que también se cruzen entre roles. null responde no deja filas al grupo, y TableQueryFilter.AllRows deja que los filtros de los demás proveedores decidan. A lo largo Grupos, un grupo que concede TableQueryFilter.AllRows amplía el aditivo por defecto Unión a cada fila — así que el filtro de un rol solo significa algo cuando los otros grupos conceden Ese filtro de mesa también. TablePermissions Clase
Propiedades
Nombre | Tipo | Default | Descripción |
|---|---|---|---|
Columns | TableColumnPermissions | Las columnas que el usuario puede leer y escribir. TableColumnPermissions.None a menos que Algo llamado ellos: Se concede acceso a columnas, nunca se asume. | |
Operations | TableSecurityPermission | Las operaciones que el usuario puede realizar sobre la tabla como un valor de bandera. Esto es un valor a nivel de tabla gate, no un filtro de fila — incluyelo TableSecurityPermission.Read si el usuario puede ver cualquiera de las filas de la tabla y ampliar cuáles pasan por Services.TableQueryContext). |
ColumnsTableColumnPermissions.None a menos que Algo llamado ellos: Se concede acceso a columnas, nunca se asume.OperationsTableSecurityPermission.Read si el usuario puede ver cualquiera de las filas de la tabla y ampliar cuáles pasan por Services.TableQueryContext).Métodos
Nombre | Parámetros | Tipo | Descripción |
|---|---|---|---|
AllColumns | TablePermissions | Opta por no participar en la seguridad a nivel de columna — cada columna es legible y se puede escribir. El Comportamiento previo a la seguridad de columnas, explicado en lugar de heredado. | |
Allow | TableSecurityPermission operations | TablePermissions | Añade operations a lo que este valor ya aporta. |
Allowing | TableSecurityPermission operations | TablePermissions | Concesiones operations y sin columnas. Cadena Reading(System.String[]), Writing(System.String[]) O TablePermissions.ReadingAllColumns para abrir columnas. |
Reading | string[] columns | TablePermissions | Permite leer las columnas nombradas, además de las ya permitidas. Insensible a mayúsculas y mayúsculas, Así que la ortografía del portal del creador funciona tan bien como el nombre lógico. |
ReadingAllColumns | TablePermissions | Opta por no tener seguridad de lectura a nivel de columna para esta tabla: cada columna, incluidas las añadidas a la mesa más tarde. Deliberadamente explícito — el marco no lo asumirá. | |
ReadingAllColumnsExcept | string[] columns | TablePermissions | Opta por no participar en la seguridad de lectura a nivel de columna excepto para las columnas nombradas — todas las columnas excepto Estos, incluidos los añadidos después. No se abre para nada nuevo, así que prefiero Reading(System.String[]) en tablas que contienen datos sensibles. |
ReadingAndWriting | string[] columns | TablePermissions | Permite tanto leer como escribir las columnas nombradas. |
WithColumns | TableColumnPermissions columns | TablePermissions | Sustituye las reglas de columna al por mayor, para un proveedor que preferiría construir un Services.TableColumnPermissions a sí mismo que encadenar los métodos fluidos. |
Withholding | WithheldColumnBehavior behavior | TablePermissions | Cómo las columnas retenidas de esta tabla vuelven — omitidas, o presentadas con un valor enmascarado. Anula la regla de todo SecurityOptions.WithheldColumnBehavior el sitio para esta tabla. |
Writing | string[] columns | TablePermissions | Permite escribir las columnas nombradas — cuando se crea un registro y cuando se actualiza — en Además de cualquiera ya permitida. Escribir una columna no implica leerla — nombrala en Reading(System.String[]) también si el usuario ve lo que escribieron. |
WritingAllColumns | TablePermissions | Opta por no tener seguridad de escritura a nivel de columna para esta tabla: el usuario puede escribir cualquier columna, al crear y al actualizar. Reemplaza ambos conjuntos de escritura, así que llama WritingOnCreate(System.String[]) / WritingOnUpdate(System.String[]) después, no antes. | |
WritingAllColumnsExcept | string[] columns | TablePermissions | Opta por no participar en la seguridad de escritura a nivel de columna, excepto en las columnas nombradas, que permanecen en modo solo lectura. Reemplaza ambos conjuntos de escritura, así que llama WritingOnCreate(System.String[]) / WritingOnUpdate(System.String[]) Después de ella, no antes. |
WritingOnCreate | string[] columns | TablePermissions | Permite establecer las columnas nombradas cuando se crea un registro, y solo entonces: una vez que exista son de solo lectura, incluso si una llamada anterior — Writing(System.String[]), TablePermissions.WritingAllColumns — los hacía escribibles. La prioridad de un caso, elegido cuando lo es Registrado y corregido a partir de entonces. Llámalo después de cualquier Writing(System.String[]), no implica lectura. |
WritingOnUpdate | string[] columns | TablePermissions | Permite cambiar las columnas nombradas en un registro existente, pero no configurarlas cuando está creadas, incluso si una llamada anterior las hiciera escribibles — notas de resolución que solo significan algo que una vez que el caso existe. Llámalo después de cualquier Writing(System.String[]), no implica lectura. |
AllColumnsAllowoperations a lo que este valor ya aporta.Allowingoperations y sin columnas. Cadena Reading(System.String[]), Writing(System.String[]) O TablePermissions.ReadingAllColumns para abrir columnas.ReadingReadingAllColumnsReadingAllColumnsExceptReading(System.String[]) en tablas que contienen datos sensibles.ReadingAndWritingWithColumnsServices.TableColumnPermissions a sí mismo que encadenar los métodos fluidos.WithholdingSecurityOptions.WithheldColumnBehavior el sitio para esta tabla.WritingReading(System.String[]) también si el usuario ve lo que escribieron.WritingAllColumnsWritingOnCreate(System.String[]) / WritingOnUpdate(System.String[]) después, no antes.WritingAllColumnsExceptWritingOnCreate(System.String[]) / WritingOnUpdate(System.String[]) Después de ella, no antes.WritingOnCreateWriting(System.String[]), TablePermissions.WritingAllColumns — los hacía escribibles. La prioridad de un caso, elegido cuando lo es Registrado y corregido a partir de entonces. Llámalo después de cualquier Writing(System.String[]), no implica lectura.WritingOnUpdateWriting(System.String[]), no implica lectura.AuthClientOptions Clase
Propiedades
Nombre | Tipo | Default | Descripción |
|---|---|---|---|
LocalAccountsEnabled | bool | True | Si el portal acepta cuentas locales de usuario/contraseña. Por |
SignUpEnabled | bool | True | Si iniciar sesión puede crear un usuario del portal. Por |
LocalAccountsEnabledSignUpEnabled