.NET Library
    • 01 Sep 2024
    • 7 Minutes to read
    • Dark
      Light
    • PDF

    .NET Library

    • Dark
      Light
    • PDF

    Article summary

    Note: This is an early access feature

    Introduction

    The PlainID SQL PDP Authorizer Library enables the use of the SQL PDP service with Entity Framework Core. It applies SQL filtering rules in addition to the SQL queries generated by Entity Framework Core. The library is compatible with .NET Core 6 and .NET Core 7, and it also supports .NET Core/Standard 2 for ADO.NET-like solutions (see the relevant section below).

    Usage

    Installing via NuGET

    To install the library, use the following command in your code editor:

    dotnet add package PlainID.SQL.PDP.Auth.Lib-v 1.0.13
    

    Quick Library Setup using the "Magic Hook" Method

    This method provides a single call and automatically adds the DB interceptor and the authorization middleware to the services collection by calling the HookAuthorizationMiddleware and HookDb:

    PlainLibrary.HookAll(IServiceCollection services, Options plainOptions = null)
    

    For advanced setup:

    Authorization Middleware:

    PlainLibrary.HookAuthorizationMiddleware(IServiceCollection services, Options plainOptions = null)
    

    DB interceptor:

    PlainLibrary.HookDb(Options plainOptions = null)
    

    Initializing Library Options

    Library options can be configured using the OptionsBuilder. These options can be set through code or environment variables:

     // Reading from environment variables.
      OptionsBuilder builder = new OptionsBuilder();
                builder.LoadFromEnvironmentVariables();
                _options = builder.Build();
    
     // Assigning options manually.           
     var builder = new OptionsBuilder();
            builder
                .WithClientId("PIXEPN6N4AKKCYES51FL")
                .WithClientSecret("xUGcm3UF2q7NKzSDuNLtfq1utN38H2nF0Exrvr3O")
                .WithModifierServiceURL("https://sdl-pdp-modifier.local.platform.test/resql")
                .WithFlags(new QueryModificationFlags {
                    EmptyCLSTreatAsPermitted = true,
                    IgnoreRuntimeCLSResponse = false,
                    IgnoreSqlPdpServiceErrors = false,
                    OppositeColumnFilteringBehavior = true,
                    RuntimeCLSAsMasked = true,
                    PoliciesJoinOperation = "OR",
                    ExpandStarColumn = true,
                    JwtPassthrough = true                
                });
    
     PlainLibrary.SetOptions(builder.Build());           
    

    Environment Variables

    FlagDescriptionEnv. Variable
    ClientIdThe Client ID for the PDP service.PLAINID_CLIENT_ID
    ClientSecretThe Client Secret for the PDP service.PLAINID_CLIENT_SECRET
    ModifierServiceURLThe URL for the SQL-PDP service.PLAINID_SQLPDP_SERVICE_URL
    entityTypeIdThe entity type ID for the PDP service.PLAINID_ENTITY_TYPE_ID
    EmptyCLSTreatAsPermittedIf set to true and there are no column restrictions, all columns are allowed.
    If the PDP service returns no restrictions to columns, allow all columns rather than block them all.
    PLAINID_EMPTY_CLS_TREAT_AS_PERMITTED
    EmptyRLSTreatAsDeniedIf true and there are no row-level filtering rules, an empty statement is returned.
    If no row-level filtering rules are returned, completely block the statement or allow it as is.
    PLAINID_EMPTY_RLS_TREAT_AS_DENIED
    IgnoreRuntimeCLSResponseIf set to true, the library ignores any column-level security restrictions and treats only row-level security rules.PLAINID_IGNORE_RUNTIME_CLS_RESPONSE
    IgnoreSqlPdpServiceErrorsIf true, the library ignores any errors occurring during policy resolution; otherwise, errors propagate.
    That means that if an error occurs during a call to the PDP service, the original SQL will be returned and not be filtered.
    PLAINID_IGNORE_SQL_PDP_SERVICE_ERRORS
    OppositeColumnFilteringBehaviorIf set to true, the library filters the columns that are not listed. If set to false, the library filters the columns that are listed.
    For example: SELECT first_name, last_name, phone FROM users;
    By default, if the PDP service responds that only the first_name is allowed, then the following is returned: SELECT first_name FROM users;
    However, when the flag is on, the behavior has the reverse behavior and the result returned is: SELECT last_name, phone FROM users;
    PLAINID_OPPOSITE_COLUMN_FILTERING_BEHAVIOR
    ExpandStarColumnEnables the DB schema structure resolution to support star expansion.
    For example: SELECT * FROM users;
    In this case, for the Authorizer to know the column names, it has to know the table schema, therefore it needs a connection to the database and the ability to resolve the schema columns.
    PLAINID_EXPAND_STAR_COLUMN
    PoliciesJoinOperationJoin operation between policies. (default OR) - If there are multiple policies defined as a response to policy resolution made by the process, ensure to define how they behave when joining 2 or more policies together to form an SQL filtering rule.
    For example: SELECT first_name, last_name where phone='0544' or phone='5555'
    SELECT first_name, last_name where phone='0544' and last_name='sheinfeld'
    PLAINID_POLICIES_JOIN_OPERATION
    RuntimeCLSAsMaskedTreat column-level security filtering as masked fields rather than dropping or removing them.
    For example, this will produce: SELECT null as first_name, last_name, phone FROM users; rather than: SELECT last_name, phone FROM users;
    PLAINID_RUNTIME_CLS_AS_MASKED
    RequestMappingConfigsMaps context request parameters (headers, URI, etc.) to Runtime sections (see below for more details in Request Mapping).PLAINID_RUNTIME_REQUEST_MAPPING
    JwtPassthroughPasses the JWT supplied in the Authorization header to the Runtime request.
    If the Java application that is using this library receives a request with the JWT inside an HTTP authorization header,
    it will pass this header to the PDP service to be used to extract claims and credentials.
    PLAINID_RUNTIME_JWT_PASSTHROUGH
    EntityIdJwtMappingExtracts the claim from the JWT and passes it as the entityId to the Runtime request.
    If the JWT is being sent to the Java service using this library, the user ID from within this JWT is identified. Use this parameter to define this field inside the JWT in order to extract it and pass it as an identity or entityId to the PDP service request.
    PLAINID_ENTITY_ID_JWT_MAPPING

    Request Mapping

    The parameter accepts configuration properties that define mappings between various sources and their mapping targets in the PDP/Runtime calls. A source refers to where data is retrieved from, and a target specifies where the data should be mapped.

    Sources

    The following sources are supported:

    • headers: Data is retrieved from the request headers.
    • url: Data is extracted from the request URL based on a regex pattern.
    • qs (query-string parameter): Data is retrieved from the query string parameters of the request.

    Targets

    The following targets are supported:

    • entityAttributes: The data is mapped to the entityAttributes section in the Policy Resolution call.
    • environment: The data is mapped to the Environment section in the Policy Resolution call.
    • contextData: The data is mapped to the contextData section in the Policy Resolution call.

    Configuration Structure

    The configuration parameter is passed as key/value mapping entries. Each object should have the following structure:

    { "source.sourceKey": "target.targetKey" }
    

    Fields:

    • source: Specifies the source type. It can be headers, url, or qs.
    • sourceKey: For headers, it is the header to look for, following an optional possible JSON path extraction rule. For qs, this is the exact key to look for. For url, this is a regex pattern to match and extract data.
    • target: Specifies the target type. It can be entityAttributes, environment, or contextData.
    • targetKey: This is the key under which the data from the source is stored in the target.

    Example of a configuration:

    {
      "headers.x-user-info.$.user.department": "contextData.userInfo",
      "headers.Accept-Encoding": "entityAttributes.Accept-Encoding-Target",
      "qs.difficulty": "environment.difficulty",
      "url./api/([^/]+)(/|$)": "environment.urlPartExample"
    }
    
    • Data from the x-user-info header is extracted and mapped through the JSON path $.user.department to the contextData section under the key userInfo.
    • Data from the Accept-Encoding header is mapped to the contextData section under the key Accept-Encoding-Target.
    • Data from the difficulty query string parameter is mapped to the environment section.
    • Data matching the regex pattern /api/([^/]+)(/|$) from the URL is mapped to entityAttributes under the key urlPartExample. For example, this extracts users from the request URL /api/users/roles.

    The configuration should be passed as a JSON string to the RequestMappingConfigs property or environment variable: PLAINID_RUNTIME_REQUEST_MAPPING.

    Setting up the Library Using Dependency Injection

    DB Interceptor

    This feature translates Entity Framework Core queries going to the SQL Server and modifies them according to PlainID policies.

    To add a DB Interceptor:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<MyDbContext>(options => 
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
                   .AddInterceptors(new PlainIDbCommandInterceptor()));
    }
    

    Authorization Middleware

    This adds standard middleware that extracts the user identity and passes it to the PlainID library as user context when calling the SQL-PDP service.

    To add default Authorization middleware:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews(options => { options.Filters.Add<PlainAuthFilter>(); });
    }
    

    Manually Injecting User Context

    To manually inject user context from your Authorization layer middleware (if applicable):

    PlainContext.SetUser("SomeUSERID");
    

    Use the following code before performing database actions (if applicable) to inject entity attributes to the PDP for further Policy resolution based on Dynamic Groups:

    EntityAttributes entityAttributes = new EntityAttributes();
    entityAttributes.AddAttribute("ATTRIBUTE_NAME", "ATTRIBUTE_VALUE");
    PlainContext.SetEntityAttributes(entityAttributes);
    

    Example usage within a filtering controller:

    public class MyCustomActionFilter : IActionFilter
    {
        public void OnActionExecuting(ActionExecutingContext context)
        {
            // Extract the user id from the authorization context.
            var userId = context.HttpContext?.User?.Identity?.Name;
            if (userId != null) {
                // Inject the userId to use in the PlainID filtering library.
                PlainContext.SetUser(userId);
            }
        }
    }
    

    Limitations

    The library's hook features will not function correctly on arm64 architecture, such as M1 Macs.

    Legacy Access Directly to the API

    The library can be used to directly access the API without using Entity Framework Core, supporting legacy code or direct use of the functionality.

    The following code illustrates how this can be done, directly calling the authorizer-sql service and returning the modified SQL query:

    using Microsoft.Extensions.Logging;
    using NLog.Config;
    using NLog.Extensions.Logging;
    using NLog.Targets;
    using PlainID.SQL.PDP.Auth.Lib;
    using LogLevel = NLog.LogLevel;
    
    public class MainClass {
        static ILogger logger;
    
        static void createLogger() {
            var config = new LoggingConfiguration();
            var consoleTarget = new ColoredConsoleTarget() {
                Layout = @"${date:format=HH\:mm\:ss} ${logger} ${message}"
            };
            config.AddTarget("console", consoleTarget);
            config.AddRule(LogLevel.Trace, LogLevel.Off, consoleTarget);
    
            var loggerFactory = new LoggerFactory();
            loggerFactory.AddNLog();
            loggerFactory.ConfigureNLog(config);
            
            logger = loggerFactory.CreateLogger<MainClass>();
        }
        
        public static void Main() {
            createLogger();
            
            logger.LogInformation("Starting Direct-Mode Test");
            
            var optionsBuilder = new OptionsBuilder();
            optionsBuilder.LoadFromEnvironmentVariables();
            optionsBuilder
                .WithClientId("PBMZCUX4GCQ6BQH1CFQ7")
                .WithClientSecret("B0JCgbzMbNIls3NjKy4P12CTv0jvL2JAzbF5T8dg")
                .WithEntityTypeId("Main")
                .WithModifierServiceURL("https://authz-sql-pdp-modifier.local.platform.test/resql")            
                .WithFlags(new QueryModificationFlags {
                    EmptyCLSTreatAsPermitted = true,
                    IgnoreRuntimeCLSResponse = false,
                    IgnoreSqlPdpServiceErrors = true,
                    OppositeColumnFilteringBehavior = true,
                    RuntimeCLSAsMasked = true
                });
            
            var options = optionsBuilder.Build();
            
            var entityAttributes = new MapArrayAttributes();
            entityAttributes.AddAttribute("DemoGroup", new object[] { "Jordan" });
                
            PlainContext.SetEntityAttributes(entityAttributes);
            PlainContext.SetUser("Jordan");
            PlainLibrary.SetLogger(logger);
            PlainLibrary.SetOptions(options);
            
            AuthServiceApi authServiceApi = new AuthServiceApi();
            var modifiedSql = authServiceApi.CallSqlModifer("SELECT first_name, last_name, department_id FROM employees");
            logger.LogInformation("Modified SQL: {modifiedSql}", modifiedSql);
            
            logger.LogInformation("Ending Direct-Mode Test");
        }
    }
    

    Was this article helpful?

    What's Next