{"id":15066072,"url":"https://github.com/jaxsnorris/row-level-security","last_synced_at":"2026-01-03T05:04:59.568Z","repository":{"id":255009163,"uuid":"848245599","full_name":"JaxsNorris/row-level-security","owner":"JaxsNorris","description":"Sample project to show how to add row level security (RLS) to SQL and Backend to intercept DB calls","archived":false,"fork":false,"pushed_at":"2024-08-27T12:24:30.000Z","size":30,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-16T01:12:23.295Z","etag":null,"topics":["dotnet-core","row-level-security","sql"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JaxsNorris.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":"2024-08-27T12:09:39.000Z","updated_at":"2024-08-27T12:27:42.000Z","dependencies_parsed_at":null,"dependency_job_id":"fa3a564a-2de6-4a03-8103-7568fb6a62ae","html_url":"https://github.com/JaxsNorris/row-level-security","commit_stats":null,"previous_names":["jaxsnorris/row-level-security"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JaxsNorris%2Frow-level-security","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JaxsNorris%2Frow-level-security/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JaxsNorris%2Frow-level-security/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JaxsNorris%2Frow-level-security/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JaxsNorris","download_url":"https://codeload.github.com/JaxsNorris/row-level-security/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243809885,"owners_count":20351407,"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":["dotnet-core","row-level-security","sql"],"created_at":"2024-09-25T01:01:08.740Z","updated_at":"2026-01-03T05:04:59.474Z","avatar_url":"https://github.com/JaxsNorris.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Intro\nThe purpose of is to give a basic idea of how row level security works in the real world.\nIn a nutshell RLS is adding security to stop access to data by some \n\n### Scenario\n- We have users that are linked to a tenant.\n- A tenant can have multiple sub-tenants which in term can have sub-tenants. No limit on the depth of the tree.\n- We have application that are linked to a tenant\n- Applications have files\n- A user linked to a tenant should be able to access the tenant and all sub-tenants down the tree\n\n# SQL Guide\n## 1. DB Table + View setup\n```mermaid\nerDiagram\n    Tenant {\n        int TenantId\n        string Name\n        string Path\n    }\n    User {\n        int UserId\n        int TenantId\n        string Name\n    }\n\n    Application {\n        int ApplicationId\n        int TenantId\n        string Description        \n    }\n    \n     ApplicationFile {\n        int ApplicationFileId\n        int ApplicationId\n        int TenantId\n        string Description        \n    }\n    User |{--|| Tenant: \"a part of\"\n    Application |{ --|| Tenant: \"belongs to\"\n    ApplicationFile |{ --|| Application: \"belongs to\"\n    \n\n```\n1. Run script 1 in db folder. This creates the DB tables + view as above image\n1. Run script 2 to insert some test data\n1. Play around with the data in SQL and via API endpoint. *See script 5 for SQL queries* \n\n| User Id      | User name      | Tenant Path   |\n| -------------| -------------  | ------------- | \n| 1             |Iron Man       | \\Marvel       |\n| 13            | The Thing     | \\Marvel\\FanasticFour |\n| 52            | Wonder Woman  | \\DC          |\n\n\n| Application Id    | Application desc      | Tenant Path           | # Files   | \n| -------------     | -------------         | -------------         | --------  |  \n| 1                 | Marvel app 1          | \\Marvel               |1          |\n| 2                 | Marvel app 2          | \\Marvel               |0          |\n| 3                 | Averager app 1        | \\Marvel\\Avengers      |0          |\n| 4                 | Fantastic Four app 1  | \\Marvel\\FanasticFour  |0          |\n| 5                 | DC APP 1              | \\DC                   |2          |\n| 6                 | DC APP 2              | \\DC                   |3          |\n\n\n## 2. Adding helper functions\n\n1. Create security schema. This is considered best practice to contain all functions and policies for RSL (see script 3)\n1. Create a reusable function to check the access for an application id. This function helps do the check to see if the session context tenant is in the allowed path (see script 3)\n\n\u003e  SESSION_CONTEXT('UserId') is used to retrieve the value that we will be passed in via our backend.\n\n\u003e We also do a wildcard search on tenant path so that `Iron man` who is set at `\\Marvel` can access anything below i.e all applications that are for `Avengers` and `Fanastic Four`\n\n```SQL\nCREATE FUNCTION security.CheckAccessForApplicationId(@ApplicationId int)\n\t\tRETURNS TABLE\n\t\t\tWITH SCHEMABINDING\n\t\t\tAS\n\t\t\tRETURN SELECT 1 AS accessResult\n\t\t\t\t   FROM dbo.[User] u\n\t\t\t\t\tINNER JOIN dbo.Tenant t on t.TenantId = u.TenantId\n\t\t\t\t\tRIGHT JOIN dbo.TenantApplicationView a on a.TenantPath LIKE t.Path+''%''\n\t\t\t\t\tWHERE u.UserId = CAST(SESSION_CONTEXT(N''UserId'') AS int)\n\t\t\t\t\tAND a.ApplicationId = @ApplicationId\n```\n\nIf the above function doesn't return a row then it will be treated as that userId doesn't have access.\n\n## 3. Adding the predicate\n1. Add the predicate check on the application table (see script 4)\n\n\u003e The `FILTER PREDICATE` takes cares of `SELECT` and `DELETE` statements. since you have to be able to see the info to be able to delete :)\n\n\n``` SQL\nEXECUTE('CREATE SECURITY POLICY security.TenantAccessFilter\n\tADD FILTER PREDICATE security.CheckAccessForApplicationId(ApplicationId)\n        ON dbo.Application,\n    ADD BLOCK PREDICATE security.CheckAccessForApplicationId(ApplicationId)\n        ON dbo.Application AFTER INSERT\n\tWITH (STATE = ON)');\n```\n## 4. Play around with the data - Select\n1. Lets see the select filter enforced. *see script 5*\n```SQL \nSELECT *  FROM [dbo].[Application]\n```\nThis returns nothing since the above filter is taking affect.\nWe need to set the session context key  `UserId` with a value. Lets set it to `The Thing`. Looking at the above tables he should only be able to see 1 application \n```SQL\nEXEC sys.sp_set_session_context @key = N'UserId', @value = '13'; \nSELECT *  FROM [dbo].[Application]\n```\nContinue to play around with different users and see how it filters out the work. \n\n## 5. Play around with the data - Insert\n1. Lets see how well the RLS works for inserting data.\nLets see if Hulk can add a DC application.\n```SQL\nEXEC sys.sp_set_session_context @key = N'UserId', @value = '5';\nINSERT INTO [dbo].[Application]\n           ([ApplicationId]\n           ,[TenantId]\n           ,[Description])\n     VALUES\n           (7,\n\t\t   5, -- DC tenant Id\n\t\t   'DC 3');\n```\nWe get an error message that shows us that we are now enforcing RLS so that one tenant won't be able to add new applications for another.\n```\nThe attempted operation failed because the target object 'row_level_security.dbo.Application' has a block predicate that conflicts with this operation. If the operation is performed on a view, the block predicate might be enforced on the underlying table. Modify the operation to target only the rows that are allowed by the block predicate.\n```\n\n## 6. Play around with the data - Delete\n1. How does it work for deletes.\nLets see if Flash can pull a fast one and delete one of the fanastic fours applications\n```SQL\nEXEC sys.sp_set_session_context @key = N'UserId', @value = '53'; \nDELETE FROM [dbo].[Application]\nWHERE ApplicationId = 4\n```\nNo error message this time but it does say `0 rows affected`\n\nIf we change back to a Marvel or Fanastic Four user you will see that application 4 is still there\n```\nEXEC sys.sp_set_session_context @key = N'UserId', @value = '10'; \nSELECT *FROM [dbo].[Application]\nWHERE ApplicationId = 4\n```\n## 7. What about application files?\n1. What about the tables linked to the application?\nAs the Thing lets see if we can see the files\n```SQL\nEXEC sys.sp_set_session_context @key = N'UserId', @value = '13';\nSELECT *  FROM [row_level_security].[dbo].[ApplicationFile]\n```\nThis returns all the files even though the application for fanastic four doesn't have any linked\n\n2. To fix this we need to add the predicate on every table we want to restrict. \n\u003e Since ApplicationFile has an ApplicationId we can reuse our existing function. However if we had another table that didn't we could create a new function that applies simliar filtering\n```SQL\nEXECUTE('ALTER SECURITY POLICY security.TenantAccessFilter\n\tADD FILTER PREDICATE security.CheckAccessForApplicationId(ApplicationId)\n        ON dbo.ApplicationFile,\n    ADD BLOCK PREDICATE security.CheckAccessForApplicationId(ApplicationId)\n        ON dbo.ApplicationFile AFTER INSERT');\n```\n3. Run the above query again. Now Thing can't see any files as expected\n\n# API Guide\n## 1. Creating a DB interceptor\n1. We need to write a DB interceptor that can set the session context UserId\n*see SessionContextDbConnectionInterceptor.cs for full code*\n\n\u003e To make things easier we are going to just use a user-id that is passed in the request header. In the real world this will probably be a claim added to your user principal during authenication\n ```C#\n private int? GetUserId()\n{\n    if (httpContextAccessor.HttpContext != null\n        \u0026\u0026 httpContextAccessor.HttpContext.Request.Headers.TryGetValue(\"user-id\", out var headerUserId))\n        return int.Parse(headerUserId.ToString());\n\n    return null;\n}\n ```\nIn both the `ConnectionOpenedAsync` and `ConnectionOpened` method we need to override and set the session context before any queries are run\n```C#\n private static DbCommand CreateCommand(DbConnection connection, int userId)\n {\n     var cmd = connection.CreateCommand();\n\n     var param = cmd.CreateParameter();\n     param.ParameterName = \"@key\";\n     param.Value = \"UserId\";\n     param.DbType = DbType.String;\n     cmd.Parameters.Add(param);\n\n     param = cmd.CreateParameter();\n     param.ParameterName = \"@value\";\n     param.Value = userId;\n     param.DbType = DbType.String;\n     cmd.Parameters.Add(param);\n\n     cmd.CommandType = CommandType.StoredProcedure;\n     cmd.CommandText = @\"sp_set_session_context\";\n     return cmd;\n }\n```\n## 2. Adding the interceptor to the DB context\n\nIn the `RowLevelSecurityDbContext` we can add the interceptor.\n```C#\nprotected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)\n{\n    optionsBuilder.AddInterceptors(new SessionContextDbConnectionInterceptor(_httpContextAccessor));\n}\n```\n\n## 3. Play around with it\nTry hit the endpoint via your favourite http client or via CURL commands below\nAs Iron man we expect to see 4 applications\n```\ncurl --location --request GET 'https://localhost:7236/Application' --header 'user-id:1'\n```\nAs The Thing we expect to see 1 applications\n```\ncurl --location --request GET 'https://localhost:7236/Application' --header 'user-id:13'\n```\nAs Wonder Woman we expect to see 2 applications\n```\ncurl --location --request GET 'https://localhost:7236/Application' --header 'user-id:52'\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjaxsnorris%2Frow-level-security","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjaxsnorris%2Frow-level-security","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjaxsnorris%2Frow-level-security/lists"}