• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

b1f6c1c4 / ProfessionalAccounting / 328

28 Jun 2025 10:48PM UTC coverage: 48.177% (-3.2%) from 51.346%
328

push

appveyor

b1f6c1c4
fix bugs

5035 of 10451 relevant lines covered (48.18%)

107.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

2.82
/AccountingServer.Shell/AuthnShell.cs
1
/* Copyright (C) 2025 b1f6c1c4
2
 *
3
 * This file is part of ProfessionalAccounting.
4
 *
5
 * ProfessionalAccounting is free software: you can redistribute it and/or
6
 * modify it under the terms of the GNU Affero General Public License as
7
 * published by the Free Software Foundation, version 3.
8
 *
9
 * ProfessionalAccounting is distributed in the hope that it will be useful, but
10
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License
12
 * for more details.
13
 *
14
 * You should have received a copy of the GNU Affero General Public License
15
 * along with ProfessionalAccounting.  If not, see
16
 * <https://www.gnu.org/licenses/>.
17
 */
18

19
using System;
20
using System.Collections.Generic;
21
using System.Linq;
22
using System.Threading.Tasks;
23
using AccountingServer.BLL;
24
using AccountingServer.BLL.Util;
25
using AccountingServer.Entities;
26
using AccountingServer.Shell.Util;
27
using static AccountingServer.BLL.Parsing.Synthesizer;
28

29
namespace AccountingServer.Shell;
30

31
internal class AuthnShell : IShellComponent
32
{
33
    private readonly DbSession m_Db;
34

35
    public readonly AuthnManager Mgr;
36

37
    public AuthnShell(DbSession db)
10✔
38
    {
10✔
39
        m_Db = db;
10✔
40
        Mgr = new(m_Db);
10✔
41
    }
10✔
42

43
    /// <inheritdoc />
44
    public IAsyncEnumerable<string> Execute(string expr, Context ctx, string term)
45
    {
×
46
        expr = expr.Rest();
×
47
        return expr.Initial() switch
×
48
            {
49
                "" => ListIdentity(ctx, false),
×
50
                "invite" => Invite(ctx, expr.Rest().Trim()),
×
51
                "ids" => ListIdentites(ctx),
×
52
                "list" => ListAuthn(ctx, expr.Rest().Trim()),
×
53
                "cert" => SaveCert(ctx, expr.Rest().Trim()),
×
54
                "rm" => RemoveAuthn(ctx, expr.Rest().Trim()),
×
55
                _ => throw new InvalidOperationException("表达式无效"),
×
56
            };
57
    }
×
58

59
    /// <inheritdoc />
60
    public bool IsExecutable(string expr) => expr.Initial() == "an";
6✔
61

62
    public static async IAsyncEnumerable<string> ListIdentity(Context ctx, bool brief)
63
    {
×
64
        yield return $"Authenticated Identity: {ctx.TrueIdentity.Name.AsId()}\n";
×
65
        if (ctx.Identity != ctx.TrueIdentity)
×
66
            yield return $"Assume Identity: {ctx.Identity.Name.AsId()}\n";
×
67

68
        if (brief && !ctx.Identity.CanLogin(ctx.Client.User))
×
69
            throw new AccessDeniedException($"Selected entity {ctx.Client.User.AsUser()} denied");
×
70

71
        yield return $"Selected Entity: {ctx.Client.User.AsUser()}\n";
×
72

73
        if (brief)
×
74
            yield break;
×
75

76
        yield return "\n---- Date & Time ----\n";
×
77
        yield return $"Client Date: {ctx.Client.Today.AsDate()}\n";
×
78
        yield return $"Server Date: {DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.sssZ}\n";
×
79

80
        yield return "\n---- Authn details ----\n";
×
81
        if (ctx.Session != null)
×
82
        {
×
83
            yield return "WebAuthn accepted\n";
×
84
            yield return $"\tAuthn ID: {ctx.Session.Authn.StringID}\n";
×
85
            yield return $"\tInvitedAt: {ctx.Session.Authn.InvitedAt:yyyy-MM-ddTHH:mm:ss.sssZ}\n";
×
86
            yield return $"\tCreatedAt: {ctx.Session.Authn.CreatedAt:yyyy-MM-ddTHH:mm:ss.sssZ}\n";
×
87
            yield return $"\tLastUsedAt: {ctx.Session.Authn.LastUsedAt:yyyy-MM-ddTHH:mm:ss.sssZ}\n";
×
88
            yield return $"\tSession CreatedAt: {ctx.Session.CreatedAt:yyyy-MM-ddTHH:mm:ss.sssZ}\n";
×
89
            yield return $"\tSession ExpiresAt: {ctx.Session.ExpiresAt:yyyy-MM-ddTHH:mm:ss.sssZ}\n";
×
90
            yield return $"\tSession MaxExpiresAt: {ctx.Session.MaxExpiresAt:yyyy-MM-ddTHH:mm:ss.sssZ}\n";
×
91
        }
×
92
        else
93
            yield return "No WebAuthn credential present\n";
×
94

95
        if (ctx.Certificate != null)
×
96
        {
×
97
            if (ctx.Certificate.ID != null)
×
98
            {
×
99
                yield return "Client certificate accepted\n";
×
100
                yield return $"\tAuthn ID: {ctx.Certificate.StringID}\n";
×
101
                yield return $"\tCreatedAt: {ctx.Certificate.CreatedAt:s}\n";
×
102
                yield return $"\tLastUsedAt: {ctx.Certificate.LastUsedAt:s}\n";
×
103
            }
×
104
            else
105
                yield return "Client certificate acknowledged but not accepted\n";
×
106

107
            yield return $"\tFingerprint: {ctx.Certificate.Fingerprint}\n";
×
108
            yield return $"\tSubjectDN: {ctx.Certificate.SubjectDN}\n";
×
109
            yield return $"\tIssuerDN: {ctx.Certificate.IssuerDN}\n";
×
110
            yield return $"\tSerial: {ctx.Certificate.Serial}\n";
×
111
            yield return $"\tValid not Before: {ctx.Certificate.Start}\n";
×
112
            yield return $"\tValid not After: {ctx.Certificate.End}\n";
×
113
        }
×
114
        else
115
            yield return "No client certificate present\n";
×
116

117
        yield return "\n---- RBAC details ----\n";
×
118
        if (ctx.TrueIdentity.P.AllAssumes == null)
×
119
            yield return "Assumable Identities: *\n";
×
120
        else
121
        {
×
122
            var assumes = ctx.TrueIdentity.P.AllAssumes.Select(static (s) => s.AsId());
×
123
            yield return $"Assumable Identities: {string.Join(", ", assumes)}\n";
×
124
        }
×
125

126
        if (ctx.Identity.P.AllKnowns == null)
×
127
            yield return "Known Identities: *\n";
×
128
        else
129
        {
×
130
            var knowns = ctx.Identity.P.AllKnowns.Select(static (s) => s.AsId());
×
131
            yield return $"Known Identities: {string.Join(", ", knowns)}\n";
×
132
        }
×
133

134
        if (ctx.Identity.P.AllRoles == null)
×
135
            yield return "Associated Roles: *\n";
×
136
        else
137
        {
×
138
            var roles = ctx.Identity.P.AllRoles.Select(static (r) => r.Name.AsId());
×
139
            yield return $"Associated Roles: {string.Join(", ", roles)}\n";
×
140
        }
×
141

142
        if (ctx.Identity.P.AllUsers == null)
×
143
            yield return "Associated Entities: *\n";
×
144
        else
145
        {
×
146
            var users = ctx.Identity.P.AllUsers.Select(static (s) => s.AsUser());
×
147
            yield return $"Associated Entities: {string.Join(", ", users)}\n";
×
148
        }
×
149

150
        yield return "Allowable Permissions:\n";
×
151
        yield return $"\tInvoke: {ctx.Identity.P.Invoke.Grant}\n";
×
152
        yield return $"\tView: {Synth(ctx.Identity.P.View.Grant)}\n";
×
153
        yield return $"\tEdit: {Synth(ctx.Identity.P.Edit.Grant)}\n";
×
154
        yield return $"\tVoucher: {Synth(ctx.Identity.P.Voucher.Grant)}\n";
×
155
        yield return $"\tAsset: {Synth(ctx.Identity.P.Asset.Grant)}\n";
×
156
        yield return $"\tAmort: {Synth(ctx.Identity.P.Amort.Grant)}\n";
×
157

158
        yield return $"Debit/Credit Imbalance: {(ctx.Identity.P.Imba ? "Granted" : "Denied")}\n";
×
159
        yield return $"Reflect on Denies: {(ctx.Identity.P.Reflect ? "Granted" : "Denied")}\n";
×
160

161
        if (ctx.Identity.P.Reflect)
×
162
        {
×
163
            yield return "Full Permissions:\n";
×
164
            yield return $"\tInvoke: {ctx.Identity.P.Invoke.Query}\n";
×
165
            yield return $"\tView: {Synth(ctx.Identity.P.View.Query)}\n";
×
166
            yield return $"\tEdit: {Synth(ctx.Identity.P.Edit.Query)}\n";
×
167
            yield return $"\tVoucher: {Synth(ctx.Identity.P.Voucher.Query)}\n";
×
168
            yield return $"\tAsset: {Synth(ctx.Identity.P.Asset.Query)}\n";
×
169
            yield return $"\tAmort: {Synth(ctx.Identity.P.Amort.Query)}\n";
×
170
        }
×
171
    }
×
172

173
    public async IAsyncEnumerable<string> Invite(Context ctx, string id)
174
    {
×
175
        ctx.Identity.WillInvoke("invite");
×
176
        var aid = await Mgr.InviteWebAuthn(id);
×
177
        yield return $"Invitation created, please visit the following link:\n";
×
178
        yield return $"https://{AuthnManager.Server}/invite?q={aid.StringID}\n";
×
179
        yield return $"This link will expire at {aid.ExpiresAt:s} UTC time.\n";
×
180
    }
×
181

182
    public async IAsyncEnumerable<string> ListIdentites(Context ctx)
183
    {
×
184
        ctx.Identity.WillInvoke("an-ids");
×
185

186
        if (RbacManager.UnlimitedName != null)
×
187
            yield return $"{((string)null).AsId()} (aka. {RbacManager.UnlimitedName.AsId()})\n";
×
188

189
        foreach (var id in RbacManager.Identities)
×
190
            if (id.Disabled)
×
191
                yield return $"{id.Name.AsId()} (disabled)\n";
×
192
            else
193
                yield return $"{id.Name.AsId()}\n";
×
194
    }
×
195

196
    public async IAsyncEnumerable<string> ListAuthn(Context ctx, string id)
197
    {
×
198
        if (!string.IsNullOrEmpty(id))
×
199
        {
×
200
            ctx.TrueIdentity.WillInvoke("an-list-other");
×
201
        }
×
202
        else if (ctx.Identity == ctx.TrueIdentity)
×
203
        {
×
204
            ctx.TrueIdentity.WillInvoke("an-list-self");
×
205
            id = ctx.TrueIdentity.Name;
×
206
        }
×
207
        else
208
        {
×
209
            ctx.TrueIdentity.WillInvoke("an-list-other");
×
210
            id = ctx.Identity.Name;
×
211
        }
×
212

213
        yield return $"Authentication methods for {id.AsId()}:\n\n";
×
214

215
        if (id == null) // unlimited
×
216
            id = RbacManager.UnlimitedName;
×
217

218
        await foreach (var aid in m_Db.SelectAuths(id))
×
219
        {
×
220
            if (aid is WebAuthn wa)
×
221
            {
×
222
                yield return $"{aid.StringID} WebAuthn\n";
×
223
                if (wa.AttestationOptions != null)
×
224
                {
×
225
                    yield return $"\tPending invitation since {wa.InvitedAt:yyyy-MM-ddTHH:mm:ss.sssZ}\n\n";
×
226
                    continue;
×
227
                }
228

229
                yield return $"\tCreated at {aid.CreatedAt:yyyy-MM-ddTHH:mm:ss.sssZ}; ";
×
230
                yield return $"Last used at {aid.LastUsedAt:yyyy-MM-ddTHH:mm:ss.sssZ}\n";
×
231
                yield return $"\tCredentialId = {Authn.FromBytes(wa.CredentialId)}\n";
×
232
                yield return $"\tBackup: {(wa.IsBackupEligible ? "" : "in")}eligible, ";
×
233
                yield return $"{(wa.IsBackedUp ? "" : "not")} backed up\n";
×
234
                yield return $"\tAttestation format: {wa.AttestationFormat}\n";
×
235
                if (wa.Transports != null)
×
236
                    yield return $"\tTransports: {string.Join(" ", wa.Transports)}\n\n";
×
237

238
                continue;
×
239
            }
240
            if (aid is CertAuthn ca)
×
241
            {
×
242
                yield return $"{aid.StringID} CertAuthn\n";
×
243
                yield return $"\tCreated at {aid.CreatedAt:yyyy-MM-ddTHH:mm:ss.sssZ}; ";
×
244
                yield return $"Last used at {aid.LastUsedAt:yyyy-MM-ddTHH:mm:ss.sssZ}\n";
×
245
                yield return $"\tFingerprint: {ctx.Certificate.Fingerprint}\n";
×
246
                yield return $"\tSubjectDN: {ctx.Certificate.SubjectDN}\n";
×
247
                yield return $"\tIssuerDN: {ctx.Certificate.IssuerDN}\n";
×
248
                yield return $"\tSerial: {ctx.Certificate.Serial}\n";
×
249
                yield return $"\tValid not Before: {ctx.Certificate.Start}\n";
×
250
                yield return $"\tValid not After: {ctx.Certificate.End}\n\n";
×
251
                continue;
×
252
            }
253
            yield return $"{aid.StringID} Unknown\n\n";
×
254
            yield return $"\tCreated at {aid.CreatedAt:yyyy-MM-ddTHH:mm:ss.sssZ}; ";
×
255
            yield return $"Last used at {aid.LastUsedAt:yyyy-MM-ddTHH:mm:ss.sssZ}\n";
×
256
        }
×
257
    }
×
258

259
    private async IAsyncEnumerable<string> SaveCert(Context ctx, string id)
260
    {
×
261
        if (ctx.Certificate == null || string.IsNullOrEmpty(ctx.Certificate.Fingerprint))
×
262
            throw new ApplicationException("No certifiacte present");
×
263

264
        if (!string.IsNullOrEmpty(id))
×
265
        {
×
266
            ctx.TrueIdentity.WillInvoke("an-cert-other");
×
267
            ctx.Certificate.IdentityName = id;
×
268
        }
×
269
        else if (ctx.Identity == ctx.TrueIdentity)
×
270
        {
×
271
            ctx.TrueIdentity.WillInvoke("an-cert-self");
×
272
            ctx.Certificate.IdentityName = ctx.TrueIdentity.Name;
×
273
        }
×
274
        else
275
        {
×
276
            ctx.TrueIdentity.WillInvoke("an-cert-other");
×
277
            ctx.Certificate.IdentityName = ctx.Identity.Name;
×
278
        }
×
279

280
        await Mgr.RegisterCert(ctx.Certificate);
×
281

282
        yield return $"Certificate saved for identity {ctx.Identity.Name.AsId()}, you can now authenticate\n";
×
283
        yield return $"using certificate {ctx.Certificate.Fingerprint}\n";
×
284
        yield return $"Auth: {ctx.Certificate.ID}\n";
×
285
    }
×
286

287
    private async IAsyncEnumerable<string> RemoveAuthn(Context ctx, string sid)
288
    {
×
289
        if (string.IsNullOrWhiteSpace(sid))
×
290
        {
×
291
            yield return "An ID is required; find it by an-list\n";
×
292
            yield break;
×
293
        }
294

295
        var id = Authn.ToBytes(sid);
×
296
        var aid = await m_Db.SelectAuth(id);
×
297
        if (aid == null)
×
298
        {
×
299
            yield return "No such AuthIdentity for removal\n";
×
300
            yield break;
×
301
        }
302

303
        if (aid.IdentityName == ctx.TrueIdentity.Name)
×
304
            ctx.TrueIdentity.WillInvoke("an-rm-self");
×
305
        else
306
            ctx.TrueIdentity.WillInvoke("an-rm-other");
×
307

308
        if (!await m_Db.DeleteAuth(id))
×
309
            throw new ApplicationException("Delete failed");
×
310

311
        yield return $"AuthIdentity {sid} removed\n";
×
312
    }
×
313
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc