Compartilhar via


Grupos do Microsoft Entra (ME-ID), Funções de Administrador e Funções de Aplicativo

Este artigo explica como configurar Blazor WebAssembly para usar grupos e funções do ME-ID (Microsoft Entra ID).

O ME-ID fornece várias abordagens de autorização que podem ser combinadas com ASP.NET Core Identity:

  • Grupos
    • Segurança
    • Microsoft 365
    • Distribuição
  • Funções
    • Funções internas de administrador do ME-ID
    • Funções de Aplicativo

As diretrizes neste artigo se aplicam aos cenários de implantação do ME-ID Blazor WebAssembly descritos nos seguintes artigos:

Os exemplos deste artigo aproveitam os novos recursos do .NET/C#. Quando você usar os exemplos com o .NET 7 ou versões anteriores, pequenas modificações serão necessárias. No entanto, os exemplos de texto e código que pertencem à interação com o ME-ID e o Microsoft Graph são os mesmos para todas as versões do ASP.NET Core.

Aplicativo de exemplo

Acesse o aplicativos de exemplo, chamado BlazorWebAssemblyEntraGroupsAndRoles, pela pasta da versão mais recente na raiz do repositório com o link a seguir. O exemplo é fornecido para o .NET 8 ou posteriores. Consulte o arquivo README do aplicativo de exemplo para ver as etapas para executar o aplicativo.

O aplicativo de exemplo inclui um componente UserClaims para exibir as declarações de um usuário. O componente UserData exibe as propriedades básicas da conta do usuário.

Exibir ou baixar código de exemplo (como baixar)

Pré-requisito

As diretrizes neste artigo implementam a API do Microsoft Graph de acordo com as diretrizes do SDK do Graph em Usar a API do Graph com ASP.NET Core Blazor WebAssembly. Siga as diretrizes de implementação do SDK do Graph para configurar o aplicativo e testá-lo para confirmar se o aplicativo pode obter dados da API do Graph para uma conta de usuário de teste. Além disso, consulte os links cruzados do artigo de segurança do artigo API do Graph para examinar os conceitos de segurança do Microsoft Graph.

Ao testar com o SDK do Graph localmente, recomendamos o uso de uma nova sessão do navegador InPrivate/incógnito para cada teste a fim de evitar que os cookies persistentes interfiram nos testes. Para obter mais informações, consulte Proteger um aplicativo autônomo ASP.NET Core Blazor WebAssembly com contas do Microsoft Entra ID.

Ferramentas online de registro de aplicativos do ME-ID

Este artigo refere-se ao portal do Azure ao solicitar que você configure o registro do aplicativo do ME-ID, mas o Centro de Administração do Microsoft Entra também é uma opção viável para gerenciar registros de aplicativos do ME-ID. Qualquer interface pode ser usada, mas as diretrizes neste artigo abordam especificamente gestos do portal do Azure.

Escopos

Permissões e escopos significam a mesma coisa e são usados de forma intercambiável na documentação de segurança e no portal do Azure. A menos que o texto esteja se referindo ao portal do Azure, este artigo usa escopo/escopos ao se referir a permissões do Graph.

Os escopos não diferenciam maiúsculas de minúsculas, portanto, User.Read e user.read são a mesma coisa. Sinta-se à vontade para usar qualquer um dos formatos, mas recomendamos uma escolha consistente no código do aplicativo.

Para permitir que a API do Microsoft Graph chame um perfil de usuário, atribuição de função e dados de associação de grupo, o aplicativo é configurado com o escopo User.Read delegado (https://graph.microsoft.com/User.Read) no portal do Azure porque o acesso aos dados de usuário de leitura é determinado pelos escopos concedidos (delegados) a usuários individuais. Este escopo é necessário além dos escopos necessários nos cenários de implantação do ME-ID descritos nos artigos listados anteriormente (Autônomo com contas Microsoft ou Autônomo com o ME-ID).

Os escopos adicionais necessários incluem:

  • Escopo RoleManagement.Read.Directory delegado (https://graph.microsoft.com/RoleManagement.Read.Directory): permite que o aplicativo leia as configurações de RBAC (controle de acesso baseado em função) do diretório da sua empresa em nome do usuário conectado. Isso inclui a leitura de modelos de função do diretório, funções do diretório e associações. As associações de função do diretório são usadas para criar declarações directoryRole no aplicativo para Funções internas de administrador do ME-ID. É necessário o consentimento do administrador.
  • Escopo AdministrativeUnit.Read.All delegado (https://graph.microsoft.com/AdministrativeUnit.Read.All): permite que o aplicativo leia unidades administrativas e a associação de unidade administrativa em nome do usuário conectado. Essas associações são usadas para criar declarações administrativeUnit no aplicativo. É necessário o consentimento do administrador.

Para obter mais informações, consulte Visão geral das permissões e consentimento na plataforma de identity da Microsoft e Visão geral das permissões do Microsoft Graph.

Conta de usuário personalizada

Atribua usuários a grupos de segurança do ME-ID e funções de administrador do ME-ID no portal do Azure.

Os exemplos neste artigo:

  • Pressupõem que um usuário seja atribuído à função Administrador de cobrança do ME-ID no locatário do ME-ID do portal do Azure para autorização para acessar dados da API do servidor.
  • Use políticas de autorização para controlar o acesso no aplicativo.

Estenda RemoteUserAccount para incluir propriedades para:

CustomUserAccount.cs:

using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

namespace BlazorWebAssemblyEntraGroupsAndRoles;

public class CustomUserAccount : RemoteUserAccount
{
    [JsonPropertyName("roles")]
    public List<string>? Roles { get; set; }

    [JsonPropertyName("oid")]
    public string? Oid { get; set; }
}

Adicione uma referência de pacote para Microsoft.Graph.

Observação

Para obter diretrizes sobre como adicionar pacotes a aplicativos .NET, consulte os artigos em Instalar e gerenciar pacotes no Fluxo de trabalho de consumo de pacotes (documentação do NuGet). Confirme as versões corretas de pacote em NuGet.org.

Adicione as classes e a configuração do utilitário do SDK do Graph nas diretrizes do SDK do Graph do artigo Usar A API do Graph com ASP.NET CoreBlazor WebAssembly. Especifique os escopos User.Read, RoleManagement.Read.Directory e AdministrativeUnit.Read.All do token de acesso, como mostra o artigo no arquivo de exemplo wwwroot/appsettings.json.

Adicione a fábrica de conta de usuário personalizada a seguir ao aplicativo. A fábrica de usuários personalizada é usada para estabelecer:

  • Declarações de Função de Aplicativo (role) (abordadas na seção Funções de Aplicativo).

  • Exemplo de declarações de dados de perfil de usuário para o número de telefone celular do usuário (mobilePhone) e o local do escritório (officeLocation).

  • Declarações de função de administrador do ME-ID (directoryRole).

  • Declarações da Unidade administrativa do ME-ID (administrativeUnit).

  • Declarações de grupo do ME-ID (directoryGroup).

  • Um ILogger (logger) para conveniência, caso você deseje registrar informações ou erros.

CustomAccountFactory.cs:

using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions.Authentication;

namespace BlazorWebAssemblyEntraGroupsAndRoles;

public class CustomAccountFactory(IAccessTokenProviderAccessor accessor,
        IServiceProvider serviceProvider, ILogger<CustomAccountFactory> logger,
        IConfiguration config)
    : AccountClaimsPrincipalFactory<CustomUserAccount>(accessor)
{
    private readonly ILogger<CustomAccountFactory> logger = logger;
    private readonly IServiceProvider serviceProvider = serviceProvider;
    private readonly string? baseUrl = string.Join("/",
        config.GetSection("MicrosoftGraph")["BaseUrl"],
        config.GetSection("MicrosoftGraph")["Version"]);

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account,
        RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity is not null &&
            initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = initialUser.Identity as ClaimsIdentity;

            if (userIdentity is not null && !string.IsNullOrEmpty(baseUrl) &&
                account.Oid is not null)
            {
                account?.Roles?.ForEach((role) =>
                {
                    userIdentity.AddClaim(new Claim("role", role));
                });

                try
                {
                    var client = new GraphServiceClient(
                        new HttpClient(),
                        serviceProvider
                            .GetRequiredService<IAuthenticationProvider>(),
                        baseUrl);

                    var user = await client.Me.GetAsync();

                    if (user is not null)
                    {
                        userIdentity.AddClaim(new Claim("mobilephone",
                            user.MobilePhone ?? "(000) 000-0000"));
                        userIdentity.AddClaim(new Claim("officelocation",
                            user.OfficeLocation ?? "Not set"));
                    }

                    var memberOf = client.Users[account?.Oid].MemberOf;

                    var graphDirectoryRoles = await memberOf.GraphDirectoryRole.GetAsync();

                    if (graphDirectoryRoles?.Value is not null)
                    {
                        foreach (var entry in graphDirectoryRoles.Value)
                        {
                            if (entry.RoleTemplateId is not null)
                            {
                                userIdentity.AddClaim(
                                    new Claim("directoryRole", entry.RoleTemplateId));
                            }
                        }
                    }

                    var graphAdministrativeUnits = await memberOf.GraphAdministrativeUnit.GetAsync();

                    if (graphAdministrativeUnits?.Value is not null)
                    {
                        foreach (var entry in graphAdministrativeUnits.Value)
                        {
                            if (entry.Id is not null)
                            {
                                userIdentity.AddClaim(
                                    new Claim("administrativeUnit", entry.Id));
                            }
                        }
                    }

                    var graphGroups = await memberOf.GraphGroup.GetAsync();

                    if (graphGroups?.Value is not null)
                    {
                        foreach (var entry in graphGroups.Value)
                        {
                            if (entry.Id is not null)
                            {
                                userIdentity.AddClaim(
                                    new Claim("directoryGroup", entry.Id));
                            }
                        }
                    }
                }
                catch (AccessTokenNotAvailableException exception)
                {
                    exception.Redirect();
                }
            }
        }

        return initialUser;
    }
}

O código anterior:

  • Não inclui associações transitivas. Se o aplicativo exigir declarações de associação de grupo diretas e transitivas, substitua a propriedade MemberOf (IUserMemberOfCollectionWithReferencesRequestBuilder) por TransitiveMemberOf (IUserTransitiveMemberOfCollectionWithReferencesRequestBuilder).
  • Define valores GUID em declarações directoryRole como IDs de modelo da função de administrador do ME-ID (Microsoft.Graph.Models.DirectoryRole.RoleTemplateId). As IDs de modelo são identificadores estáveis para criar políticas de autorização de usuário em aplicativos, o que é abordado posteriormente neste artigo. Não use entry.Id para valores de declaração de função do diretório, pois eles não são estáveis nos locatários.

Em seguida, configure a autenticação MSAL para usar a fábrica de contas de usuário personalizada.

Confirme se o arquivo Program usa o namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

Atualize a chamada AddMsalAuthentication para o seguinte. Observe que o RemoteUserAccount da estrutura Blazor é substituído pelo CustomUserAccount do aplicativo para a fábrica de entidades de segurança de declarações de conta e autenticação da MSAL:

builder.Services.AddMsalAuthentication<RemoteAuthenticationState,
    CustomUserAccount>(options =>
    {
        builder.Configuration.Bind("AzureAd",
            options.ProviderOptions.Authentication);
        options.UserOptions.RoleClaim = "role";
    })
    .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, CustomUserAccount,
        CustomAccountFactory>();

Confirme a presença do código do SDK do Graph no arquivo Program descrito pelo artigo Usar a API do Graph com ASP.NET Core Blazor WebAssembly:

var baseUrl =
    string.Join("/",
        builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
            "https://graph.microsoft.com",
        builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
            "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);

Importante

Confirme no registro do aplicativo no portal do Azure se as seguintes permissões foram concedidas:

  • User.Read
  • RoleManagement.Read.Directory (requer consentimento do administrador)
  • AdministrativeUnit.Read.All (requer consentimento do administrador)

Confirme se a configuração wwwroot/appsettings.json está correta de acordo com as diretrizes do SDK do Graph.

wwwroot/appsettings.json:

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": true
  },
  "MicrosoftGraph": {
    "BaseUrl": "https://graph.microsoft.com",
    "Version": "v1.0",
    "Scopes": [
      "User.Read",
      "RoleManagement.Read.Directory",
      "AdministrativeUnit.Read.All"
    ]
  }
}

Forneça valores para os seguintes espaços reservados do registro do ME-ID do aplicativo no portal do Azure:

  • {TENANT ID}: o valor da GUID da ID do diretório (locatário).
  • {CLIENT ID}: o valor da GUID da ID do aplicativo (cliente).

Configuração de autorização

Crie uma política para cada Função de aplicativo (por nome de função), Função de administrador interno do ME-ID (por ID/GUID do Modelo de função) ou grupo de segurança (por ID/GUID de objeto) no arquivo Program. O exemplo a seguir cria uma política para a função integrada Administrador de cobrança do ME-ID:

builder.Services.AddAuthorizationCore(options =>
{
    options.AddPolicy("BillingAdministrator", policy => 
        policy.RequireClaim("directoryRole", 
            "b0f54661-2d74-4c50-afa3-1ec803f12efe"));
});

Para acessar a lista completa de IDs (GUIDS) das Funções de administrador do ME-ID, consulte IDs de modelo de função na documentação do ME-ID. Para obter uma GUID (ID de grupo) de segurança do Azure ou do O365, consulte a ID do objeto do grupo no painel Grupos do portal do Azure do registro do aplicativo. Para obter mais informações sobre as políticas de autorização, consulte Autorização baseada em política no ASP.NET Core.

Nos exemplos a seguir, o aplicativo usa a política anterior para autorizar o usuário.

O componente AuthorizeView funciona com a política:

<AuthorizeView Policy="BillingAdministrator">
    <Authorized>
        <p>
            The user is in the 'Billing Administrator' ME-ID Administrator Role
            and can see this content.
        </p>
    </Authorized>
    <NotAuthorized>
        <p>
            The user is NOT in the 'Billing Administrator' role and sees this
            content.
        </p>
    </NotAuthorized>
</AuthorizeView>

O acesso a um componente inteiro pode ser baseado na política usando uma diretiva de atributo [Authorize] (AuthorizeAttribute):

@page "/"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "BillingAdministrator")]

Se o usuário não estiver autorizado, ele será redirecionado para a página de entrada do ME-ID.

Uma verificação de política também pode ser executada no código com lógica de procedimento.

CheckPolicy.razor:

@page "/checkpolicy"
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

<h1>Check Policy</h1>

<p>This component checks a policy in code.</p>

<button @onclick="CheckPolicy">Check 'BillingAdministrator' policy</button>

<p>Policy Message: @policyMessage</p>

@code {
    private string policyMessage = "Check hasn't been made yet.";

    [CascadingParameter]
    private Task<AuthenticationState> authenticationStateTask { get; set; }

    private async Task CheckPolicy()
    {
        var user = (await authenticationStateTask).User;

        if ((await AuthorizationService.AuthorizeAsync(user, 
            "BillingAdministrator")).Succeeded)
        {
            policyMessage = "Yes! The 'BillingAdministrator' policy is met.";
        }
        else
        {
            policyMessage = "No! 'BillingAdministrator' policy is NOT met.";
        }
    }
}

Usando as abordagens anteriores, você também pode criar acesso baseado em política para grupos de segurança, em que a GUID usada para a política corresponde à

Funções de Aplicativo

Para configurar o aplicativo no portal do Azure para fornecer declarações de associação de Funções de aplicativo, veja Adicionar funções de aplicativo ao seu aplicativo e recebê-las no token na documentação do ME-ID.

O exemplo a seguir pressupõe que o aplicativo está configurado com duas funções atribuídas a um usuário de teste:

  • Admin
  • Developer

Embora não seja possível atribuir funções a grupos sem uma conta ME-ID Premium, você pode atribuir funções aos usuários e receber declarações de funções para usuários com uma conta padrão do Azure. As diretrizes nesta seção não exigem uma conta Premium do ME-ID.

Adote uma das seguintes abordagens para adicionar funções de aplicativo no ME-ID:

  • Ao trabalhar com o diretório padrão, siga as diretrizes em Adicionar funções de aplicativo a seu aplicativo e recebê-las no token para configurar funções do ME-ID.

  • Se você não estiver trabalhando com o diretório padrão, edite o manifesto do aplicativo no portal do Azure para estabelecer as funções do aplicativo manualmente na entrada appRoles do arquivo de manifesto. Veja a seguir uma entrada de exemplo appRoles que cria funções Admin e Developer. Essas funções de exemplo serão usadas posteriormente no nível do componente para implementar restrições de acesso:

    Importante

    A abordagem a seguir só é recomendada para aplicativos que não estão registrados no diretório padrão da conta do Azure. No caso de aplicativos registrados no diretório padrão, consulte o ponto anterior desta lista.

    "appRoles": [
      {
        "allowedMemberTypes": [
          "User"
        ],
        "description": "Administrators manage developers.",
        "displayName": "Admin",
        "id": "584e483a-7101-404b-9bb1-83bf9463e335",
        "isEnabled": true,
        "lang": null,
        "origin": "Application",
        "value": "Admin"
      },
      {
        "allowedMemberTypes": [
          "User"
        ],
        "description": "Developers write code.",
        "displayName": "Developer",
        "id": "82770d35-2a93-4182-b3f5-3d7bfe9dfe46",
        "isEnabled": true,
        "lang": null,
        "origin": "Application",
        "value": "Developer"
      }
    ],
    

Para atribuir uma função a um usuário (ou grupo, se você tiver uma conta do Azure de camada Premium):

  1. Acesse Aplicativos empresariais na área do ME-ID do portal do Azure.
  2. Selecione o aplicativo. Selecione Gerenciar>Usuários e grupos na barra lateral.
  3. Marque a caixa de seleção para uma ou mais contas de usuário.
  4. No menu acima da lista de usuários, selecione Editar atribuição.
  5. Para a entrada Selecionar uma função, selecione Nenhuma selecionada.
  6. Escolha uma função na lista e use o botão Selecionar para selecioná-la.
  7. Use o botão Atribuir na parte inferior da tela para atribuir a função.

Várias funções são atribuídas no portal do Azure adicionando novamente um usuário para cada atribuição de função adicional. Use o botão Adicionar usuário/grupo na parte superior da lista de usuários para adicionar novamente um usuário. Use as etapas anteriores para atribuir outra função ao usuário. Você pode repetir esse processo quantas vezes forem necessárias para adicionar funções adicionais a um usuário (ou grupo).

O CustomAccountFactory mostrado na seção Conta de usuário personalizada é configurado para agir em uma declaração role com um valor de matriz JSON. Adicione e registre o CustomAccountFactory no aplicativo, conforme mostrado na seção Conta de usuário personalizada. Não é necessário fornecer código para remover a declaração role original porque ela é removida automaticamente pela estrutura.

No arquivo Program, adicione ou confirme a declaração chamada "role" como a declaração de função das verificações ClaimsPrincipal.IsInRole:

builder.Services.AddMsalAuthentication(options =>
{
    ...

    options.UserOptions.RoleClaim = "role";
});

Observação

Se você preferir usar a declaração directoryRoles (Funções de administrador do ME-ID), atribua "directoryRoles" à RemoteAuthenticationUserOptions.RoleClaim.

Depois de concluir as etapas anteriores para criar e atribuir funções a usuários (ou grupos, se você tiver uma conta do Azure de camada Premium) e implementar o CustomAccountFactory com o SDK do Graph, conforme explicado anteriormente neste artigo e em Usar API do Graph com ASP.NET Core Blazor WebAssembly, você deverá ver uma declaração role para cada função atribuída à qual um usuário conectado é atribuído (ou funções atribuídas a grupos dos quais eles são membros). Execute o aplicativo com um usuário de teste para confirmar se as declarações estão presentes conforme o esperado. Ao testar com o SDK do Graph localmente, recomendamos o uso de uma nova sessão do navegador InPrivate/incógnito para cada teste a fim de evitar que os cookies persistentes interfiram nos testes. Para obter mais informações, consulte Proteger um aplicativo autônomo ASP.NET Core Blazor WebAssembly com contas do Microsoft Entra ID.

As abordagens de autorização de componentes estão funcionais neste momento. Qualquer um dos mecanismos de autorização em componentes do aplicativo pode usar a função Admin para autorizar o usuário:

Há suporte para vários testes de função:

  • Exige que o usuário esteja na função Admin ou Developer com o componente AuthorizeView:

    <AuthorizeView Roles="Admin, Developer">
        ...
    </AuthorizeView>
    
  • Exige que o usuário esteja nas funções Admin e Developer com o componente AuthorizeView:

    <AuthorizeView Roles="Admin">
        <AuthorizeView Roles="Developer" Context="innerContext">
            ...
        </AuthorizeView>
    </AuthorizeView>
    

    Para obter mais informações no Context para AuthorizeView interno, confira autenticação e autorização Blazor do ASP.NET Core.

  • Exige que o usuário esteja na função Admin ou Developer com o atributo [Authorize]:

    @attribute [Authorize(Roles = "Admin, Developer")]
    
  • Exige que o usuário esteja nas funções Admin e Developer com o atributo [Authorize]:

    @attribute [Authorize(Roles = "Admin")]
    @attribute [Authorize(Roles = "Developer")]
    
  • Exige que o usuário esteja na função Admin ou Developer com o código de procedimento:

    @code {
        private async Task DoSomething()
        {
            var authState = await AuthenticationStateProvider
                .GetAuthenticationStateAsync();
            var user = authState.User;
    
            if (user.IsInRole("Admin") || user.IsInRole("Developer"))
            {
                ...
            }
            else
            {
                ...
            }
        }
    }
    
  • Exige que o usuário esteja nas funções Admin e Developer com o código de procedimento, alterando o OR condicional (||) para um AND condicional (&&) no exemplo anterior:

    if (user.IsInRole("Admin") && user.IsInRole("Developer"))
    

Há suporte para vários testes de função:

  • Exige que o usuário esteja na função Admin ou Developer com o atributo [Authorize]:

    [Authorize(Roles = "Admin, Developer")]
    
  • Exige que o usuário esteja nas funções Admin e Developer com o atributo [Authorize]:

    [Authorize(Roles = "Admin")]
    [Authorize(Roles = "Developer")]
    
  • Exige que o usuário esteja na função Admin ou Developer com o código de procedimento:

    static readonly string[] scopeRequiredByApi = new string[] { "API.Access" };
    
    ...
    
    [HttpGet]
    public IEnumerable<ReturnType> Get()
    {
        HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
    
        if (User.IsInRole("Admin") || User.IsInRole("Developer"))
        {
            ...
        }
        else
        {
            ...
        }
    
        return ...
    }
    
  • Exige que o usuário esteja nas funções Admin e Developer com o código de procedimento, alterando o OR condicional (||) para um AND condicional (&&) no exemplo anterior:

    if (User.IsInRole("Admin") && User.IsInRole("Developer"))
    

Como as comparações de cadeia de caracteres do .NET diferenciam maiúsculas de minúsculas por padrão, a correspondência entre nomes de função também diferencia maiúsculas de minúsculas. Por exemplo, Admin (A em maiúsculas) não é tratado como a mesma função que admin (a em minúsculas).

O caso Pascal normalmente é usado para nomes de função (por exemplo, BillingAdministrator), mas o uso do caso Pascal não é um requisito estrito. Diferentes esquemas de uso de maiúsculas e minúsculas, como maiúsculas e minúsculas concatenadas, kebab e snake são permitidos. O uso de espaços em nomes de função também é incomum, mas permitido. Por exemplo, billing administrator é um formato de nome de função incomum em aplicativos .NET, mas válido.

Recursos adicionais