diff options
author | crupest <crupest@outlook.com> | 2022-12-10 16:56:33 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2022-12-20 20:32:53 +0800 |
commit | f208e75f9e074a3faab489de1093f660b87c2ec7 (patch) | |
tree | 2c2379c5155b7cef31c56c6559418ce1d66128a5 /docker/crupest-api/CrupestApi | |
parent | 8c478bb1a07a55b9056ada029e0d6f9fe34d8d38 (diff) | |
download | crupest-f208e75f9e074a3faab489de1093f660b87c2ec7.tar.gz crupest-f208e75f9e074a3faab489de1093f660b87c2ec7.tar.bz2 crupest-f208e75f9e074a3faab489de1093f660b87c2ec7.zip |
Develop secret api. v24
Diffstat (limited to 'docker/crupest-api/CrupestApi')
10 files changed, 163 insertions, 261 deletions
diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TableInfoTest.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TableInfoTest.cs index c6791c6..62fb9e5 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TableInfoTest.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons.Tests/Crud/TableInfoTest.cs @@ -24,7 +24,7 @@ public class TableInfoTest [Fact] public void GenerateSelectSqlTest() { - var (sql, parameters) = _tableInfo.GenerateSelectSql(WhereClause.Create().Eq("Name", "Hello")); + var (sql, parameters) = _tableInfo.GenerateSelectSql(null, WhereClause.Create().Eq("Name", "Hello")); var parameterName = parameters.ParameterNames.First(); // TODO: Is there a way to auto detect parameters? diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs index 88b5ced..f1fb99b 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs @@ -86,16 +86,39 @@ public class ColumnInfo protected void OnBeforeInsert(ColumnInfo column, ref object? value) { - TryCoerceStringFromNullToEmpty(ref value); - if (column.IsNotNull && !column.IsAutoIncrement) + if (column.IsClientGenerate && value is not null) + { + throw new Exception($"Column {column.ColumnName} can't be set manually."); + } + + var defaultValueGeneratorMethod = DefaultValueGeneratorMethod; + if (defaultValueGeneratorMethod is not null) { - throw new Exception($"Column {column.ColumnName} can't be empty."); + value = defaultValueGeneratorMethod.Invoke(null, new object[] { }); } + + + OnBeforeSet(column, ref value); } protected void OnBeforeUpdate(ColumnInfo column, ref object? value) { + OnBeforeSet(column, ref value); + + if (column.IsNoUpdate) + { + throw new Exception($"Column {column.ColumnName} not updatable."); + } + } + + protected void OnBeforeSet(ColumnInfo column, ref object? value) + { TryCoerceStringFromNullToEmpty(ref value); + + if (value is null && column.IsNotNull) + { + throw new Exception($"Column {column.ColumnName} can't be null."); + } } public string ColumnName @@ -108,9 +131,33 @@ public class ColumnInfo } } + public MethodInfo? DefaultValueGeneratorMethod + { + get + { + object? value = Metadata.GetValueOrDefault(ColumnMetadataKeys.DefaultValueGenerator); + Debug.Assert(value is null || value is string); + MethodInfo? result; + if (value is null) + { + string methodName = ColumnName + "DefaultValueGenerator"; + result = Table.EntityType.GetMethod(methodName, BindingFlags.Static); + } + else + { + string methodName = (string)value; + result = Table.EntityType.GetMethod(methodName, BindingFlags.Static) ?? throw new Exception("The default value generator does not exist."); + } + + return result; + } + } + public bool IsPrimaryKey => Metadata.GetValueOrDefault(ColumnMetadataKeys.IsPrimaryKey) is true; public bool IsAutoIncrement => Metadata.GetValueOrDefault(ColumnMetadataKeys.IsAutoIncrement) is true; public bool IsNotNull => IsPrimaryKey || Metadata.GetValueOrDefault(ColumnMetadataKeys.NotNull) is true; + public bool IsClientGenerate => Metadata.GetValueOrDefault(ColumnMetadataKeys.ClientGenerate) is true; + public bool IsNoUpdate => Metadata.GetValueOrDefault(ColumnMetadataKeys.NoUpdate) is true; public ColumnIndexType Index => Metadata.GetValueOrDefault<ColumnIndexType?>(ColumnMetadataKeys.Index) ?? ColumnIndexType.None; diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs index 91e49f8..1ca2ce8 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnMetadata.cs @@ -17,6 +17,18 @@ public static class ColumnMetadataKeys /// This indicates that you take care of generate this column value when create entity. User calling the api can not specify the value. /// </summary> public const string ClientGenerate = nameof(ColumnAttribute.DefaultEmptyForString); + + /// <summary> + /// The default value generator method name in entity type. Default to null, aka, search for ColumnNameDefaultValueGenerator. + /// </summary> + /// <returns></returns> + public const string DefaultValueGenerator = nameof(ColumnAttribute.DefaultValueGenerator); + + /// <summary> + /// The column can only be set when inserted, can't be changed in update. + /// </summary> + /// <returns></returns> + public const string NoUpdate = nameof(ColumnAttribute.NoUpdate); } public interface IColumnMetadata @@ -87,6 +99,12 @@ public class ColumnAttribute : Attribute, IColumnMetadata /// <see cref="ColumnMetadataKeys.ClientGenerate"/> public bool ClientGenerate { get; init; } + /// <see cref="ColumnMetadataKeys.DefaultValueGenerator"/> + public string? DefaultValueGenerator { get; init; } + + /// <see cref="ColumnMetadataKeys.NoUpdate"/> + public bool NoUpdate { get; init; } + public bool TryGetValue(string key, out object? value) { var property = GetType().GetProperty(key); diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs index e098aca..811b2e6 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs @@ -40,6 +40,16 @@ public class CrudService<TEntity> : IDisposable where TEntity : class return _table.Select(_dbConnection, filter).Cast<TEntity>().ToList(); } + public bool Exists(IWhereClause? filter) + { + return _table.SelectCount(_dbConnection, filter) > 0; + } + + public int Count(IWhereClause? filter) + { + return _table.SelectCount(_dbConnection, filter); + } + public int Insert(IInsertClause insertClause) { return _table.Insert(_dbConnection, insertClause); diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs index aa25a23..3467625 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudWebApplicationExtensions.cs @@ -2,8 +2,13 @@ namespace CrupestApi.Commons.Crud; public static class CrudWebApplicationExtensions { - public static IApplicationBuilder UseCrud(this IApplicationBuilder app, string path) + public static WebApplication UseCrud(this WebApplication app, string path) { + app.MapGet(path, async (context) => + { + + }); + return app; } } diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs index 2ee01ca..e8f8abf 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/DbConnectionFactory.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Options; namespace CrupestApi.Commons.Crud; -// TODO: Implement and register this service. public interface IDbConnectionFactory { IDbConnection Get(string? name = null); diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs index 28dc1ad..5bb19ad 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/TableInfo.cs @@ -207,7 +207,7 @@ CREATE TABLE {tableName}( } } - public (string sql, DynamicParameters parameters) GenerateSelectSql(IWhereClause? whereClause, IOrderByClause? orderByClause = null, int? skip = null, int? limit = null, string? dbProviderId = null) + public (string sql, DynamicParameters parameters) GenerateSelectSql(string? what, IWhereClause? whereClause, IOrderByClause? orderByClause = null, int? skip = null, int? limit = null, string? dbProviderId = null) { CheckRelatedColumns(whereClause); CheckRelatedColumns(orderByClause); @@ -215,7 +215,7 @@ CREATE TABLE {tableName}( var parameters = new DynamicParameters(); StringBuilder result = new StringBuilder() - .Append("SELECT * FROM ") + .Append($"SELECT {what ?? "*"} FROM ") .Append(TableName); if (whereClause is not null) @@ -372,49 +372,43 @@ CREATE TABLE {tableName}( return result; } - private object? ConvertFromDynamicToEntity(dynamic d) + public virtual int SelectCount(IDbConnection dbConnection, IWhereClause? where = null, IOrderByClause? orderBy = null, int? skip = null, int? limit = null) { - if (d is null) return null; + var (sql, parameters) = GenerateSelectSql("COUNT(*)", where, orderBy, skip, limit); + return dbConnection.QuerySingle<int>(sql, parameters); - Type dynamicType = d.GetType(); - - var result = Activator.CreateInstance(EntityType); - - foreach (var column in ColumnInfos) - { - var propertyInfo = column.PropertyInfo; - if (propertyInfo is not null) - { - var dynamicProperty = dynamicType.GetProperty(column.ColumnName); - if (dynamicProperty is null) continue; - object? value = dynamicProperty.GetValue(d); - value = column.ColumnType.ConvertFromDatabase(value); - propertyInfo.SetValue(result, value); - } - } - - return result; } public virtual IEnumerable<object?> Select(IDbConnection dbConnection, IWhereClause? where = null, IOrderByClause? orderBy = null, int? skip = null, int? limit = null) { - var (sql, parameters) = GenerateSelectSql(where, orderBy, skip, limit); + return Select<IEnumerable<object?>>(dbConnection, null, where, orderBy, skip, limit); + } + + public virtual IEnumerable<TResult> Select<TResult>(IDbConnection dbConnection, string? what, IWhereClause? where = null, IOrderByClause? orderBy = null, int? skip = null, int? limit = null) + { + var (sql, parameters) = GenerateSelectSql(what, where, orderBy, skip, limit); return dbConnection.Query<dynamic>(sql, parameters).Select(d => { - var e = ConvertFromDynamicToEntity(d); + Type dynamicType = d.GetType(); + + var result = Activator.CreateInstance<TResult>(); foreach (var column in ColumnInfos) { + object? value = null; + var dynamicProperty = dynamicType.GetProperty(column.ColumnName); + if (dynamicProperty is not null) value = dynamicProperty.GetValue(d); + column.Hooks.AfterSelect(column, ref value); + if (value is not null) + value = column.ColumnType.ConvertFromDatabase(value); var propertyInfo = column.PropertyInfo; if (propertyInfo is not null) { - var value = propertyInfo.GetValue(e); - column.Hooks.AfterSelect(column, ref value); - propertyInfo.SetValue(e, value); + propertyInfo.SetValue(result, value); } } - return e; + return result; }); } @@ -422,12 +416,20 @@ CREATE TABLE {tableName}( { var (sql, parameters) = GenerateInsertSql(insert); - foreach (var item in insert.Items) + foreach (var column in ColumnInfos) { - var column = GetColumn(item.ColumnName); - var value = item.Value; + InsertItem? item = insert.Items.FirstOrDefault(i => i.ColumnName == column.ColumnName); + var value = item?.Value; column.Hooks.BeforeInsert(column, ref value); - item.Value = value; + if (item is null) + { + if (value is not null) + insert.Items.Add(new InsertItem(column.ColumnName, value)); + } + else + { + item.Value = value; + } } return dbConnection.Execute(sql, ConvertParameters(parameters)); @@ -437,13 +439,22 @@ CREATE TABLE {tableName}( { var (sql, parameters) = GenerateUpdateSql(where, update); - foreach (var item in update.Items) + foreach (var column in ColumnInfos) { - var column = GetColumn(item.ColumnName); - var value = item.Value; + UpdateItem? item = update.Items.FirstOrDefault(i => i.ColumnName == column.ColumnName); + var value = item?.Value; column.Hooks.BeforeUpdate(column, ref value); - item.Value = value; + if (item is null) + { + if (value is not null) + update.Items.Add(new UpdateItem(column.ColumnName, value)); + } + else + { + item.Value = value; + } } + return dbConnection.Execute(sql, ConvertParameters(parameters)); } diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Secrets/ISecretsService.cs b/docker/crupest-api/CrupestApi/CrupestApi.Secrets/ISecretsService.cs deleted file mode 100644 index b5de436..0000000 --- a/docker/crupest-api/CrupestApi/CrupestApi.Secrets/ISecretsService.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace CrupestApi.Secrets; - -public interface ISecretsService -{ - Task<SecretInfo?> GetSecretAsync(string secret); - - Task<List<SecretInfo>> GetSecretListAsync(bool includeExpired = false, bool includeRevoked = false); - - Task<List<SecretInfo>> GetSecretListByKeyAsync(string key, bool includeExpired = false, bool includeRevoked = false); - - Task VerifySecretAsync(string? key, string? secret); - - // Check if "secret" query param exists and is only one. Then check the secret is valid for given key. - // If check fails, will throw a VerifySecretException with proper message that can be send to client. - Task VerifySecretForHttpRequestAsync(HttpRequest request, string? key = null, string queryKey = "secret"); - - Task<SecretInfo> CreateSecretAsync(string key, string description, DateTime? expireTime = null); - - Task RevokeSecretAsync(string secret); - - // Throw SecretNotExistException if request secret does not exist. - Task<SecretInfo> ModifySecretAsync(string secret, SecretModifyRequest modifyRequest); -} diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretInfo.cs index 009bde9..e6af39b 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretInfo.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretInfo.cs @@ -1,3 +1,5 @@ +using System.Security.Cryptography; +using System.Text; using CrupestApi.Commons.Crud; namespace CrupestApi.Secrets; @@ -6,7 +8,7 @@ public class SecretInfo { [Column(NotNull = true)] public string Key { get; set; } = default!; - [Column(NotNull = true, ClientGenerate = true)] + [Column(NotNull = true, ClientGenerate = true, NoUpdate = true)] public string Secret { get; set; } = default!; [Column(DefaultEmptyForString = true)] public string Description { get; set; } = default!; @@ -16,4 +18,31 @@ public class SecretInfo public bool Revoked { get; set; } [Column(NotNull = true)] public DateTime CreateTime { get; set; } + + private static RandomNumberGenerator RandomNumberGenerator = RandomNumberGenerator.Create(); + + private static string GenerateRandomKey(int length) + { + const string alphanum = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var result = new StringBuilder(length); + lock (RandomNumberGenerator) + { + for (int i = 0; i < length; i++) + { + result.Append(alphanum[RandomNumberGenerator.GetInt32(alphanum.Length)]); + } + } + return result.ToString(); + } + + + public static string SecretDefaultValueGenerator() + { + return GenerateRandomKey(16); + } + + public static DateTime CreateTimeDefaultValueGenerator() + { + return DateTime.UtcNow; + } } diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretsService.cs b/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretsService.cs index 5a49121..b8912cb 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretsService.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretsService.cs @@ -5,7 +5,7 @@ using Dapper; namespace CrupestApi.Secrets; -public class SecretsService : CrudService<SecretInfo>, ISecretsService +public class SecretsService : CrudService<SecretInfo> { private readonly ILogger<SecretsService> _logger; @@ -14,198 +14,4 @@ public class SecretsService : CrudService<SecretInfo>, ISecretsService { _logger = loggerFactory.CreateLogger<SecretsService>(); } - - public async Task<SecretInfo> CreateSecretAsync(SecretInfo secretInfo) - { - if (secretInfo.Secret is not null) - { - throw new ArgumentException("Secret is auto generated. Don't specify it explicit."); - } - - secretInfo.Secret = GenerateRandomKey(16); - secretInfo.CreateTime = DateTime.Now; - - await InsertAsync(_table.GenerateInsertClauseFromEntity(secretInfo)); - - return secretInfo; - } - - public async Task<List<SecretInfo>> GetSecretListAsync(bool includeExpired = false, bool includeRevoked = false) - { - return (await QueryAsync()).ToList(); - } - - public async Task<List<SecretInfo>> GetSecretListByKeyAsync(string key, bool includeExpired = false, bool includeRevoked = false) - { - WhereClause where = WhereClause.Create(); - - where.Eq(nameof(SecretInfo.Key), key); - - if (!includeExpired) - { - where.Add(nameof(SecretInfo.ExpireTime), "<=", ) - } - - if (!includeRevoked) - { - where.Eq(nameof(SecretInfo.Revoked), false); - } - - return (await QueryAsync(where)).ToList(); - } - - public async Task<SecretInfo> ModifySecretAsync(string secret, SecretModifyRequest modifyRequest) - { - var dbConnection = await EnsureDatabase(); - - var secretInfo = await GetSecretAsync(dbConnection, secret); - - if (secretInfo is null) - { - throw new EntityNotExistException("Secret not found."); - } - - var queryParams = new DynamicParameters(); - var updateColumnList = new List<string>(); - - if (modifyRequest.Key is not null) - { - queryParams.Add("Key", modifyRequest.Key); - updateColumnList.Add("Key"); - } - - if (modifyRequest.Description is not null) - { - queryParams.Add("Description", modifyRequest.Description); - updateColumnList.Add("Description"); - } - - if (modifyRequest.SetExpireTime is true) - { - queryParams.Add("ExpireTime", modifyRequest.ExpireTime?.ToString("O")); - updateColumnList.Add("ExpireTime"); - } - - if (modifyRequest.Revoked is true && secretInfo.Revoked is not true) - { - queryParams.Add("Revoked", true); - updateColumnList.Add("Revoked"); - } - - if (updateColumnList.Count == 0) - { - return secretInfo; - } - - queryParams.Add("Secret", secret); - - var updateColumnString = updateColumnList.GenerateUpdateColumnString(); - - var changeCount = await dbConnection.ExecuteAsync($@" -UPDATE secrets SET {updateColumnString} WHERE Secret = @Secret; - ", queryParams); - - Debug.Assert(changeCount == 1); - - return secretInfo; - } - - public async Task RevokeSecretAsync(string secret) - { - await ModifySecretAsync(secret, new SecretModifyRequest - { - Revoked = true, - }); - } - - public async Task VerifySecretAsync(string? key, string? secret) - { - var dbConnection = await EnsureDatabase(); - - if (secret is null) - { - if (key is not null) - { - throw new VerifySecretException(key, "A secret with given key is needed."); - } - } - - var entity = await dbConnection.QueryFirstOrDefaultAsync<SecretInfo>(@" -SELECT Id, Key, Secret, Description, ExpireTime, Revoked, CreateTime FROM secrets WHERE Key = @Key AND Secret = @Secret - ", new - { - Key = key, - Secret = secret, - }); - - if (entity is null) - { - throw new VerifySecretException(key, "Secret token is invalid."); - } - - if (entity.Revoked is true) - { - throw new VerifySecretException(key, "Secret token is revoked."); - } - - if (entity.ExpireTime is not null && DateTime.ParseExact(entity.ExpireTime, "O", null) > DateTime.Now) - { - throw new VerifySecretException(key, "Secret token is expired."); - } - - if (key is not null) - { - if (entity.Key != key) - { - throw new VerifySecretException(key, "Secret is not for this key", VerifySecretException.ErrorKind.Forbidden); - } - } - } - - public async Task VerifySecretForHttpRequestAsync(HttpRequest request, string? key, string queryKey = "secret") - { - string? secret = null; - - var authorizationHeaders = request.Headers.Authorization.ToList(); - if (authorizationHeaders.Count > 1) - { - _logger.LogWarning("There are multiple Authorization headers in the request. Will use the last one."); - } - if (authorizationHeaders.Count > 0) - { - var authorizationHeader = authorizationHeaders[^1] ?? ""; - if (!authorizationHeader.StartsWith("Bearer ")) - { - throw new VerifySecretException(key, "Authorization header must start with 'Bearer '."); - } - - secret = authorizationHeader.Substring("Bearer ".Length).Trim(); - } - - var secretQueryParam = request.Query[queryKey].ToList(); - if (secretQueryParam.Count > 1) - { - _logger.LogWarning($"There are multiple '{queryKey}' query parameters in the request. Will use the last one."); - } - if (secretQueryParam.Count > 0) - { - if (secret is not null) - { - _logger.LogWarning("Secret found both in Authorization header and query parameter. Will use the one in query parameter."); - } - secret = secretQueryParam[^1] ?? ""; - } - - await VerifySecretAsync(key, secret); - } - - public Task<SecretInfo?> GetSecretAsync(string secret) - { - throw new NotImplementedException(); - } - - public Task<SecretInfo> CreateSecretAsync(string key, string description, DateTime? expireTime = null) - { - throw new NotImplementedException(); - } } |