aboutsummaryrefslogtreecommitdiff
path: root/docker/crupest-api/CrupestApi/CrupestApi.Secrets/SecretsService.cs
blob: 5a491211acc4add23449f1b57c943838d9634581 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
using System.Diagnostics;
using CrupestApi.Commons;
using CrupestApi.Commons.Crud;
using Dapper;

namespace CrupestApi.Secrets;

public class SecretsService : CrudService<SecretInfo>, ISecretsService
{
    private readonly ILogger<SecretsService> _logger;

    public SecretsService(ITableInfoFactory tableInfoFactory, IDbConnectionFactory dbConnectionFactory, ILoggerFactory loggerFactory)
    : base("secrets", tableInfoFactory, dbConnectionFactory, loggerFactory)
    {
        _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();
    }
}