From 5a589eee6a684e4cd5aab9576c4324f5d6a505ff Mon Sep 17 00:00:00 2001 From: Gabriel Santos Date: Mon, 12 Jan 2026 14:11:54 -0500 Subject: [PATCH 1/8] added to be able to read in claims --- .../Model/ClaimQueryRestrictionAttribute.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/Gemstone.Data/Model/ClaimQueryRestrictionAttribute.cs diff --git a/src/Gemstone.Data/Model/ClaimQueryRestrictionAttribute.cs b/src/Gemstone.Data/Model/ClaimQueryRestrictionAttribute.cs new file mode 100644 index 000000000..e755774fe --- /dev/null +++ b/src/Gemstone.Data/Model/ClaimQueryRestrictionAttribute.cs @@ -0,0 +1,85 @@ +//****************************************************************************************************** +// ClaimQueryRestrictionAttribute.cs - Gbtc +// +// Copyright © 2026, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may +// not use this file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 01/09/2026 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Data; + +namespace Gemstone.Data.Model; + +/// +/// Defines an attribute that will mark a modeled table with a record restriction that applies +/// to secure query functions for a modeled . +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class ClaimQueryRestrictionAttribute : Attribute +{ + /// + /// Defines filter SQL expression for restriction as a composite format string - does not + /// include WHERE. When escaping is needed for field names, use standard ANSI quotes. + /// + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format string will be converted into + /// query parameters where each of the corresponding values in the + /// collection will be applied as values to an executed + /// query. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + public readonly string FilterExpression; + + /// + /// Defines claims which will be checked for parameter values + /// + public readonly string[] Claims; + + /// + /// Creates a new parameterized with the specified + /// SQL filter expression and parameters. + /// + /// + /// Filter SQL expression for restriction as a composite format string - does not include WHERE. + /// When escaping is needed for field names, use standard ANSI quotes. + /// + /// Claims to use for parameter values. + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format + /// will be converted into query parameters where each of the corresponding values in the + /// collection will retrieved and applied as + /// values to an executed query. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + public ClaimQueryRestrictionAttribute(string filterExpression, params string[] claims) + { + FilterExpression = filterExpression ?? throw new ArgumentNullException(nameof(filterExpression)); + Claims = claims ?? throw new ArgumentNullException(nameof(claims)); + } +} From c20f72a9b3329f829dcabfe69f81210d33082af9 Mon Sep 17 00:00:00 2001 From: Gabriel Santos Date: Mon, 12 Jan 2026 14:12:11 -0500 Subject: [PATCH 2/8] added table operations wrapper that takes into account claims --- .../Model/SecureTableOperations.cs | 1158 +++++++++++++++++ 1 file changed, 1158 insertions(+) create mode 100644 src/Gemstone.Data/Model/SecureTableOperations.cs diff --git a/src/Gemstone.Data/Model/SecureTableOperations.cs b/src/Gemstone.Data/Model/SecureTableOperations.cs new file mode 100644 index 000000000..5d9f48c1d --- /dev/null +++ b/src/Gemstone.Data/Model/SecureTableOperations.cs @@ -0,0 +1,1158 @@ +//****************************************************************************************************** +// SecureTableOperations.cs - Gbtc +// +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 02/01/2016 - G. Santos +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Gemstone.Expressions.Model; +using Gemstone.Reflection.MemberInfoExtensions; + +namespace Gemstone.Data.Model; + +/// +/// A wrapper class for +/// +/// Modeled table. +public class SecureTableOperations where T : class, new() +{ + /// + /// which performs DB operations. + /// + public TableOperations BaseOperations { get; } + + /// + /// Creates a new + /// + /// table operation which to wrap calls of. + public SecureTableOperations(TableOperations operations) + { + BaseOperations = operations; + } + + /// + /// Creates a new + /// + /// db to create secure operations to. + public SecureTableOperations(AdoDataConnection connection) + { + BaseOperations = new(connection); + } + + #region [ Methods ] + + /// + /// Transforms a into an equivalent , as defined by the model's . + /// + /// Claims principal which is making the request. + /// + /// + private static RecordRestriction? GetClaimRecordRestriction(ClaimsPrincipal principal) + { + if (s_claimQueryRestrictionAttribute is null) + return null; + + object[] claims = s_claimQueryRestrictionAttribute.Claims + .Select(claimKey => principal.FindFirst(claimKey) ?? throw new InvalidOperationException($"Unable to retrieve {claimKey} claim from user.")) + .Select(claim => claim.Value) + .ToArray(); + + return new RecordRestriction(s_claimQueryRestrictionAttribute.FilterExpression, claims); + } + + /// + /// Queries database and returns a single modeled table record for the specified . + /// + /// Claims principal which is making the request. + /// Record restriction to apply. + /// A single modeled table record for the queried record. + /// + /// + /// If no record is found for specified , null will be returned. + /// + /// + /// This is a convenience call to + /// specifying the parameter with a limit of 1 record. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + public T? QueryRecord(ClaimsPrincipal principal, RecordRestriction? restriction) => + BaseOperations.QueryRecord(restriction + GetClaimRecordRestriction(principal)); + + /// + /// Queries database and returns a single modeled table record for the specified . + /// + /// Claims principal which is making the request. + /// Record restriction to apply. + /// Propagates notification that operations should be canceled. + /// A single modeled table record for the queried record. + /// + /// + /// If no record is found for specified , null will be returned. + /// + /// + /// This is a convenience call to + /// specifying the parameter with a limit of 1 record. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + public ValueTask QueryRecordAsync(ClaimsPrincipal principal, RecordRestriction? restriction, CancellationToken cancellationToken) => + BaseOperations.QueryRecordAsync(restriction + GetClaimRecordRestriction(principal), cancellationToken); + + /// + /// Queries database and returns a single modeled table record for the specified , + /// execution of query will apply . + /// + /// Claims principal which is making the request. + /// Field name expression used for sort order, include ASC or DESC as needed - does not include ORDER BY; defaults to primary keys. + /// Record restriction to apply. + /// A single modeled table record for the queried record. + /// + /// + /// If no record is found for specified , null will be returned. + /// + /// + /// This is a convenience call to + /// specifying the parameter with a limit of 1 record. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + public T? QueryRecord(ClaimsPrincipal principal, string? orderByExpression, RecordRestriction? restriction) => + BaseOperations.QueryRecord(orderByExpression, restriction + GetClaimRecordRestriction(principal)); + + /// + /// Queries database and returns a single modeled table record for the specified , + /// execution of query will apply . + /// + /// Claims principal which is making the request. + /// Field name expression used for sort order, include ASC or DESC as needed - does not include ORDER BY; defaults to primary keys. + /// Record restriction to apply. + /// Propagates notification that operations should be canceled. + /// A single modeled table record for the queried record. + /// + /// + /// If no record is found for specified , null will be returned. + /// + /// + /// This is a convenience call to + /// specifying the parameter with a limit of 1 record. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + public ValueTask QueryRecordAsync(ClaimsPrincipal principal, string? orderByExpression, RecordRestriction? restriction, CancellationToken cancellationToken) => + BaseOperations.QueryRecordAsync(orderByExpression, restriction + GetClaimRecordRestriction(principal), cancellationToken); + + /// + /// Queries database and returns a single modeled table record for the specified SQL filter + /// expression and parameters. + /// + /// Claims principal which is making the request. + /// + /// Filter SQL expression for restriction as a composite format string - does not include WHERE. + /// When escaping is needed for field names, use standard ANSI quotes. + /// + /// Restriction parameter values. + /// A single modeled table record for the queried record. + /// + /// + /// If no record is found for specified filter expression and parameters, null will be returned. + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format + /// will be converted into query parameters where each of the corresponding values in the + /// collection will be applied as + /// values to an executed query. + /// + /// + /// If any of the specified reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + /// This is a convenience call to + /// specifying the parameter with a limit of 1 record. + /// + /// + public T? QueryRecordWhere(ClaimsPrincipal principal, string? filterExpression, params object?[] parameters) => + QueryRecord(principal, new RecordRestriction(filterExpression, parameters)); + + /// + /// Queries database and returns a single modeled table record for the specified SQL filter + /// expression and parameters. + /// + /// Claims principal which is making the request. + /// + /// Filter SQL expression for restriction as a composite format string - does not include WHERE. + /// When escaping is needed for field names, use standard ANSI quotes. + /// + /// Propagates notification that operations should be canceled. + /// Restriction parameter values. + /// A single modeled table record for the queried record. + /// + /// + /// If no record is found for specified filter expression and parameters, null will be returned. + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format + /// will be converted into query parameters where each of the corresponding values in the + /// collection will be applied as + /// values to an executed query. + /// + /// + /// If any of the specified reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + /// This is a convenience call to + /// specifying the parameter with a limit of 1 record. + /// + /// + public ValueTask QueryRecordWhereAsync(ClaimsPrincipal principal, string? filterExpression, CancellationToken cancellationToken, params object?[] parameters) => + QueryRecordAsync(principal, new RecordRestriction(filterExpression, parameters), cancellationToken); + + /// + /// Queries database and returns modeled table records for the specified parameters. + /// + /// Claims principal which is making the request. + /// Field name expression used for sort order, include ASC or DESC as needed - does not include ORDER BY; defaults to primary keys. + /// Record restriction to apply, if any. + /// Limit of number of record to return. + /// An enumerable of modeled table row instances for queried records. + /// + /// + /// If no record or is provided, all rows will be returned. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + public IEnumerable QueryRecords(ClaimsPrincipal principal, string? orderByExpression = null, RecordRestriction? restriction = null, int limit = -1) => + BaseOperations.QueryRecords(orderByExpression, restriction + GetClaimRecordRestriction(principal), limit); + + /// + /// Queries database and returns modeled table records for the specified parameters. + /// + /// Claims principal which is making the request. + /// Field name expression used for sort order, include ASC or DESC as needed - does not include ORDER BY; defaults to primary keys. + /// Record restriction to apply, if any. + /// Limit of number of record to return. + /// Propagates notification that operations should be canceled. + /// An enumerable of modeled table row instances for queried records. + /// + /// + /// If no record or is provided, all rows will be returned. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + public IAsyncEnumerable QueryRecordsAsync(ClaimsPrincipal principal, string? orderByExpression = null, RecordRestriction? restriction = null, int limit = -1, CancellationToken cancellationToken = default) => + BaseOperations.QueryRecordsAsync(orderByExpression, restriction + GetClaimRecordRestriction(principal), limit, cancellationToken); + + /// + /// Queries database and returns modeled table records for the specified . + /// + /// Claims principal which is making the request. + /// Record restriction to apply. + /// An enumerable of modeled table row instances for queried records. + /// + /// + /// This is a convenience call to only + /// specifying the parameter. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + public IEnumerable QueryRecords(ClaimsPrincipal principal, RecordRestriction? restriction) => + BaseOperations.QueryRecords(restriction + GetClaimRecordRestriction(principal)); + + /// + /// Queries database and returns modeled table records for the specified . + /// + /// Claims principal which is making the request. + /// Record restriction to apply. + /// Propagates notification that operations should be canceled. + /// An enumerable of modeled table row instances for queried records. + /// + /// + /// This is a convenience call to only + /// specifying the parameter. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + public IAsyncEnumerable QueryRecordsAsync(ClaimsPrincipal principal, RecordRestriction? restriction, CancellationToken cancellationToken) => + BaseOperations.QueryRecordsAsync(restriction + GetClaimRecordRestriction(principal), cancellationToken); + + /// + /// Queries database and returns modeled table records for the specified SQL filter expression + /// and parameters. + /// + /// Claims principal which is making the request. + /// + /// Filter SQL expression for restriction as a composite format string - does not include WHERE. + /// When escaping is needed for field names, use standard ANSI quotes. + /// + /// Restriction parameter values. + /// An enumerable of modeled table row instances for queried records. + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format + /// will be converted into query parameters where each of the corresponding values in the + /// collection will be applied as + /// values to an executed query. + /// + /// + /// If any of the specified reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + /// This is a convenience call to only + /// specifying the parameter. + /// + /// + public IEnumerable QueryRecordsWhere(ClaimsPrincipal principal, string? filterExpression, params object?[] parameters) => + BaseOperations.QueryRecords(new RecordRestriction(filterExpression, parameters) + GetClaimRecordRestriction(principal)); + + /// + /// Queries database and returns modeled table records for the specified SQL filter expression + /// and parameters. + /// + /// Claims principal which is making the request. + /// + /// Filter SQL expression for restriction as a composite format string - does not include WHERE. + /// When escaping is needed for field names, use standard ANSI quotes. + /// + /// Propagates notification that operations should be canceled. + /// Restriction parameter values. + /// An enumerable of modeled table row instances for queried records. + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format + /// will be converted into query parameters where each of the corresponding values in the + /// collection will be applied as + /// values to an executed query. + /// + /// + /// If any of the specified reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + /// This is a convenience call to only + /// specifying the parameter. + /// + /// + public IAsyncEnumerable QueryRecordsWhereAsync(ClaimsPrincipal principal, string? filterExpression, CancellationToken cancellationToken, params object?[] parameters) => + BaseOperations.QueryRecordsAsync(new RecordRestriction(filterExpression, parameters) + GetClaimRecordRestriction(principal), cancellationToken); + + /// + /// Queries database and returns modeled table records for the specified sorting and paging parameters. + /// + /// Claims principal which is making the request. + /// Field name to order-by. + /// Sort ascending flag; set to false for descending. + /// Page number of records to return (1-based). + /// Current page size. + /// An enumerable of modeled table row instances for queried records. + /// + /// + /// This function is used for record paging. Primary keys are cached server-side, typically per user session, + /// to maintain desired per-page sort order. Call to manually clear cache + /// when table contents are known to have changed. + /// + /// + /// If the specified has been marked with , + /// establishing the primary key cache operation will take longer to execute since query data will need to + /// be downloaded locally and decrypted so the proper sort order can be determined. + /// + /// + public IEnumerable QueryRecords(ClaimsPrincipal principal, string? sortField, bool ascending, int page, int pageSize) => + BaseOperations.QueryRecords(sortField, ascending, page, pageSize, GetClaimRecordRestriction(principal)); + + /// + /// Queries database and returns modeled table records for the specified sorting and paging parameters. + /// + /// Claims principal which is making the request. + /// Field name to order-by. + /// Sort ascending flag; set to false for descending. + /// Page number of records to return (1-based). + /// Current page size. + /// Propagates notification that operations should be canceled. + /// An enumerable of modeled table row instances for queried records. + /// + /// + /// This function is used for record paging. Primary keys are cached server-side, typically per user session, + /// to maintain desired per-page sort order. Call to manually clear cache + /// when table contents are known to have changed. + /// + /// + /// If the specified has been marked with , + /// establishing the primary key cache operation will take longer to execute since query data will need to + /// be downloaded locally and decrypted so the proper sort order can be determined. + /// + /// + public IAsyncEnumerable QueryRecordsAsync(ClaimsPrincipal principal, string? sortField, bool ascending, int page, int pageSize, CancellationToken cancellationToken) => + BaseOperations.QueryRecordsAsync(sortField, ascending, page, pageSize, cancellationToken, GetClaimRecordRestriction(principal)); + + /// + /// Queries database and returns modeled table records for the specified sorting, paging and search parameters. + /// Search executed against fields modeled with . + /// + /// Claims principal which is making the request. + /// Field name to order-by. + /// Sort ascending flag; set to false for descending. + /// Page number of records to return (1-based). + /// Current page size. + /// Record Filters to be applied. + /// An enumerable of modeled table row instances for queried records. + /// + /// + /// This function is used for record paging. Primary keys are cached server-side, typically per user session, + /// to maintain desired per-page sort order. Call to manually clear cache + /// when table contents are known to have changed. + /// + /// + /// If the specified has been marked with , + /// establishing the primary key cache operation will take longer to execute since query data will need to + /// be downloaded locally and decrypted so the proper sort order can be determined. + /// + /// + /// This is a convenience call to where restriction + /// is generated by using . + /// + /// + public IEnumerable QueryRecords(ClaimsPrincipal principal, string? sortField, bool ascending, int page, int pageSize, params IRecordFilter?[]? recordFilters) => + BaseOperations.QueryRecords(sortField, ascending, page, pageSize, (BaseOperations.GetSearchRestrictions(recordFilters) ?? []).Append(GetClaimRecordRestriction(principal)).ToArray()); + + /// + /// Queries database and returns modeled table records for the specified sorting, paging and search parameters. + /// Search executed against fields modeled with . + /// + /// Claims principal which is making the request. + /// Field name to order-by. + /// Sort ascending flag; set to false for descending. + /// Page number of records to return (1-based). + /// Current page size. + /// Propagates notification that operations should be canceled. + /// Record Filters to be applied. + /// An enumerable of modeled table row instances for queried records. + /// + /// + /// This function is used for record paging. Primary keys are cached server-side, typically per user session, + /// to maintain desired per-page sort order. Call to manually clear cache + /// when table contents are known to have changed. + /// + /// + /// If the specified has been marked with , + /// establishing the primary key cache operation will take longer to execute since query data will need to + /// be downloaded locally and decrypted so the proper sort order can be determined. + /// + /// + /// This is a convenience call to where restriction + /// is generated by using . + /// + /// + public IAsyncEnumerable QueryRecordsAsync(ClaimsPrincipal principal, string? sortField, bool ascending, int page, int pageSize, CancellationToken cancellationToken, params IRecordFilter?[]? recordFilters) => + BaseOperations.QueryRecordsAsync(sortField, ascending, page, pageSize, cancellationToken, (BaseOperations.GetSearchRestrictions(recordFilters) ?? []).Append(GetClaimRecordRestriction(principal)).ToArray()); + + /// + /// Queries database and returns modeled table records for the specified sorting and paging parameters. + /// + /// Claims principal which is making the request. + /// Field name to order-by. + /// Sort ascending flag; set to false for descending. + /// Page number of records to return (1-based). + /// Current page size. + /// Record restrictions to apply, if any. + /// An enumerable of modeled table row instances for queried records. + /// + /// + /// This function is used for record paging. Primary keys are cached server-side, typically per user session, + /// to maintain desired per-page sort order. Call to manually clear cache + /// when table contents are known to have changed. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If the specified has been marked with , + /// establishing the primary key cache operation will take longer to execute since query data will need to + /// be downloaded locally and decrypted so the proper sort order can be determined. + /// + /// + public IEnumerable QueryRecords(ClaimsPrincipal principal, string? sortField, bool ascending, int page, int pageSize, params RecordRestriction?[]? restrictions) => + BaseOperations.QueryRecords(sortField, ascending, page, pageSize, (restrictions ?? []).Append(GetClaimRecordRestriction(principal)).ToArray()); + + /// + /// Queries database and returns modeled table records for the specified sorting and paging parameters. + /// + /// Claims principal which is making the request. + /// Field name to order-by. + /// Sort ascending flag; set to false for descending. + /// Page number of records to return (1-based). + /// Current page size. + /// Propagates notification that operations should be canceled. + /// Record restrictions to apply, if any. + /// An enumerable of modeled table row instances for queried records. + /// + /// + /// This function is used for record paging. Primary keys are cached server-side, typically per user session, + /// to maintain desired per-page sort order. Call to manually clear cache + /// when table contents are known to have changed. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If the specified has been marked with , + /// establishing the primary key cache operation will take longer to execute since query data will need to + /// be downloaded locally and decrypted so the proper sort order can be determined. + /// + /// + public IAsyncEnumerable QueryRecordsAsync(ClaimsPrincipal principal, string? sortField, bool ascending, int page, int pageSize, [EnumeratorCancellation] CancellationToken cancellationToken, params RecordRestriction?[]? restrictions) => + BaseOperations.QueryRecordsAsync(sortField, ascending, page, pageSize, cancellationToken, (restrictions ?? []).Append(GetClaimRecordRestriction(principal)).ToArray()); + + /// + /// Gets total record count for the modeled table. + /// + /// Claims principal which is making the request. + /// + /// Total record count for the modeled table. + /// + public int QueryRecordCount(ClaimsPrincipal principal) => + BaseOperations.QueryRecordCount([GetClaimRecordRestriction(principal)]); + + /// + /// Gets total record count for the modeled table. + /// + /// Claims principal which is making the request. + /// Propagates notification that operations should be canceled. + /// + /// Total record count for the modeled table. + /// + public Task QueryRecordCountAsync(ClaimsPrincipal principal, CancellationToken cancellationToken) => + BaseOperations.QueryRecordCountAsync(cancellationToken, [GetClaimRecordRestriction(principal)]); + + /// + /// Gets the record count for the modeled table based on search parameter. + /// Search executed against fields modeled with . + /// + /// Claims principal which is making the request. + /// to be filtered by + /// Record count for the modeled table based on search parameter. + /// + /// This is a convenience call to where restriction + /// is generated by + /// + public int QueryRecordCount(ClaimsPrincipal principal, params IRecordFilter?[]? recordFilter) => + BaseOperations.QueryRecordCount((BaseOperations.GetSearchRestrictions(recordFilter) ?? []).Append(GetClaimRecordRestriction(principal)).ToArray()); + + /// + /// Gets the record count for the modeled table based on search parameter. + /// Search executed against fields modeled with . + /// + /// Claims principal which is making the request. + /// Propagates notification that operations should be canceled. + /// to be filtered by + /// Record count for the modeled table based on search parameter. + /// + /// This is a convenience call to where restriction + /// is generated by + /// + public Task QueryRecordCountAsync(ClaimsPrincipal principal, CancellationToken cancellationToken, params IRecordFilter?[]? recordFilter) => + BaseOperations.QueryRecordCountAsync(cancellationToken, (BaseOperations.GetSearchRestrictions(recordFilter) ?? []).Append(GetClaimRecordRestriction(principal)).ToArray()); + + /// + /// Gets the record count for the specified - or - total record + /// count for the modeled table if is null. + /// + /// Claims principal which is making the request. + /// Record restrictions to apply, if any. + /// + /// Record count for the specified - or - total record count + /// for the modeled table if no is provided. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + public int QueryRecordCount(ClaimsPrincipal principal, params RecordRestriction?[]? restrictions) => + BaseOperations.QueryRecordCount((restrictions ?? []).Append(GetClaimRecordRestriction(principal)).ToArray()); + + /// + /// Gets the record count for the specified - or - total record + /// count for the modeled table if is null. + /// + /// Claims principal which is making the request. + /// Propagates notification that operations should be canceled. + /// Record restrictions to apply, if any. + /// + /// Record count for the specified - or - total record count + /// for the modeled table if no is provided. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + public Task QueryRecordCountAsync(ClaimsPrincipal principal, CancellationToken cancellationToken, params RecordRestriction?[]? restrictions) => + BaseOperations.QueryRecordCountAsync(cancellationToken, (restrictions ?? []).Append(GetClaimRecordRestriction(principal)).ToArray()); + + /// + /// Gets the record count for the modeled table for the specified SQL filter expression and parameters. + /// + /// Claims principal which is making the request. + /// + /// Filter SQL expression for restriction as a composite format string - does not include WHERE. + /// When escaping is needed for field names, use standard ANSI quotes. + /// + /// Restriction parameter values. + /// Record count for the modeled table for the specified parameters. + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format + /// will be converted into query parameters where each of the corresponding values in the + /// collection will be applied as + /// values to an executed query. + /// + /// + /// If any of the specified reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + /// This is a convenience call to . + /// + /// + public int QueryRecordCountWhere(ClaimsPrincipal principal, string? filterExpression, params object?[] parameters) => + BaseOperations.QueryRecordCount(new RecordRestriction(filterExpression, parameters) + GetClaimRecordRestriction(principal)); + + /// + /// Gets the record count for the modeled table for the specified SQL filter expression and parameters. + /// + /// Claims principal which is making the request. + /// + /// Filter SQL expression for restriction as a composite format string - does not include WHERE. + /// When escaping is needed for field names, use standard ANSI quotes. + /// + /// Propagates notification that operations should be canceled. + /// Restriction parameter values. + /// Record count for the modeled table for the specified parameters. + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format + /// will be converted into query parameters where each of the corresponding values in the + /// collection will be applied as + /// values to an executed query. + /// + /// + /// If any of the specified reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + /// This is a convenience call to . + /// + /// + public Task QueryRecordCountWhereAsync(ClaimsPrincipal principal, string? filterExpression, CancellationToken cancellationToken, params object?[] parameters) => + BaseOperations.QueryRecordCountAsync(cancellationToken, new RecordRestriction(filterExpression, parameters) + GetClaimRecordRestriction(principal)); + + /// + /// Deletes the records referenced by the specified . + /// + /// Claims principal which is making the request. + /// Record restriction to apply + /// + /// Flag that determines if any existing should be applied. Defaults to + /// setting. + /// + /// Number of rows affected. + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// cannot be null. + public int DeleteRecord(ClaimsPrincipal principal, RecordRestriction? restriction, bool? applyRootQueryRestriction = null) => + BaseOperations.DeleteRecord(restriction + GetClaimRecordRestriction(principal), applyRootQueryRestriction); + + /// + /// Deletes the records referenced by the specified . + /// + /// Claims principal which is making the request. + /// Record restriction to apply + /// Propagates notification that operations should be canceled. + /// + /// Flag that determines if any existing should be applied. Defaults to + /// setting. + /// + /// Number of rows affected. + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// cannot be null. + public Task DeleteRecordAsync(ClaimsPrincipal principal, RecordRestriction? restriction, CancellationToken cancellationToken, bool? applyRootQueryRestriction = null) => + BaseOperations.DeleteRecordAsync(restriction + GetClaimRecordRestriction(principal), cancellationToken, applyRootQueryRestriction); + + /// + /// Deletes the records referenced by the specified SQL filter expression and parameters. + /// + /// Claims principal which is making the request. + /// + /// Filter SQL expression for restriction as a composite format string - does not include WHERE. + /// When escaping is needed for field names, use standard ANSI quotes. + /// + /// Restriction parameter values. + /// Number of rows affected. + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format + /// will be converted into query parameters where each of the corresponding values in the + /// collection will be applied as + /// values to an executed query. + /// + /// + /// If any of the specified reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + /// This is a convenience call to . + /// + /// + public int DeleteRecordWhere(ClaimsPrincipal principal, string filterExpression, params object?[] parameters) => + DeleteRecord(principal, new RecordRestriction(filterExpression, parameters)); + + /// + /// Deletes the records referenced by the specified SQL filter expression and parameters. + /// + /// Claims principal which is making the request. + /// + /// Filter SQL expression for restriction as a composite format string - does not include WHERE. + /// When escaping is needed for field names, use standard ANSI quotes. + /// + /// Propagates notification that operations should be canceled. + /// Restriction parameter values. + /// Number of rows affected. + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format + /// will be converted into query parameters where each of the corresponding values in the + /// collection will be applied as + /// values to an executed query. + /// + /// + /// If any of the specified reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + /// This is a convenience call to . + /// + /// + public Task DeleteRecordWhereAsync(ClaimsPrincipal principal, string filterExpression, CancellationToken cancellationToken, params object?[] parameters) => + DeleteRecordAsync(principal, new RecordRestriction(filterExpression, parameters), cancellationToken); + + /// + /// Updates the database with the specified modeled table , + /// any model properties marked with will + /// be evaluated and applied before the record is provided to the data source. + /// + /// Claims principal which is making the request. + /// Record to update. + /// Record restriction to apply, if any. + /// + /// Flag that determines if any existing should be applied. Defaults to + /// setting. + /// + /// Number of rows affected. + /// + /// + /// Record restriction is only used for custom update expressions or in cases where modeled + /// table has no defined primary keys. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + public int UpdateRecord(ClaimsPrincipal principal, T record, RecordRestriction? restriction = null, bool? applyRootQueryRestriction = null) => + BaseOperations.UpdateRecord(record, restriction + GetClaimRecordRestriction(principal), applyRootQueryRestriction); + + /// + /// Updates the database with the specified modeled table , + /// any model properties marked with will + /// be evaluated and applied before the record is provided to the data source. + /// + /// Claims principal which is making the request. + /// Record to update. + /// Propagates notification that operations should be canceled. + /// Record restriction to apply, if any. + /// + /// Flag that determines if any existing should be applied. Defaults to + /// setting. + /// + /// Number of rows affected. + /// + /// + /// Record restriction is only used for custom update expressions or in cases where modeled + /// table has no defined primary keys. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + public Task UpdateRecordAsync(ClaimsPrincipal principal, T record, CancellationToken cancellationToken, RecordRestriction? restriction = null, bool? applyRootQueryRestriction = null) => + BaseOperations.UpdateRecordAsync(record, cancellationToken, restriction + GetClaimRecordRestriction(principal), applyRootQueryRestriction); + + /// + /// Updates the database with the specified modeled table + /// referenced by the specified SQL filter expression and parameters, any model properties + /// marked with will be evaluated and applied + /// before the record is provided to the data source. + /// + /// Claims principal which is making the request. + /// Record to update. + /// + /// Filter SQL expression for restriction as a composite format string - does not include WHERE. + /// When escaping is needed for field names, use standard ANSI quotes. + /// + /// Restriction parameter values. + /// Number of rows affected. + /// + /// + /// Record restriction is only used for custom update expressions or in cases where modeled + /// table has no defined primary keys. + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format + /// will be converted into query parameters where each of the corresponding values in the + /// collection will be applied as + /// values to an executed query. + /// + /// + /// If any of the specified reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + /// This is a convenience call to . + /// + /// + public int UpdateRecordWhere(ClaimsPrincipal principal, T record, string filterExpression, params object?[] parameters) => + UpdateRecord(principal, record, new RecordRestriction(filterExpression, parameters)); + + /// + /// Updates the database with the specified modeled table + /// referenced by the specified SQL filter expression and parameters, any model properties + /// marked with will be evaluated and applied + /// before the record is provided to the data source. + /// + /// Claims principal which is making the request. + /// Record to update. + /// + /// Filter SQL expression for restriction as a composite format string - does not include WHERE. + /// When escaping is needed for field names, use standard ANSI quotes. + /// + /// Propagates notification that operations should be canceled. + /// Restriction parameter values. + /// Number of rows affected. + /// + /// + /// Record restriction is only used for custom update expressions or in cases where modeled + /// table has no defined primary keys. + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format + /// will be converted into query parameters where each of the corresponding values in the + /// collection will be applied as + /// values to an executed query. + /// + /// + /// If any of the specified reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + /// This is a convenience call to . + /// + /// + public Task UpdateRecordWhereAsync(ClaimsPrincipal principal, T record, string filterExpression, CancellationToken cancellationToken, params object?[] parameters) => + UpdateRecordAsync(principal, record, cancellationToken, new RecordRestriction(filterExpression, parameters)); + + /// + /// Updates the database with the specified , any model properties + /// marked with will be evaluated and applied + /// before the record is provided to the data source. + /// + /// Claims principal which is making the request. + /// of queried data to be updated. + /// Record restriction to apply, if any. + /// Number of rows affected. + /// + /// + /// Record restriction is only used for custom update expressions or in cases where modeled + /// table has no defined primary keys. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + public int UpdateRecord(ClaimsPrincipal principal, DataRow row, RecordRestriction? restriction = null) => + BaseOperations.UpdateRecord(row, restriction + GetClaimRecordRestriction(principal)); + + /// + /// Updates the database with the specified , any model properties + /// marked with will be evaluated and applied + /// before the record is provided to the data source. + /// + /// Claims principal which is making the request. + /// of queried data to be updated. + /// Propagates notification that operations should be canceled. + /// Record restriction to apply, if any. + /// Number of rows affected. + /// + /// + /// Record restriction is only used for custom update expressions or in cases where modeled + /// table has no defined primary keys. + /// + /// + /// If any of the parameters reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + public Task UpdateRecordAsync(ClaimsPrincipal principal, DataRow row, CancellationToken cancellationToken, RecordRestriction? restriction = null) => + BaseOperations.UpdateRecordAsync(row, cancellationToken, restriction + GetClaimRecordRestriction(principal)); + + /// + /// Updates the database with the specified referenced by the + /// specified SQL filter expression and parameters, any model properties marked with + /// will be evaluated and applied before + /// the record is provided to the data source. + /// + /// Claims principal which is making the request. + /// of queried data to be updated. + /// + /// Filter SQL expression for restriction as a composite format string - does not include WHERE. + /// When escaping is needed for field names, use standard ANSI quotes. + /// + /// Restriction parameter values. + /// Number of rows affected. + /// + /// + /// Record restriction is only used for custom update expressions or in cases where modeled + /// table has no defined primary keys. + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format + /// will be converted into query parameters where each of the corresponding values in the + /// collection will be applied as + /// values to an executed query. + /// + /// + /// If any of the specified reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + /// This is a convenience call to . + /// + /// + public int UpdateRecordWhere(ClaimsPrincipal principal, DataRow row, string filterExpression, params object?[] parameters) => + UpdateRecord(principal, row, new RecordRestriction(filterExpression, parameters)); + + /// + /// Updates the database with the specified referenced by the + /// specified SQL filter expression and parameters, any model properties marked with + /// will be evaluated and applied before + /// the record is provided to the data source. + /// + /// Claims principal which is making the request. + /// of queried data to be updated. + /// + /// Filter SQL expression for restriction as a composite format string - does not include WHERE. + /// When escaping is needed for field names, use standard ANSI quotes. + /// + /// Propagates notification that operations should be canceled. + /// Restriction parameter values. + /// Number of rows affected. + /// + /// + /// Record restriction is only used for custom update expressions or in cases where modeled + /// table has no defined primary keys. + /// + /// + /// Each indexed parameter, e.g., "{0}", in the composite format + /// will be converted into query parameters where each of the corresponding values in the + /// collection will be applied as + /// values to an executed query. + /// + /// + /// If any of the specified reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + /// If needed, field names that are escaped with standard ANSI quotes in the filter expression + /// will be updated to reflect what is defined in the user model. + /// + /// + /// This is a convenience call to . + /// + /// + public Task UpdateRecordWhereAsync(ClaimsPrincipal principal, DataRow row, string filterExpression, CancellationToken cancellationToken, params object?[] parameters) => + UpdateRecordAsync(principal, row, cancellationToken, new RecordRestriction(filterExpression, parameters)); + + #endregion + + #region [ Static ] + + private static readonly ClaimQueryRestrictionAttribute? s_claimQueryRestrictionAttribute; + + static SecureTableOperations() + { + typeof(T).TryGetAttribute(out s_claimQueryRestrictionAttribute); + } + + #endregion +} From ec3e2e13910d4b93e294b9f9a5b5d667663ca008 Mon Sep 17 00:00:00 2001 From: Gabriel Santos Date: Tue, 13 Jan 2026 10:28:41 -0500 Subject: [PATCH 3/8] added support for multiple attributes --- .../Model/SecureTableOperations.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Gemstone.Data/Model/SecureTableOperations.cs b/src/Gemstone.Data/Model/SecureTableOperations.cs index 5d9f48c1d..d1e887b51 100644 --- a/src/Gemstone.Data/Model/SecureTableOperations.cs +++ b/src/Gemstone.Data/Model/SecureTableOperations.cs @@ -73,15 +73,20 @@ public SecureTableOperations(AdoDataConnection connection) /// private static RecordRestriction? GetClaimRecordRestriction(ClaimsPrincipal principal) { - if (s_claimQueryRestrictionAttribute is null) + if (s_claimQueryRestrictionAttributes is null) return null; - - object[] claims = s_claimQueryRestrictionAttribute.Claims - .Select(claimKey => principal.FindFirst(claimKey) ?? throw new InvalidOperationException($"Unable to retrieve {claimKey} claim from user.")) - .Select(claim => claim.Value) - .ToArray(); - return new RecordRestriction(s_claimQueryRestrictionAttribute.FilterExpression, claims); + RecordRestriction? completeRestriction = null; + foreach(ClaimQueryRestrictionAttribute attribute in s_claimQueryRestrictionAttributes) + { + object[] claimValues = attribute.Claims + .Select(claimKey => principal.FindFirst(claimKey) ?? throw new InvalidOperationException($"Unable to retrieve {claimKey} claim from user.")) + .Select(claim => claim.Value) + .ToArray(); + completeRestriction += new RecordRestriction(attribute.FilterExpression, claimValues); + } + + return completeRestriction; } /// @@ -1147,11 +1152,12 @@ public Task UpdateRecordWhereAsync(ClaimsPrincipal principal, DataRow row, #region [ Static ] - private static readonly ClaimQueryRestrictionAttribute? s_claimQueryRestrictionAttribute; + private static readonly ClaimQueryRestrictionAttribute[]? s_claimQueryRestrictionAttributes; static SecureTableOperations() { - typeof(T).TryGetAttribute(out s_claimQueryRestrictionAttribute); + if (typeof(T).TryGetAttributes(out ClaimQueryRestrictionAttribute[]? claimAttributes)) + s_claimQueryRestrictionAttributes = claimAttributes; } #endregion From 29498584cfd25d00e11dbc25cbeabb709c95bda2 Mon Sep 17 00:00:00 2001 From: Gabriel Santos Date: Thu, 15 Jan 2026 15:56:46 -0500 Subject: [PATCH 4/8] moved creation of restriction logic out of here and into static functions --- .../Model/ClaimQueryRestrictionAttribute.cs | 49 +++---------------- 1 file changed, 8 insertions(+), 41 deletions(-) diff --git a/src/Gemstone.Data/Model/ClaimQueryRestrictionAttribute.cs b/src/Gemstone.Data/Model/ClaimQueryRestrictionAttribute.cs index e755774fe..d5fa3838e 100644 --- a/src/Gemstone.Data/Model/ClaimQueryRestrictionAttribute.cs +++ b/src/Gemstone.Data/Model/ClaimQueryRestrictionAttribute.cs @@ -27,29 +27,14 @@ namespace Gemstone.Data.Model; /// -/// Defines an attribute that will mark a modeled table with a record restriction that applies +/// Defines an attribute that will mark a modeled table static function as a method to create a /// to secure query functions for a modeled . /// -[AttributeUsage(AttributeTargets.Class)] -public sealed class ClaimQueryRestrictionAttribute : Attribute +/// The static function should be a part of the modeled class, and have the footprint +/// MethodName( []) +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public sealed class ClaimRestrictionAttribute : Attribute { - /// - /// Defines filter SQL expression for restriction as a composite format string - does not - /// include WHERE. When escaping is needed for field names, use standard ANSI quotes. - /// - /// - /// - /// Each indexed parameter, e.g., "{0}", in the composite format string will be converted into - /// query parameters where each of the corresponding values in the - /// collection will be applied as values to an executed - /// query. - /// - /// - /// If needed, field names that are escaped with standard ANSI quotes in the filter expression - /// will be updated to reflect what is defined in the user model. - /// - /// - public readonly string FilterExpression; /// /// Defines claims which will be checked for parameter values @@ -57,29 +42,11 @@ public sealed class ClaimQueryRestrictionAttribute : Attribute public readonly string[] Claims; /// - /// Creates a new parameterized with the specified - /// SQL filter expression and parameters. + /// Creates a new parameterized with the specified claims. /// - /// - /// Filter SQL expression for restriction as a composite format string - does not include WHERE. - /// When escaping is needed for field names, use standard ANSI quotes. - /// - /// Claims to use for parameter values. - /// - /// - /// Each indexed parameter, e.g., "{0}", in the composite format - /// will be converted into query parameters where each of the corresponding values in the - /// collection will retrieved and applied as - /// values to an executed query. - /// - /// - /// If needed, field names that are escaped with standard ANSI quotes in the filter expression - /// will be updated to reflect what is defined in the user model. - /// - /// - public ClaimQueryRestrictionAttribute(string filterExpression, params string[] claims) + /// Claims to use for parameter values. The order of parameters is in the same order as claims, grabbing as many values as each claim provides + public ClaimRestrictionAttribute(params string[] claims) { - FilterExpression = filterExpression ?? throw new ArgumentNullException(nameof(filterExpression)); Claims = claims ?? throw new ArgumentNullException(nameof(claims)); } } From 74408056bbb0f85c880df9744e07aafb3be16008 Mon Sep 17 00:00:00 2001 From: Gabriel Santos Date: Thu, 15 Jan 2026 15:58:45 -0500 Subject: [PATCH 5/8] reworked secure restriction forming to use changed attribute --- .../Model/SecureTableOperations.cs | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/Gemstone.Data/Model/SecureTableOperations.cs b/src/Gemstone.Data/Model/SecureTableOperations.cs index d1e887b51..d5010ba6e 100644 --- a/src/Gemstone.Data/Model/SecureTableOperations.cs +++ b/src/Gemstone.Data/Model/SecureTableOperations.cs @@ -25,12 +25,12 @@ using System.Collections.Generic; using System.Data; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Gemstone.Expressions.Model; -using Gemstone.Reflection.MemberInfoExtensions; namespace Gemstone.Data.Model; @@ -66,27 +66,17 @@ public SecureTableOperations(AdoDataConnection connection) #region [ Methods ] /// - /// Transforms a into an equivalent , as defined by the model's . + /// Transforms a into an equivalent , as defined by the model's . /// /// Claims principal which is making the request. /// /// private static RecordRestriction? GetClaimRecordRestriction(ClaimsPrincipal principal) { - if (s_claimQueryRestrictionAttributes is null) + if (!s_claimFunctions.Any()) return null; - RecordRestriction? completeRestriction = null; - foreach(ClaimQueryRestrictionAttribute attribute in s_claimQueryRestrictionAttributes) - { - object[] claimValues = attribute.Claims - .Select(claimKey => principal.FindFirst(claimKey) ?? throw new InvalidOperationException($"Unable to retrieve {claimKey} claim from user.")) - .Select(claim => claim.Value) - .ToArray(); - completeRestriction += new RecordRestriction(attribute.FilterExpression, claimValues); - } - - return completeRestriction; + return s_claimFunctions.Select(func => func(principal)).Aggregate((a,b) => a+b); } /// @@ -1152,12 +1142,33 @@ public Task UpdateRecordWhereAsync(ClaimsPrincipal principal, DataRow row, #region [ Static ] - private static readonly ClaimQueryRestrictionAttribute[]? s_claimQueryRestrictionAttributes; + private static readonly IEnumerable> s_claimFunctions; static SecureTableOperations() { - if (typeof(T).TryGetAttributes(out ClaimQueryRestrictionAttribute[]? claimAttributes)) - s_claimQueryRestrictionAttributes = claimAttributes; + s_claimFunctions = typeof(T) + .GetMethods(BindingFlags.Public | BindingFlags.Static) + .Select(method => new { Method = method, Attribute = method.GetCustomAttribute() }) + .Where(obj => obj.Attribute is not null) + .Select(obj => { + ParameterInfo[] parameters = obj.Method.GetParameters(); + if (obj.Method.ReturnType != typeof(RecordRestriction) || parameters.Length != 1 || parameters[0].ParameterType != typeof(object[])) + throw new InvalidOperationException( + $"Method \"{obj.Method.Name}\" marked with \"{typeof(ClaimRestrictionAttribute).Name}\" in model \"{typeof(T).Name}\" has an invalid signature. " + + $"Expected: public static {typeof(RecordRestriction).Name} Method(params {typeof(object[])} input)"); + + Func createRestriction = (principal) => + { + object[] claimValues = obj.Attribute.Claims + .Select(claimKey => principal.FindAll(claimKey)) + .SelectMany(claims => claims.Select(claim => (object) claim.Value)) + .ToArray(); + + return (RecordRestriction)obj.Method.Invoke(null, [claimValues]); + }; + + return createRestriction; + }); } #endregion From c8e22473736fc254ccc7e4e94ea5630ad4dd6a57 Mon Sep 17 00:00:00 2001 From: Gabriel Santos Date: Mon, 19 Jan 2026 10:34:04 -0500 Subject: [PATCH 6/8] added delete record overlord --- .../Model/SecureTableOperations.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/Gemstone.Data/Model/SecureTableOperations.cs b/src/Gemstone.Data/Model/SecureTableOperations.cs index d5010ba6e..1f1c4937a 100644 --- a/src/Gemstone.Data/Model/SecureTableOperations.cs +++ b/src/Gemstone.Data/Model/SecureTableOperations.cs @@ -793,6 +793,43 @@ public int DeleteRecord(ClaimsPrincipal principal, RecordRestriction? restrictio public Task DeleteRecordAsync(ClaimsPrincipal principal, RecordRestriction? restriction, CancellationToken cancellationToken, bool? applyRootQueryRestriction = null) => BaseOperations.DeleteRecordAsync(restriction + GetClaimRecordRestriction(principal), cancellationToken, applyRootQueryRestriction); + /// + /// Deletes the specified modeled table from the database. + /// + /// Claims principal which is making the request. + /// Record to delete. + /// Number of rows affected. + public int DeleteRecord(ClaimsPrincipal principal, T record) + { + IEnumerable whereElements = BaseOperations + .GetPrimaryKeyFieldNames(true) + .Select((primaryKeyField, index) => $"{primaryKeyField} = {index}"); + RecordRestriction recordRestrict = new RecordRestriction( + string.Join(" AND ", whereElements), + BaseOperations.GetPrimaryKeys(record) + ); + return DeleteRecord(principal, recordRestrict); + } + + /// + /// Deletes the specified modeled table from the database. + /// + /// Claims principal which is making the request. + /// Record to delete. + /// Propagates notification that operations should be canceled. + /// Number of rows affected. + public Task DeleteRecordAsync(ClaimsPrincipal principal, T record, CancellationToken cancellationToken) + { + IEnumerable whereElements = BaseOperations + .GetPrimaryKeyFieldNames(true) + .Select((primaryKeyField, index) => $"{primaryKeyField} = {index}"); + RecordRestriction recordRestrict = new RecordRestriction( + string.Join(" AND ", whereElements), + BaseOperations.GetPrimaryKeys(record) + ); + return DeleteRecordAsync(principal, recordRestrict, cancellationToken); + } + /// /// Deletes the records referenced by the specified SQL filter expression and parameters. /// From 80c2d2544b8d4a0cd0c3b5844dfe224ac541efb5 Mon Sep 17 00:00:00 2001 From: Gabriel Santos Date: Mon, 19 Jan 2026 10:34:38 -0500 Subject: [PATCH 7/8] added newrecord indirection --- src/Gemstone.Data/Model/SecureTableOperations.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Gemstone.Data/Model/SecureTableOperations.cs b/src/Gemstone.Data/Model/SecureTableOperations.cs index 1f1c4937a..3aea37df4 100644 --- a/src/Gemstone.Data/Model/SecureTableOperations.cs +++ b/src/Gemstone.Data/Model/SecureTableOperations.cs @@ -65,6 +65,14 @@ public SecureTableOperations(AdoDataConnection connection) #region [ Methods ] + /// + /// Creates a new modeled record instance, applying any modeled default values as specified by a + /// or on the + /// model properties. + /// + /// New modeled record instance with any defined default values applied. + public T? NewRecord() => BaseOperations.NewRecord(); + /// /// Transforms a into an equivalent , as defined by the model's . /// From b8367e408443e0f35e87127bb01822c04af30eef Mon Sep 17 00:00:00 2001 From: Gabriel Santos Date: Mon, 19 Jan 2026 10:34:52 -0500 Subject: [PATCH 8/8] added property indirections --- .../Model/SecureTableOperations.cs | 146 +++++++++++++++++- 1 file changed, 142 insertions(+), 4 deletions(-) diff --git a/src/Gemstone.Data/Model/SecureTableOperations.cs b/src/Gemstone.Data/Model/SecureTableOperations.cs index 3aea37df4..14ae9b7d0 100644 --- a/src/Gemstone.Data/Model/SecureTableOperations.cs +++ b/src/Gemstone.Data/Model/SecureTableOperations.cs @@ -23,6 +23,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Data; using System.Linq; using System.Reflection; @@ -40,10 +41,6 @@ namespace Gemstone.Data.Model; /// Modeled table. public class SecureTableOperations where T : class, new() { - /// - /// which performs DB operations. - /// - public TableOperations BaseOperations { get; } /// /// Creates a new @@ -63,6 +60,147 @@ public SecureTableOperations(AdoDataConnection connection) BaseOperations = new(connection); } + #region [ Properties ] + + /// + /// which performs DB operations. + /// + public TableOperations BaseOperations { get; } + + /// + /// Gets instance associated with this used for database operations. + /// + public AdoDataConnection Connection => BaseOperations.Connection; + + /// + /// Gets the table name defined for the modeled table, includes any escaping as defined in model. + /// + public string TableName => BaseOperations.TableName; + + /// + /// Gets the table name defined for the modeled table without any escape characters. + /// + /// + /// A table name will only be escaped if the model requested escaping with the . + /// + public string UnescapedTableName => BaseOperations.UnescapedTableName; + + /// + /// Gets the wildcard character used for pattern matching within queries. + /// + public string WildcardChar => BaseOperations.WildcardChar; + + /// + /// Gets flag that determines if modeled table has a primary key that is an identity field. + /// + public bool HasPrimaryKeyIdentityField => BaseOperations.HasPrimaryKeyIdentityField; + + /// + /// Gets or sets delegate used to handle table operation exceptions. + /// + /// + /// When exception handler is provided, table operations will not throw exceptions for database calls, any + /// encountered exceptions will be passed to handler for processing. Otherwise, exceptions will be thrown + /// on the call stack. + /// + public Action? ExceptionHandler => BaseOperations.ExceptionHandler; + + /// + /// Gets or sets flag that determines if field names should be treated as case-sensitive. Defaults to false. + /// + /// + /// In cases where modeled table fields have applied , this flag will be used + /// to properly update escaped field names that may be case-sensitive. For example, escaped field names in Oracle + /// are case-sensitive. This value is typically false. + /// + public bool UseCaseSensitiveFieldNames => BaseOperations.UseCaseSensitiveFieldNames; + + /// + /// Gets or sets primary key cache. + /// + /// + /// + /// The overloads that include paging parameters + /// cache the sorted and filtered primary keys of queried records between calls so that paging is fast and + /// efficient. Since the primary keys are cached, an instance of the should + /// exist per user session when using query functions that support pagination. In web based implementations, + /// the primary cache should be stored with user session state data and then restored between instances of + /// the that are created along with a connection that is opened per page. + /// + /// + /// The function should be called to manually clear cache when table + /// contents are known to have changed. Note that calls to any overload + /// will automatically clear any existing primary key cache. + /// + /// + /// Primary keys values are stored in data table without interpretation, i.e., in their raw form as queried + /// from the database. Primary key data in cache will be encrypted for models with primary key fields that + /// are marked with the + /// + /// + public DataTable? PrimaryKeyCache => BaseOperations.PrimaryKeyCache; + + /// + /// Gets or sets root record restriction that applies to query table operations. + /// + /// + /// + /// Defining a root query restriction creates a base query filter that gets applied to all query operations, + /// even when another restriction is applied - in this case the root restriction will be pre-pended to the + /// specified query, e.g.: + /// + /// restriction = RootQueryRestriction + restriction; + /// + /// A root query restriction is useful to apply a common state to the query operations, e.g., always + /// filtering records for a specific user or context. + /// + /// + /// A root query restriction can be manually assigned to a instance or + /// automatically assigned by marking a model with the . + /// + /// + /// If any of the reference a table field that is modeled with + /// either an or , then the function + /// will need to be called, replacing the target parameter with the + /// returned value so that the field value will be properly set prior to executing the database function. + /// + /// + public RecordRestriction? RootQueryRestriction => BaseOperations.RootQueryRestriction; + + /// + /// Gets or sets flag that determines if should be applied to update operations. + /// + /// + /// + /// If only references primary key fields, then this property value should be set + /// to false since default update operations for a modeled record already work against primary key fields. + /// + /// + /// This flag can be manually set per instance or handled automatically by marking + /// a model with the and assigning a value to the attribute property + /// . + /// + /// + public bool ApplyRootQueryRestrictionToUpdates => BaseOperations.ApplyRootQueryRestrictionToUpdates; + + /// + /// Gets or sets flag that determines if should be applied to delete operations. + /// + /// + /// + /// If only references primary key fields, then this property value should be set + /// to false since default delete operations for a modeled record already work against primary key fields. + /// + /// + /// This flag can be manually set per instance or handled automatically by marking + /// a model with the and assigning a value to the attribute property + /// . + /// + /// + public bool ApplyRootQueryRestrictionToDeletes => BaseOperations.ApplyRootQueryRestrictionToDeletes; + + #endregion + #region [ Methods ] ///