{"id":25588393,"url":"https://github.com/applicita/orleans.multitenant","last_synced_at":"2025-03-12T21:13:40.016Z","repository":{"id":61726170,"uuid":"551300342","full_name":"Applicita/Orleans.Multitenant","owner":"Applicita","description":"Secure, flexible tenant separation for Microsoft Orleans","archived":false,"fork":false,"pushed_at":"2025-02-24T15:37:30.000Z","size":222,"stargazers_count":78,"open_issues_count":1,"forks_count":11,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-02-28T17:51:50.901Z","etag":null,"topics":["distributed-systems","multitenancy","multitenant","multitenant-api","multitenant-applications","orleans","orleans-applications","orleans-example","orleans-storage-provider","orleans-streaming-provider"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Applicita.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-10-14T06:39:54.000Z","updated_at":"2025-02-28T07:34:27.000Z","dependencies_parsed_at":"2024-03-28T12:32:38.978Z","dependency_job_id":"4fa99726-f75b-49ac-9cde-37c38b983c01","html_url":"https://github.com/Applicita/Orleans.Multitenant","commit_stats":{"total_commits":28,"total_committers":2,"mean_commits":14.0,"dds":0.0357142857142857,"last_synced_commit":"c04aa9a20ce3616391f9e748cd0c383436f02e4c"},"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Applicita%2FOrleans.Multitenant","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Applicita%2FOrleans.Multitenant/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Applicita%2FOrleans.Multitenant/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Applicita%2FOrleans.Multitenant/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Applicita","download_url":"https://codeload.github.com/Applicita/Orleans.Multitenant/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243293794,"owners_count":20268142,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["distributed-systems","multitenancy","multitenant","multitenant-api","multitenant-applications","orleans","orleans-applications","orleans-example","orleans-storage-provider","orleans-streaming-provider"],"created_at":"2025-02-21T08:36:17.444Z","updated_at":"2025-03-12T21:13:40.010Z","avatar_url":"https://github.com/Applicita.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"﻿# \u003cimg src=\"img/CSharp-Toolkit-Icon.png\" alt=\"Backend Toolkit\" width=\"64px\" /\u003eOrleans.Multitenant\nSecure, flexible tenant separation for Microsoft Orleans 8\n\n\u003e [![Nuget (with prereleases)](https://img.shields.io/nuget/vpre/Orleans.Multitenant?color=gold\u0026label=NuGet:%20Orleans.Multitenant\u0026style=plastic)](https://www.nuget.org/packages/Orleans.Multitenant)\u003cbr /\u003e\n\u003e (install in silo client and grain implementation projects)\n\n## Summary\n[Microsoft Orleans 8](https://github.com/dotnet/orleans/releases/tag/v8.0.0) is a great technology for building distributed, cloud-native applications. It was designed to reduce the complexity of building this type of applications for C# developers.\n\nHowever, creating multi tenant applications with Orleans out of the box requires careful design, complex coding and significant testing to prevent unintentional leakage of communication or stored data across tenants. Orleans.Multitenant adds this capability to Orleans for free, as an uncomplicated, flexible and extensible API that lets developers:\n\n- **Separate storage** per tenant in any Orleans storage provider by configuring the storage provider options per tenant:\u003cbr /\u003e\n  ![Example Azure Table Storage](img/example-azure-table-storage.png)\n- **Separate communication** across tenants for grain calls and streams, or allow specific access between tenants:\u003cbr /\u003e \n  ![Example Access Authorizer](img/example-access-authorizer.png)\u003cbr /\u003e\n\n- **Choose where to use** - for part or all of an application; combine regular stream/storage providers with multitenant ones, use tenant-specific grains/streams and tenant unaware ones. Want to add multitenant storage to an existing application? You can bring along existing grain state in the null tenant. Or add a multitenant storage provider and keep the existing non-multitenant provider as well\n\n- **Secure** against development mistakes: unauthorized access to a tenant specific grain or stream throws an `UnauthorizedException`, and using a non-tenant aware API on a tenant aware stream is blocked and logged.\n\n## Scope and limitations\n- Tenant id's are part of the key for a `GrainId` or `StreamId` and can be any string; the same goes for keys within a tenant. The creation and lifecycle management of tenant id's is the responsibility of the application developer; as far as Orleans.Multitenant is concerned, tenants are **virtual** just like grains and streams - so conceptually all possible tenant id's always exist\n\n- Orleans.Multitenant guards against unauthorized access from grains that have a GrainId, since only there a tenant-specific context exists (the grain key contains the tenant id). Guarding against unauthorized tenant access that is not initiated from a tenant grain (e.g. when using a cluster client in an ASP.NET controller, or in a stateless worker grain or a grain service) is the responsibility of the application developer, since what constitutes a tenant context there is application specific\n\n- Only `IGrainWithStringKey` grains can be tenant specific\n\n## Usage\nAll multitenant features can be independenty enabled and configured at silo startup, with the `ISiloBuilder` `AddMultitenant*` extension methods.\nSee the inline documentation for more details on how to use the API's that are mentioned in this readme. All the public API's come with full inline documentation\n\n### Add multitenant storage\nTo add tenant storage separation to any Orleans storage provider, use `AddMultitenantGrainStorage` and `AddMultitenantGrainStorageAsDefault` on an `ISiloBuilder` or `IServiceCollection`:\n\n```csharp \nsiloBuilder\n.AddMultitenantGrainStorageAsDefault\u003cAzureTableGrainStorage, AzureTableStorageOptions, AzureTableGrainStorageOptionsValidator\u003e(\n    (silo, name) =\u003e silo.AddAzureTableGrainStorage(name, options =\u003e\n        options.ConfigureTableServiceClient(tableStorageConnectionString)),\n        // Called during silo startup, to ensure that any common dependencies\n        // needed for tenant-specific provider instances are initialized\n\n    configureTenantOptions: (options, tenantId) =\u003e {\n        options.ConfigureTableServiceClient(tableStorageConnectionString);\n        options.TableName = $\"OrleansGrainState{tenantId}\";\n    }   // Called on the first grain state access for a tenant in a silo,\n        // to initialize the options for the tenant-specific provider instance\n        // just before it is instantiated\n )\n```\n\n#### Customize storage provider constructor parameters\nBy default, the parameters passed into the storage provider instance for a tenant are the tenant provider name (which contains the tenant Id) and the tenant options. Some storage providers may expect a different (wrapper) type for the options, or you may want to pass in additional parameters (e.g. `ClusterOptions`).\n\nTo do this, you can pass in an optional `GrainStorageProviderParametersFactory\u003cTGrainStorageOptions\u003e? getProviderParameters` parameter.\n\n##### Example: .NET Aspire with Azure Blob Storage for grain state\nIf you are using the [.NET Aspire Orleans Integration](https://learn.microsoft.com/en-us/dotnet/aspire/frameworks/orleans) to configure the default grain storage for the silo like this:\n```csharp\n// Add the resources which you will use for Orleans clustering and\n// grain state storage.\nvar storage = builder.AddAzureStorage(\"orleans\").RunAsEmulator();\nvar clusteringTable = storage.AddTables(\"clustering\");\nvar grainStorage = storage.AddBlobs(\"grain-state\");\n\n// Add the Orleans resource to the Aspire DistributedApplication\n// builder, then configure it with Azure Table Storage for clustering.\nvar orleans = builder.AddOrleans(\"default\")\n                     .WithClustering(clusteringTable);\n// Note that we don't call WithGrainStorage here, since Multitenant grain storage is added in the silo code\n// where we use the blob client that Aspire configures from above grainStorage resource, which is passed to apis using WithReference below\n\nvar yourProject = builder\n    .AddProject\u003cProjects. ...\u003e(\"...\")\n    .WithReference(orleans)\n    .WithReference(grainStorage) // We use this instead of .WithGrainStorage(grainStorage) to pass only the blob client to the Orleans silo\n    .WaitFor(clusteringTable)\n    .WaitFor(grainStorage);\n```\nthen you can use the following code to configure the tenant-specific storage provider instances for Azure Blob Storage:\n\n```csharp\n// Use Aspire to configure the clients for clustering and grain state\nbuilder.AddKeyedAzureTableClient(\"clustering\");\nbuilder.AddKeyedAzureBlobClient(\"grain-state\");\n\nbuilder.UseOrleans(\n    silo =\u003e silo\n    .AddMultitenantGrainStorageAsDefault\u003cAzureBlobGrainStorage, AzureBlobStorageOptions, AzureBlobStorageOptionsValidator\u003e(\n        // Called during silo startup, to ensure that any common dependencies\n        // needed for tenant-specific provider instances are initialized\n        (silo, name) =\u003e silo.AddAzureBlobGrainStorage(name, (OptionsBuilder\u003cAzureBlobStorageOptions\u003e options) =\u003e\n            options.Configure\u003cIServiceProvider\u003e((options, services) =\u003e \n                options.BlobServiceClient = services.GetRequiredKeyedService\u003cBlobServiceClient\u003e(\"grain-state\")) // Use the BlobServiceClient registered by AddKeyedAzureBlobClient above\n        ),\n\n        // Called on the first grain state access for a tenant in a silo,\n        // to initialize the options for the tenant-specific provider instance just before it is instantiated\n        configureTenantOptions: (options, tenantId) =\u003e\n        {\n            #pragma warning disable CA1308 // Normalize strings to uppercase\n            options.ContainerName += \"-\" + tenantId.ToLowerInvariant();\n            #pragma warning restore CA1308 // Normalize strings to uppercase\n        },\n\n        getProviderParameters: (services, providerName, tenantProviderName, options) =\u003e\n        {\n            options.BlobServiceClient = services.GetRequiredKeyedService\u003cBlobServiceClient\u003e(\"grain-state\"); // Use the BlobServiceClient registered by AddKeyedAzureBlobClient above\n            return [options, options.BuildContainerFactory(services, options)];\n        }\n    )\n);\n```\nNote that you do not need to include the `tenantProviderName` in the returned provider parameters; it is added automatically.\n\nThe parameters passed to `getProviderParameters` allow to access relevant services from DI to retrieve additional provider parameters, if needed.\n\n##### Example: ADO.NET for grain state\nE.g. the Orleans ADO.NET storage provider constructor expects an `IOptions\u003cAdoNetGrainStorageOptions\u003e` instead of an `AdoNetGrainStorageOptions`. You can use `getProviderParameters` to wrap the `AdoNetGrainStorageOptions` in an `IOptions\u003cAdoNetGrainStorageOptions\u003e`:\n\n```csharp\n.AddMultitenantGrainStorageAsDefault\u003cAdoNetGrainStorage, AdoNetGrainStorageOptions, AdoNetGrainStorageOptionsValidator\u003e(\n    (silo, name) =\u003e silo.AddAdoNetGrainStorage(name, options =\u003e options.ConnectionString = sqlConnectionString),\n\n    configureTenantOptions: (options, tenantId) =\u003e options.ConnectionString = sqlConnectionString.Replace(\"[DatabaseName]\", tenantId, StringComparison.Ordinal),\n\n    getProviderParameters: (services, providerName, tenantProviderName, options) =\u003e [Options.Create(options)]\n)\n```\n\n### Add multitenant streams\nTo configure a silo to use a specific stream provider type as a named stream provider with tenant separation, use `AddMultitenantStreams`. Any Orleans stream provider can be used:\n```csharp\n.AddMultitenantStreams(\n    \"provider_name\", (silo, name) =\u003e silo\n    .AddMemoryStreams\u003cDefaultMemoryMessageBodySerializer\u003e(name)\n    .AddMemoryGrainStorage(name)\n )\n```\nBoth implicit and explicit stream subscriptions are supported.\n\n### Add multitenant communication separation\nTo configure a silo to use tenant separation for grain communication, use `AddMultitenantCommunicationSeparation` . Separation will be enforced for both grain calls and streams (the latter if used together with `AddMultitenantStreams`)\n\nOptionally pass in an `ICrossTenantAuthorizer` factory and/or an `IGrainCallTenantSeparator` factory, to control which tenants are authorized to communicate, and which grain calls require authorization:\n```csharp\n.AddMultitenantCommunicationSeparation(_ =\u003e new ExtendedCrossTenantAccessAuthorizer())\n```\n```csharp\nclass ExtendedCrossTenantAccessAuthorizer : ICrossTenantAuthorizer\n{\n    internal const string RootTenantId = \"RootTenant\";\n\n    public bool IsAccessAuthorized(string? sourceTenantId, string? targetTenantId)\n    =\u003e  string.CompareOrdinal(sourceTenantId, RootTenantId) == 0;\n    // Allow access from the root tenant to any tenant\n}\n```\n\nBy default different tenants are not authorized to communicate, and only calls to `Orleans.*` grain interfaces are exempted from authorization\n\n- An attempt to make an unauthorized grain call causes an `UnauthorizedAccessException` to be thrown. The call does not reach the target grain\n- An unauthorized attempt to access a stream provider using `GetTenantStreamProvider` causes an `UnauthorizedAccessException` to be thrown\n- An attempt to publish an event to a multitenant stream without using `GetTenantStreamProvider` (i.e. using the Orleans built-in `GetStreamProvider` API) causes the event to be blocked by the stream filter; an error with event Id `TenantUnawareStreamApiUsed` is logged in the silo log (also see `AddMultitenantStreams`)\n\n### Access tenant grains and streams from a tenant grain\nWhere a tenant grain is available,\n\n- To access grains within the same tenant from within a `Grain`, use the `Grain` extension method `this.GetTenantGrainFactory()`:\n  ```csharp\n  var sameTenantGrain = this.GetTenantGrainFactory().GetGrain\u003cIMyGrain\u003e(\"key_within_tenant\");\n  ```\n\n- To access grains that belong to another tenant from within a `Grain`, use the `Grain` extension method `this.GetTenantGrainFactory(\"tenant_id\")`:\n  ```csharp\n  var otherTenantGrain = this.GetTenantGrainFactory(\"tenant_id\").GetGrain\u003cIMyGrain\u003e(\"key_within_tenant\");\n  ```\n\n- To access grains within the same tenant that an `IAddressable` (i.e. a grain) belongs to, use the `IGrainFactory` extension method `factory.ForTenantOf(grain)`:\n  ```csharp\n  var sameTenantGrain = factory.ForTenantOf(grain).GetGrain\u003cIMyGrain\u003e(\"key_within_tenant\");\n  ```\n  A tenant grain factory is a very lightweight, allocation-free factory wrapper; it can be stored/cached as desired, but it's overhead is extremely low even without that.\n\n- To access streams within the same tenant, use the `Grain` extension method `this.GetTenantStreamProvider(\"provider_name\")`:\n  ```csharp\n  var sameTenantStream = this.GetTenantStreamProvider(\"provider_name\").GetStream\u003cint\u003e(\"stream_namespace\", \"stream_key_within_tenant\");\n  ```\n  A tenant stream provider is a very lightweight, allocation-free stream provider wrapper; it can be stored/cached as desired, but it's overhead is extremely low even without that.\n\n- To access streams that belong to another tenant, use the `Grain` extension method `this.GetTenantStreamProvider(\"provider_name\", \"tenant_id\"):\n  ```csharp\n  var otherTenantStream = this.GetTenantStreamProvider(\"provider_name\", \"tenant_id\").GetStream\u003cint\u003e(\"stream_namespace\", \"stream_key_within_tenant\");\n  ```\n\nWhen `AddMultitenantCommunicationSeparation` is used, all of the above methods are guarded against unautorized access.\n\n### Access tenant grains and streams without a tenant grain\nWhere no tenant grain is available (e.g. in a cluster client, a stateless worker grain or a grain service),\n- To access tenant grains, use the `IGrainFactory` extension method `factory.ForTenant(\"tenant_id\")`:\u003cbr /\u003e\n  ```csharp\n  var tenantGrain = factory.ForTenant(\"tenant_id\").GetGrain\u003cIMyGrain\u003e(\"key_within_tenant\");\n  ```\n\n- To access tenant streams, use the `IClusterClient` extension method `client.GetTenantStreamProvider(\"provider name\", \"tenant id\"):\u003cbr /\u003e\n  ```csharp\n  var tenantStream = client.GetTenantStreamProvider(\"provider_name\", \"tenant_id\").GetStream\u003cint\u003e(\"stream_namespace\", \"stream_key_within_tenant\");\n  ```\n\n**Note** that guarding against unauthorized tenant access that is not initiated from a tenant grain (e.g. when using a cluster client in an ASP.NET controller, or in a stateless worker grain or a grain service) is the responsibility of the application developer, since what constitutes a tenant context there is application specific\n\n### Grain/stream key and tenant id\nTenant id's are stored in the key of a tenant specific `GrainId` / `StreamId`. Use these methods to access the individual parts of the key:\n```csharp\nstring? GetTenantId(this IAddressable grain);\nstring  GetKeyWithinTenant(this IAddressable grain);\n\nstring? GetTenantId(this GrainId grainId);\nstring GetKeyWithinTenant(this GrainId grainId);\n\nstring? GetTenantId(this StreamId streamId);\nstring  GetKeyWithinTenant(this StreamId streamId);\n```\n\n### The null tenant\nNote that a tenant id with value `null` means that a grain was not created with the tenant aware API's as described in this readme. This could e.g. be the case when 3rd party code is responsible for creating the grain keys.\n\nEven though the null tenant cannot be specified in the tenant aware API's, it is a valid tenant Id value in the parameters of the `ICrossTenantAuthorizer.IsAccessAuthorized` callback. This enables support for scenario's like above.\n\nTo access null tenant grains, use the Orleans built-in `IGrainFactory`, and register an `ICrossTenantAuthorizer` that allows access between the null tenant and other tenants. You can also exclude specific interface namespaces from the need to be authorized by registering an `IGrainCallTenantSeparator` (see [Add multitenant communication separation](#add-multitenant-communication-separation)).\n\nThe `MultitenantStorageOptions.TenantIdForNullTenant` setting specifies the non-null string value representing the null tenant. This value can be specified in `appsettings.json` in the `MultitenantStorage` section, and is passed as the `tenantId` parameter of the `configureTenantOptions` action, which can be specified in `AddMultitenantGrainStorage` methods. This setting allows developers to choose a name for the null tenant in storage that does not conflict with other valid tenant names in the application.\n\n### Tenant unaware streams\nTo access tenant unaware streams (e.g. streams whose keys are defined by 3rd party code), use the Orleans built-in `IStreamProvider`. There is no need for an `ICrossTenantAuthorizer` to enable this access, because an `IStreamProvider` does not have the `TenantSeparatingStreamFilter` attached.\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapplicita%2Forleans.multitenant","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fapplicita%2Forleans.multitenant","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapplicita%2Forleans.multitenant/lists"}