@h-ai/iam 0.1.0-alpha.73 → 0.1.0-alpha.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +29 -15
- package/README.ja.md +28 -14
- package/README.md +28 -14
- package/dist/browser.d.ts +51 -2
- package/dist/browser.js +1 -1
- package/dist/{chunk-22SERPFD.js → chunk-KPG3M34Q.js} +37 -15
- package/dist/chunk-KPG3M34Q.js.map +1 -0
- package/dist/index.js +452 -190
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/dist/chunk-22SERPFD.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { HaiIamError, IamConfigSchema, SessionConfigSchema, SecurityConfigSchema, LoginConfigSchema, OtpConfigSchema, ApiKeyConfigSchema, AuthUiConfigSchema, PasswordConfigSchema, RbacConfigSchema, AgreementConfigSchema, PasswordResetConfigSchema, RegisterConfigSchema } from './chunk-
|
|
2
|
-
export { AccountConfigSchema, AgreementConfigSchema, ApiKeyConfigSchema, AuthStrategyTypeSchema, AuthUiConfigSchema, CaptchaConfigSchema, HaiIamError, IamConfigSchema, LdapConfigSchema, LoginAccessConfigSchema, LoginConfigSchema, OtpConfigSchema, PasswordConfigSchema, PasswordResetConfigSchema, RbacConfigSchema, RegisterConfigSchema, SecurityConfigSchema, SessionConfigSchema } from './chunk-
|
|
1
|
+
import { HaiIamError, IamConfigSchema, SessionConfigSchema, SecurityConfigSchema, LoginConfigSchema, OtpConfigSchema, ApiKeyConfigSchema, AuthUiConfigSchema, PasswordConfigSchema, RbacConfigSchema, AgreementConfigSchema, PasswordResetConfigSchema, RegisterConfigSchema } from './chunk-KPG3M34Q.js';
|
|
2
|
+
export { AccountConfigSchema, AgreementConfigSchema, ApiKeyConfigSchema, AuthStrategyTypeSchema, AuthUiConfigSchema, CaptchaConfigSchema, HaiIamError, IamConfigSchema, LdapConfigSchema, LoginAccessConfigSchema, LoginConfigSchema, OtpConfigSchema, PasswordConfigSchema, PasswordResetConfigSchema, RbacConfigSchema, RegisterConfigSchema, SecurityConfigSchema, SessionConfigSchema } from './chunk-KPG3M34Q.js';
|
|
3
3
|
import { cache } from '@h-ai/cache';
|
|
4
4
|
import { core, err, ok } from '@h-ai/core';
|
|
5
5
|
import { crypto as crypto$1 } from '@h-ai/crypto';
|
|
6
6
|
import { migrateSchema, reldb, schemaColumns, BaseReldbCrudRepository, HaiReldbError } from '@h-ai/reldb';
|
|
7
|
-
import { createHash, randomBytes, randomInt } from 'crypto';
|
|
7
|
+
import { randomUUID, createHash, randomBytes, randomInt } from 'crypto';
|
|
8
8
|
import { isIP, BlockList } from 'net';
|
|
9
9
|
import { verifySolution, pbkdf2, createChallenge } from 'altcha/lib';
|
|
10
10
|
import { z } from 'zod';
|
|
@@ -213,7 +213,9 @@ var en_US_default = {
|
|
|
213
213
|
iam_captchaRequired: "Complete the security check first",
|
|
214
214
|
iam_captchaInvalid: "Security check is invalid or expired; try again",
|
|
215
215
|
iam_captchaDisabled: "Security challenges are disabled",
|
|
216
|
-
iam_captchaUnavailable: "Security check is temporarily unavailable"
|
|
216
|
+
iam_captchaUnavailable: "Security check is temporarily unavailable",
|
|
217
|
+
iam_sensitiveOperationsInvalid: "Invalid sensitive operation IDs: use at most 1000 entries of 1\u2013128 characters each.",
|
|
218
|
+
iam_terminalTypeInvalid: "Invalid terminal type: use web, app, desktop or api."
|
|
217
219
|
};
|
|
218
220
|
|
|
219
221
|
// messages/ja-JP.json
|
|
@@ -419,7 +421,9 @@ var ja_JP_default = {
|
|
|
419
421
|
iam_captchaRequired: "\u5148\u306B\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u3092\u5B8C\u4E86\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
420
422
|
iam_captchaInvalid: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u304C\u7121\u52B9\u307E\u305F\u306F\u671F\u9650\u5207\u308C\u3067\u3059\u3002\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
421
423
|
iam_captchaDisabled: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u306F\u7121\u52B9\u3067\u3059",
|
|
422
|
-
iam_captchaUnavailable: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u3092\u4E00\u6642\u7684\u306B\u5229\u7528\u3067\u304D\u307E\u305B\u3093"
|
|
424
|
+
iam_captchaUnavailable: "\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u78BA\u8A8D\u3092\u4E00\u6642\u7684\u306B\u5229\u7528\u3067\u304D\u307E\u305B\u3093",
|
|
425
|
+
iam_sensitiveOperationsInvalid: "\u91CD\u8981\u64CD\u4F5C ID \u306F\u6700\u59271000\u4EF6\u3001\u54041\uFF5E128\u6587\u5B57\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
426
|
+
iam_terminalTypeInvalid: "\u7AEF\u672B\u7A2E\u5225\u306F web\u3001app\u3001desktop\u3001api \u306E\u3044\u305A\u308C\u304B\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
423
427
|
};
|
|
424
428
|
|
|
425
429
|
// messages/zh-CN.json
|
|
@@ -625,7 +629,9 @@ var zh_CN_default = {
|
|
|
625
629
|
iam_captchaRequired: "\u8BF7\u5148\u5B8C\u6210\u5B89\u5168\u9A8C\u8BC1",
|
|
626
630
|
iam_captchaInvalid: "\u5B89\u5168\u9A8C\u8BC1\u65E0\u6548\u6216\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u8BD5",
|
|
627
631
|
iam_captchaDisabled: "\u5B89\u5168\u9A8C\u8BC1\u672A\u542F\u7528",
|
|
628
|
-
iam_captchaUnavailable: "\u5B89\u5168\u9A8C\u8BC1\u6682\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"
|
|
632
|
+
iam_captchaUnavailable: "\u5B89\u5168\u9A8C\u8BC1\u6682\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
633
|
+
iam_sensitiveOperationsInvalid: "\u654F\u611F\u64CD\u4F5C ID \u5217\u8868\u65E0\u6548\uFF1A\u6700\u591A 1000 \u9879\uFF0C\u6BCF\u9879\u4E3A 1\u2013128 \u5B57\u7B26\u3002",
|
|
634
|
+
iam_terminalTypeInvalid: "\u7EC8\u7AEF\u7C7B\u578B\u65E0\u6548\uFF0C\u8BF7\u4F7F\u7528 web\u3001app\u3001desktop \u6216 api\u3002"
|
|
629
635
|
};
|
|
630
636
|
|
|
631
637
|
// src/iam-i18n.ts
|
|
@@ -635,6 +641,118 @@ var iamM = core.i18n.createMessageGetter({
|
|
|
635
641
|
"ja-JP": ja_JP_default
|
|
636
642
|
});
|
|
637
643
|
|
|
644
|
+
// src/iam-cache.ts
|
|
645
|
+
var revisionKey = "hai:iam:read_cache:revision";
|
|
646
|
+
var transactions = /* @__PURE__ */ new WeakMap();
|
|
647
|
+
var logger = core.logger.child({ module: "iam", scope: "read-cache" });
|
|
648
|
+
var degraded = false;
|
|
649
|
+
function failure() {
|
|
650
|
+
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_sessionExpired"));
|
|
651
|
+
}
|
|
652
|
+
async function revision() {
|
|
653
|
+
const current = await cache.kv.get(revisionKey);
|
|
654
|
+
if (!current.success)
|
|
655
|
+
return current;
|
|
656
|
+
if (current.data)
|
|
657
|
+
return ok(current.data);
|
|
658
|
+
const created = await cache.kv.set(revisionKey, { epoch: randomUUID(), writers: 0 }, { nx: true });
|
|
659
|
+
if (!created.success)
|
|
660
|
+
return created;
|
|
661
|
+
const loaded = await cache.kv.get(revisionKey);
|
|
662
|
+
return loaded.success && loaded.data ? ok(loaded.data) : failure();
|
|
663
|
+
}
|
|
664
|
+
async function change(delta) {
|
|
665
|
+
for (let attempt = 0; attempt < 32; attempt++) {
|
|
666
|
+
const current = await revision();
|
|
667
|
+
if (!current.success)
|
|
668
|
+
return current;
|
|
669
|
+
const written = await cache.kv.compareAndSwap(revisionKey, current.data, { epoch: randomUUID(), writers: Math.max(0, current.data.writers + delta) });
|
|
670
|
+
if (!written.success)
|
|
671
|
+
return written;
|
|
672
|
+
if (written.data)
|
|
673
|
+
return ok(void 0);
|
|
674
|
+
}
|
|
675
|
+
return failure();
|
|
676
|
+
}
|
|
677
|
+
async function readIamCache(key, load) {
|
|
678
|
+
const before = await revision();
|
|
679
|
+
if (!before.success || before.data.writers)
|
|
680
|
+
return load();
|
|
681
|
+
const address = `hai:iam:read_cache:${before.data.epoch}:${key}`;
|
|
682
|
+
const cached = await cache.kv.get(address);
|
|
683
|
+
if (cached.success && cached.data) {
|
|
684
|
+
const after2 = await revision();
|
|
685
|
+
if (after2.success && !after2.data.writers && after2.data.epoch === before.data.epoch)
|
|
686
|
+
return ok(cached.data.value);
|
|
687
|
+
return load();
|
|
688
|
+
}
|
|
689
|
+
const result = await load();
|
|
690
|
+
if (!result.success)
|
|
691
|
+
return result;
|
|
692
|
+
const after = await revision();
|
|
693
|
+
if (after.success && !after.data.writers && after.data.epoch === before.data.epoch)
|
|
694
|
+
await cache.kv.set(address, { value: result.data }, { ex: 60 });
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
async function finish() {
|
|
698
|
+
const completed = await change(-1);
|
|
699
|
+
if (!completed.success && !degraded) {
|
|
700
|
+
degraded = true;
|
|
701
|
+
logger.warn("IAM cache invalidation incomplete; authorization reads use database", { code: completed.error.code });
|
|
702
|
+
}
|
|
703
|
+
if (completed.success)
|
|
704
|
+
degraded = false;
|
|
705
|
+
}
|
|
706
|
+
async function enroll(tx) {
|
|
707
|
+
const existing = transactions.get(tx);
|
|
708
|
+
if (existing)
|
|
709
|
+
return existing;
|
|
710
|
+
const pending = (async () => {
|
|
711
|
+
const started = await change(1);
|
|
712
|
+
if (!started.success)
|
|
713
|
+
return started;
|
|
714
|
+
const commit = tx.commit.bind(tx);
|
|
715
|
+
const rollback = tx.rollback.bind(tx);
|
|
716
|
+
tx.commit = async () => {
|
|
717
|
+
const result = await commit();
|
|
718
|
+
if (result.success)
|
|
719
|
+
await finish();
|
|
720
|
+
return result;
|
|
721
|
+
};
|
|
722
|
+
tx.rollback = async () => {
|
|
723
|
+
const result = await rollback();
|
|
724
|
+
if (result.success)
|
|
725
|
+
await finish();
|
|
726
|
+
return result;
|
|
727
|
+
};
|
|
728
|
+
return ok(void 0);
|
|
729
|
+
})();
|
|
730
|
+
transactions.set(tx, pending);
|
|
731
|
+
return pending;
|
|
732
|
+
}
|
|
733
|
+
function guardIamMutation(operation, txIndex) {
|
|
734
|
+
return async (...args) => {
|
|
735
|
+
const candidate = txIndex === void 0 ? void 0 : args[txIndex];
|
|
736
|
+
const tx = candidate;
|
|
737
|
+
let started;
|
|
738
|
+
try {
|
|
739
|
+
started = tx ? await enroll(tx) : await change(1);
|
|
740
|
+
} catch {
|
|
741
|
+
return failure();
|
|
742
|
+
}
|
|
743
|
+
if (!started.success)
|
|
744
|
+
return started;
|
|
745
|
+
try {
|
|
746
|
+
return await operation(...args);
|
|
747
|
+
} catch {
|
|
748
|
+
return failure();
|
|
749
|
+
} finally {
|
|
750
|
+
if (!tx)
|
|
751
|
+
await finish();
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
638
756
|
// src/authn/apikey/iam-authn-apikey-repository.ts
|
|
639
757
|
var TABLE_NAME = "hai_iam_api_keys";
|
|
640
758
|
var API_KEY_SORT_COLUMNS = {
|
|
@@ -736,6 +854,10 @@ async function createDbApiKeyRepository() {
|
|
|
736
854
|
return apiKeyRepoInstance;
|
|
737
855
|
const repo = new DbApiKeyRepository();
|
|
738
856
|
await repo.count();
|
|
857
|
+
repo.removeById = guardIamMutation(repo.removeById.bind(repo), 1);
|
|
858
|
+
const update = repo.updateFields.bind(repo);
|
|
859
|
+
const protectedUpdate = guardIamMutation(update, 2);
|
|
860
|
+
repo.updateFields = (id, data, tx) => Object.keys(data).every((key) => key === "lastUsedAt") ? update(id, data, tx) : protectedUpdate(id, data, tx);
|
|
739
861
|
apiKeyRepoInstance = repo;
|
|
740
862
|
apiKeyRepoSqlOps = reldb.sql;
|
|
741
863
|
return repo;
|
|
@@ -882,6 +1004,7 @@ function buildSession(options, now, sessionTtl, accessToken, credentialVersion)
|
|
|
882
1004
|
roles: options.roles ?? [],
|
|
883
1005
|
permissions: options.permissions ?? [],
|
|
884
1006
|
source: options.source,
|
|
1007
|
+
terminalType: options.terminalType ?? "web",
|
|
885
1008
|
accessToken,
|
|
886
1009
|
createdAt: now,
|
|
887
1010
|
startedAt: now,
|
|
@@ -921,7 +1044,7 @@ function applySessionPatch(session, patch) {
|
|
|
921
1044
|
}
|
|
922
1045
|
|
|
923
1046
|
// src/ticket/iam-ticket-functions.ts
|
|
924
|
-
var
|
|
1047
|
+
var logger2 = core.logger.child({ module: "iam", scope: "ticket" });
|
|
925
1048
|
var TICKET_KEY_PREFIX = "hai:iam:ticket:";
|
|
926
1049
|
var DEFAULT_TICKET_TTL_MS = 3e4;
|
|
927
1050
|
function buildTicketKey(ticket) {
|
|
@@ -941,7 +1064,7 @@ function createTicketOperations() {
|
|
|
941
1064
|
};
|
|
942
1065
|
const saved = await cache.kv.set(buildTicketKey(ticket), record, { px: ttlMs, nx: true });
|
|
943
1066
|
if (!saved.success || !saved.data) {
|
|
944
|
-
|
|
1067
|
+
logger2.warn("Failed to issue ticket", { purpose: options.purpose });
|
|
945
1068
|
return err(HaiIamError.TICKET_ISSUE_FAILED, iamM("iam_ticketIssueFailed"));
|
|
946
1069
|
}
|
|
947
1070
|
return ok({ ticket, expiresAt });
|
|
@@ -1145,6 +1268,12 @@ async function createDbUserRepository() {
|
|
|
1145
1268
|
return userRepoInstance;
|
|
1146
1269
|
const repo = new DbUserRepository();
|
|
1147
1270
|
await repo.count();
|
|
1271
|
+
repo.create = guardIamMutation(repo.create.bind(repo), 1);
|
|
1272
|
+
repo.updateById = guardIamMutation(repo.updateById.bind(repo), 2);
|
|
1273
|
+
repo.deleteById = guardIamMutation(repo.deleteById.bind(repo), 1);
|
|
1274
|
+
repo.suspendInactive = guardIamMutation(repo.suspendInactive.bind(repo));
|
|
1275
|
+
repo.setPassword = guardIamMutation(repo.setPassword.bind(repo));
|
|
1276
|
+
repo.commitPasswordReset = guardIamMutation(repo.commitPasswordReset.bind(repo));
|
|
1148
1277
|
userRepoInstance = repo;
|
|
1149
1278
|
userRepoSqlOps = reldb.sql;
|
|
1150
1279
|
return repo;
|
|
@@ -1415,7 +1544,7 @@ async function resetLoginFailures(userRepository, user, policy) {
|
|
|
1415
1544
|
}
|
|
1416
1545
|
|
|
1417
1546
|
// src/authn/apikey/iam-authn-apikey-strategy.ts
|
|
1418
|
-
var
|
|
1547
|
+
var logger3 = core.logger.child({ module: "iam", scope: "apikey-strategy" });
|
|
1419
1548
|
var KEY_PREFIX_LENGTH = 12;
|
|
1420
1549
|
function generateRawKey(prefix2) {
|
|
1421
1550
|
const hex = randomBytes(32).toString("hex");
|
|
@@ -1441,7 +1570,7 @@ function createApiKeyStrategy(config) {
|
|
|
1441
1570
|
const apikeyConfig = config.apikeyConfig ? ApiKeyConfigSchema.parse(config.apikeyConfig) : ApiKeyConfigSchema.parse({});
|
|
1442
1571
|
const { userRepository, apiKeyRepository } = config;
|
|
1443
1572
|
const passwordOps = crypto$1.password;
|
|
1444
|
-
async function
|
|
1573
|
+
async function loadRawKey(rawKey) {
|
|
1445
1574
|
const prefix2 = extractKeyPrefix(rawKey);
|
|
1446
1575
|
const candidatesResult = await apiKeyRepository.findByKeyPrefix(prefix2);
|
|
1447
1576
|
if (!candidatesResult.success) {
|
|
@@ -1460,12 +1589,26 @@ function createApiKeyStrategy(config) {
|
|
|
1460
1589
|
return err(HaiIamError.APIKEY_EXPIRED, iamM("iam_apikeyExpired"));
|
|
1461
1590
|
}
|
|
1462
1591
|
apiKeyRepository.updateFields(candidate.id, { lastUsedAt: /* @__PURE__ */ new Date() }).catch(() => {
|
|
1463
|
-
|
|
1592
|
+
logger3.warn("Failed to update API Key lastUsedAt", { keyId: candidate.id });
|
|
1464
1593
|
});
|
|
1465
1594
|
return ok(toApiKey(candidate));
|
|
1466
1595
|
}
|
|
1467
1596
|
return err(HaiIamError.APIKEY_INVALID, iamM("iam_apikeyInvalid"));
|
|
1468
1597
|
}
|
|
1598
|
+
async function verifyRawKey(rawKey) {
|
|
1599
|
+
const fingerprint = createHash("sha256").update(rawKey).digest("hex");
|
|
1600
|
+
const result = await readIamCache(`verified_apikey:${fingerprint}`, async () => {
|
|
1601
|
+
const verified = await loadRawKey(rawKey);
|
|
1602
|
+
return verified.success ? ok({ ...verified.data, expiresAt: verified.data.expiresAt?.getTime() ?? null, createdAt: verified.data.createdAt.getTime(), lastUsedAt: verified.data.lastUsedAt?.getTime() ?? null }) : verified;
|
|
1603
|
+
});
|
|
1604
|
+
if (!result.success)
|
|
1605
|
+
return result;
|
|
1606
|
+
if (!result.data.enabled)
|
|
1607
|
+
return err(HaiIamError.APIKEY_DISABLED, iamM("iam_apikeyDisabled"));
|
|
1608
|
+
if (result.data.expiresAt !== null && result.data.expiresAt <= Date.now())
|
|
1609
|
+
return err(HaiIamError.APIKEY_EXPIRED, iamM("iam_apikeyExpired"));
|
|
1610
|
+
return ok({ ...result.data, expiresAt: result.data.expiresAt === null ? null : new Date(result.data.expiresAt), createdAt: new Date(result.data.createdAt), lastUsedAt: result.data.lastUsedAt === null ? null : new Date(result.data.lastUsedAt) });
|
|
1611
|
+
}
|
|
1469
1612
|
const strategy = {
|
|
1470
1613
|
type: "apikey",
|
|
1471
1614
|
name: "apikey-strategy",
|
|
@@ -1490,7 +1633,7 @@ function createApiKeyStrategy(config) {
|
|
|
1490
1633
|
if (!isUserActive(userResult.data)) {
|
|
1491
1634
|
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
1492
1635
|
}
|
|
1493
|
-
|
|
1636
|
+
logger3.debug("API Key authentication succeeded", { userId: userResult.data.id, keyId: apiKey.id });
|
|
1494
1637
|
return ok({ user: { ...toUser(userResult.data), credentialVersion: userResult.data.credentialVersion ?? "" }, apiKey });
|
|
1495
1638
|
}
|
|
1496
1639
|
};
|
|
@@ -1527,7 +1670,7 @@ function createApiKeyStrategy(config) {
|
|
|
1527
1670
|
const insertResult = await apiKeyRepository.insert(storedApiKey);
|
|
1528
1671
|
if (!insertResult.success)
|
|
1529
1672
|
return insertResult;
|
|
1530
|
-
|
|
1673
|
+
logger3.info("API Key created", { userId, keyId: id, name: options.name });
|
|
1531
1674
|
return ok({ apiKey: toApiKey(storedApiKey), rawKey });
|
|
1532
1675
|
} catch (error) {
|
|
1533
1676
|
return err(HaiIamError.INTERNAL_ERROR, iamM("iam_apikeyCreateFailed", { params: { message: String(error) } }), error);
|
|
@@ -1549,7 +1692,7 @@ function createApiKeyStrategy(config) {
|
|
|
1549
1692
|
const result = await apiKeyRepository.removeById(keyId);
|
|
1550
1693
|
if (!result.success)
|
|
1551
1694
|
return result;
|
|
1552
|
-
|
|
1695
|
+
logger3.info("API Key revoked", { keyId });
|
|
1553
1696
|
return ok(void 0);
|
|
1554
1697
|
},
|
|
1555
1698
|
async verifyApiKey(rawKey) {
|
|
@@ -1600,7 +1743,7 @@ function createLoginAccessCheck(policy) {
|
|
|
1600
1743
|
return ok(void 0);
|
|
1601
1744
|
};
|
|
1602
1745
|
}
|
|
1603
|
-
var
|
|
1746
|
+
var logger4 = core.logger.child({ module: "iam", scope: "captcha" });
|
|
1604
1747
|
var prefix = "hai:iam:captcha:";
|
|
1605
1748
|
var operationSchema = z.enum(["login", "register", "sendOtp", "passwordReset", "renewPassword"]);
|
|
1606
1749
|
var proofSchema = z.object({
|
|
@@ -1632,7 +1775,7 @@ async function createIamCaptcha(config, input) {
|
|
|
1632
1775
|
return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
|
|
1633
1776
|
return ok({ id, challenge });
|
|
1634
1777
|
} catch (error) {
|
|
1635
|
-
|
|
1778
|
+
logger4.error("Captcha challenge generation failed", { operation: operation.data, error });
|
|
1636
1779
|
return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
|
|
1637
1780
|
}
|
|
1638
1781
|
}
|
|
@@ -1668,7 +1811,7 @@ async function verifyIamCaptcha(config, operation, proof) {
|
|
|
1668
1811
|
return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
|
|
1669
1812
|
return removed.data === 1 ? ok(void 0) : err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
|
|
1670
1813
|
} catch (error) {
|
|
1671
|
-
|
|
1814
|
+
logger4.error("Captcha verification failed", { operation, error });
|
|
1672
1815
|
return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
|
|
1673
1816
|
}
|
|
1674
1817
|
}
|
|
@@ -1726,7 +1869,7 @@ async function loadLdapClientFactory() {
|
|
|
1726
1869
|
}
|
|
1727
1870
|
};
|
|
1728
1871
|
}
|
|
1729
|
-
var
|
|
1872
|
+
var logger5 = core.logger.child({ module: "iam", scope: "ldap-strategy" });
|
|
1730
1873
|
function escapeLdapFilterValue(value) {
|
|
1731
1874
|
return value.replace(/[\\*()/\0]/g, (ch) => {
|
|
1732
1875
|
const hex = ch.charCodeAt(0).toString(16).padStart(2, "0");
|
|
@@ -1829,7 +1972,7 @@ function createLdapStrategy(config) {
|
|
|
1829
1972
|
if (!failureResult.success)
|
|
1830
1973
|
return failureResult;
|
|
1831
1974
|
}
|
|
1832
|
-
|
|
1975
|
+
logger5.warn("LDAP authentication failed", { username });
|
|
1833
1976
|
return err(
|
|
1834
1977
|
HaiIamError.INVALID_CREDENTIALS,
|
|
1835
1978
|
iamM("iam_passwordWrong")
|
|
@@ -1867,7 +2010,7 @@ function createLdapStrategy(config) {
|
|
|
1867
2010
|
return assigned;
|
|
1868
2011
|
}
|
|
1869
2012
|
} catch (callbackError) {
|
|
1870
|
-
|
|
2013
|
+
logger5.warn("onUserAutoRegistered callback failed", { userId: storedUser?.id, error: callbackError });
|
|
1871
2014
|
}
|
|
1872
2015
|
}
|
|
1873
2016
|
} else if (storedUser && syncUser) {
|
|
@@ -1892,7 +2035,7 @@ function createLdapStrategy(config) {
|
|
|
1892
2035
|
const resetResult = await resetLoginFailures(userRepository, storedUser, failurePolicy);
|
|
1893
2036
|
if (!resetResult.success)
|
|
1894
2037
|
return resetResult;
|
|
1895
|
-
|
|
2038
|
+
logger5.debug("LDAP authentication succeeded", { userId: storedUser.id });
|
|
1896
2039
|
return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
|
|
1897
2040
|
} finally {
|
|
1898
2041
|
await client.unbind();
|
|
@@ -1912,7 +2055,7 @@ function createCacheOtpRepository() {
|
|
|
1912
2055
|
if (otpRepoInstance)
|
|
1913
2056
|
return otpRepoInstance;
|
|
1914
2057
|
const keyFor = (identifier) => `hai:iam:otp:${normalizeOtpIdentifier(identifier)}`;
|
|
1915
|
-
const
|
|
2058
|
+
const failure2 = () => err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"));
|
|
1916
2059
|
const invalid = () => err(HaiIamError.OTP_INVALID, iamM("iam_otpNotExistOrExpired"));
|
|
1917
2060
|
otpRepoInstance = {
|
|
1918
2061
|
async issueOtp(input) {
|
|
@@ -1927,7 +2070,7 @@ function createCacheOtpRepository() {
|
|
|
1927
2070
|
const key = keyFor(input.identifier);
|
|
1928
2071
|
const current = await cache.kv.get(key);
|
|
1929
2072
|
if (!current.success)
|
|
1930
|
-
return
|
|
2073
|
+
return failure2();
|
|
1931
2074
|
if (current.data && current.data.cooldownUntil > Date.now()) {
|
|
1932
2075
|
return err(HaiIamError.OTP_RESEND_TOO_FAST, iamM("iam_otpResendTooFast", { params: { seconds: Math.ceil((current.data.cooldownUntil - Date.now()) / 1e3) } }));
|
|
1933
2076
|
}
|
|
@@ -1935,7 +2078,7 @@ function createCacheOtpRepository() {
|
|
|
1935
2078
|
return invalid();
|
|
1936
2079
|
const published = await cache.kv.compareAndSwap(key, current.data ?? void 0, state, { px: Math.max(state.expiresAt, state.cooldownUntil) - Date.now() });
|
|
1937
2080
|
if (!published.success)
|
|
1938
|
-
return
|
|
2081
|
+
return failure2();
|
|
1939
2082
|
if (!published.data)
|
|
1940
2083
|
return err(HaiIamError.SECURITY_BUSY, iamM("iam_securityBusy"));
|
|
1941
2084
|
if (Date.now() >= state.expiresAt)
|
|
@@ -1948,7 +2091,7 @@ function createCacheOtpRepository() {
|
|
|
1948
2091
|
for (let attempt = 0; attempt < 16; attempt++) {
|
|
1949
2092
|
const current = await cache.kv.get(key);
|
|
1950
2093
|
if (!current.success)
|
|
1951
|
-
return
|
|
2094
|
+
return failure2();
|
|
1952
2095
|
const state = current.data;
|
|
1953
2096
|
if (!state || state.code === null || state.expiresAt <= Date.now() || state.attempts >= maxAttempts || version && state.version !== version)
|
|
1954
2097
|
return invalid();
|
|
@@ -1958,7 +2101,7 @@ function createCacheOtpRepository() {
|
|
|
1958
2101
|
const next = { ...state, attempts, code: correct || attempts >= maxAttempts ? null : state.code };
|
|
1959
2102
|
const committed = await cache.kv.compareAndSwap(key, state, next, { keepTtl: true });
|
|
1960
2103
|
if (!committed.success)
|
|
1961
|
-
return
|
|
2104
|
+
return failure2();
|
|
1962
2105
|
if (!committed.data)
|
|
1963
2106
|
continue;
|
|
1964
2107
|
if (!correct || state.expiresAt <= Date.now())
|
|
@@ -1970,7 +2113,7 @@ function createCacheOtpRepository() {
|
|
|
1970
2113
|
};
|
|
1971
2114
|
return otpRepoInstance;
|
|
1972
2115
|
}
|
|
1973
|
-
var
|
|
2116
|
+
var logger6 = core.logger.child({ module: "iam", scope: "otp-strategy" });
|
|
1974
2117
|
var PHONE_IDENTIFIER_REGEX = /^\+?\d{8,}$/;
|
|
1975
2118
|
var PHONE_SEPARATOR_REGEX = /[\s-]/g;
|
|
1976
2119
|
function generateOtpCode(length) {
|
|
@@ -2068,7 +2211,7 @@ function createOtpStrategy(config) {
|
|
|
2068
2211
|
return assigned;
|
|
2069
2212
|
}
|
|
2070
2213
|
} catch (callbackError) {
|
|
2071
|
-
|
|
2214
|
+
logger6.warn("onUserAutoRegistered callback failed", { userId: storedUser.id, error: callbackError });
|
|
2072
2215
|
}
|
|
2073
2216
|
}
|
|
2074
2217
|
}
|
|
@@ -2080,7 +2223,7 @@ function createOtpStrategy(config) {
|
|
|
2080
2223
|
const resetResult = await resetLoginFailures(config.userRepository, storedUser, failurePolicy);
|
|
2081
2224
|
if (!resetResult.success)
|
|
2082
2225
|
return resetResult;
|
|
2083
|
-
|
|
2226
|
+
logger6.debug("OTP authentication succeeded", { userId: storedUser.id });
|
|
2084
2227
|
return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
|
|
2085
2228
|
}
|
|
2086
2229
|
};
|
|
@@ -2124,12 +2267,12 @@ function createOtpStrategy(config) {
|
|
|
2124
2267
|
);
|
|
2125
2268
|
}
|
|
2126
2269
|
const result = { expiresAt };
|
|
2127
|
-
|
|
2270
|
+
logger6.debug("OTP challenge sent", { identifier, expiresAt });
|
|
2128
2271
|
return ok(result);
|
|
2129
2272
|
}
|
|
2130
2273
|
return { strategy, challenge };
|
|
2131
2274
|
}
|
|
2132
|
-
var
|
|
2275
|
+
var logger7 = core.logger.child({ module: "iam", scope: "password-strategy" });
|
|
2133
2276
|
var PASSWORD_UPPERCASE_REGEX = /[A-Z]/;
|
|
2134
2277
|
var PASSWORD_LOWERCASE_REGEX = /[a-z]/;
|
|
2135
2278
|
var PASSWORD_DIGIT_REGEX = /\d/;
|
|
@@ -2209,7 +2352,7 @@ function createPasswordStrategy(config) {
|
|
|
2209
2352
|
const failureResult = await recordLoginFailure(config.userRepository, user, failurePolicy);
|
|
2210
2353
|
if (!failureResult.success)
|
|
2211
2354
|
return failureResult;
|
|
2212
|
-
|
|
2355
|
+
logger7.warn("Password verification failed", { userId: user.id });
|
|
2213
2356
|
return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
|
|
2214
2357
|
}
|
|
2215
2358
|
return ok(void 0);
|
|
@@ -2243,7 +2386,7 @@ function createPasswordStrategy(config) {
|
|
|
2243
2386
|
const resetResult = await resetLoginFailures(config.userRepository, storedUser, failurePolicy);
|
|
2244
2387
|
if (!resetResult.success)
|
|
2245
2388
|
return resetResult;
|
|
2246
|
-
|
|
2389
|
+
logger7.debug("Password authentication succeeded", { userId: storedUser.id });
|
|
2247
2390
|
return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
|
|
2248
2391
|
}
|
|
2249
2392
|
};
|
|
@@ -2263,7 +2406,7 @@ function createPasswordStrategy(config) {
|
|
|
2263
2406
|
}
|
|
2264
2407
|
|
|
2265
2408
|
// src/authn/iam-authn-functions.ts
|
|
2266
|
-
var
|
|
2409
|
+
var logger8 = core.logger.child({ module: "iam", scope: "authn" });
|
|
2267
2410
|
async function createAuthnOperations(deps) {
|
|
2268
2411
|
try {
|
|
2269
2412
|
const { config, sessionFunctions, authzFunctions, ldapClientFactory, ldapSyncUser, onOtpSendEmail, onOtpSendSms } = deps;
|
|
@@ -2347,10 +2490,10 @@ async function createAuthnOperations(deps) {
|
|
|
2347
2490
|
userRepository,
|
|
2348
2491
|
config
|
|
2349
2492
|
});
|
|
2350
|
-
|
|
2493
|
+
logger8.info("Authn sub-feature initialized");
|
|
2351
2494
|
return ok({ authn: operations.authn, loginRegisteredUser: operations.loginRegisteredUser, passwordStrategyResult: passwordResult, apiKeyFunctions });
|
|
2352
2495
|
} catch (error) {
|
|
2353
|
-
|
|
2496
|
+
logger8.error("Authn sub-feature initialization failed", { error });
|
|
2354
2497
|
return err(
|
|
2355
2498
|
HaiIamError.CONFIG_ERROR,
|
|
2356
2499
|
iamM("iam_initComponentFailed"),
|
|
@@ -2430,7 +2573,8 @@ function buildAuthnOperations(deps) {
|
|
|
2430
2573
|
avatarUrl: user.avatarUrl,
|
|
2431
2574
|
...effective,
|
|
2432
2575
|
apiKeyId: apiKey?.id,
|
|
2433
|
-
apiKeyScopes: apiKey?.scopes
|
|
2576
|
+
apiKeyScopes: apiKey?.scopes,
|
|
2577
|
+
terminalType: context?.terminalType ?? (apiKey ? "api" : "web")
|
|
2434
2578
|
});
|
|
2435
2579
|
if (!sessionResult.success) {
|
|
2436
2580
|
return sessionResult;
|
|
@@ -2444,7 +2588,7 @@ function buildAuthnOperations(deps) {
|
|
|
2444
2588
|
if (!recorded.success) {
|
|
2445
2589
|
const revoked = await sessionFunctions.delete(session.accessToken);
|
|
2446
2590
|
if (!revoked.success)
|
|
2447
|
-
|
|
2591
|
+
logger8.error("Failed to revoke session after login timestamp write failure", { userId: user.id, error: revoked.error });
|
|
2448
2592
|
return recorded;
|
|
2449
2593
|
}
|
|
2450
2594
|
const { credentialVersion, ...publicUser } = user;
|
|
@@ -2473,12 +2617,14 @@ function buildAuthnOperations(deps) {
|
|
|
2473
2617
|
...credentials
|
|
2474
2618
|
});
|
|
2475
2619
|
if (!authResult.success) {
|
|
2476
|
-
|
|
2620
|
+
logger8.warn("Login failed", { type, reason: authResult.error.code });
|
|
2477
2621
|
return authResult;
|
|
2478
2622
|
}
|
|
2623
|
+
if (credentials.expectedUserId && credentials.expectedUserId !== authResult.data.id)
|
|
2624
|
+
return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
|
|
2479
2625
|
const result = await buildAuthResult(authResult.data, void 0, context);
|
|
2480
2626
|
if (result.success) {
|
|
2481
|
-
|
|
2627
|
+
logger8.info("Login succeeded", { type, userId: authResult.data.id });
|
|
2482
2628
|
}
|
|
2483
2629
|
return result;
|
|
2484
2630
|
}
|
|
@@ -2486,24 +2632,24 @@ function buildAuthnOperations(deps) {
|
|
|
2486
2632
|
const session = await sessionFunctions.verifyToken(accessToken);
|
|
2487
2633
|
if (!session.success)
|
|
2488
2634
|
return session;
|
|
2489
|
-
const
|
|
2490
|
-
userRepository.findById(session.data.userId),
|
|
2491
|
-
authzFunctions.getUserAuthorization(session.data.userId)
|
|
2492
|
-
]);
|
|
2493
|
-
if (!user.success)
|
|
2494
|
-
return user;
|
|
2495
|
-
if (!user.data?.enabled)
|
|
2496
|
-
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
2497
|
-
if (session.data.credentialVersion !== (user.data.credentialVersion ?? ""))
|
|
2498
|
-
return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
|
|
2635
|
+
const authorization = await authzFunctions.getUserAuthorization(session.data.userId);
|
|
2499
2636
|
if (!authorization.success)
|
|
2500
2637
|
return authorization;
|
|
2501
|
-
const current = { ...session.data,
|
|
2638
|
+
const current = { ...session.data, ...authorization.data };
|
|
2502
2639
|
return ok(current.apiKeyId ? applyApiKeyScopes(current, current.apiKeyScopes ?? []) : current);
|
|
2503
2640
|
}
|
|
2504
2641
|
const tickets = createTicketOperations();
|
|
2505
2642
|
const sensitive = config.reauthentication;
|
|
2506
|
-
|
|
2643
|
+
async function requiresProof(operation) {
|
|
2644
|
+
if (!sensitive?.enabled)
|
|
2645
|
+
return ok(false);
|
|
2646
|
+
if (sensitive.operations.includes(operation))
|
|
2647
|
+
return ok(true);
|
|
2648
|
+
const configured = await authzFunctions.getSensitiveOperations(operation);
|
|
2649
|
+
if (!configured.success)
|
|
2650
|
+
return configured;
|
|
2651
|
+
return ok(sensitive.operations.includes(operation) || configured.data.includes(operation));
|
|
2652
|
+
}
|
|
2507
2653
|
return { loginRegisteredUser: async (user, context) => {
|
|
2508
2654
|
const stored = await userRepository.findById(user.id);
|
|
2509
2655
|
if (!stored.success)
|
|
@@ -2514,7 +2660,10 @@ function buildAuthnOperations(deps) {
|
|
|
2514
2660
|
}, authn: {
|
|
2515
2661
|
createCaptcha: (input) => createIamCaptcha(config.captcha, input),
|
|
2516
2662
|
async reauthenticate(accessToken, password, operation) {
|
|
2517
|
-
|
|
2663
|
+
const required = await requiresProof(operation);
|
|
2664
|
+
if (!required.success)
|
|
2665
|
+
return required;
|
|
2666
|
+
if (!required.data)
|
|
2518
2667
|
return err(HaiIamError.PERMISSION_DENIED, iamM("iam_reauthenticationRequired"));
|
|
2519
2668
|
const current = await verifyCurrentSession(accessToken);
|
|
2520
2669
|
if (!current.success)
|
|
@@ -2535,7 +2684,10 @@ function buildAuthnOperations(deps) {
|
|
|
2535
2684
|
const current = await verifyCurrentSession(accessToken);
|
|
2536
2685
|
if (!current.success)
|
|
2537
2686
|
return current;
|
|
2538
|
-
|
|
2687
|
+
const required = await requiresProof(operation);
|
|
2688
|
+
if (!required.success)
|
|
2689
|
+
return required;
|
|
2690
|
+
if (!required.data)
|
|
2539
2691
|
return ok(void 0);
|
|
2540
2692
|
if (!ticket)
|
|
2541
2693
|
return err(HaiIamError.REAUTHENTICATION_REQUIRED, iamM("iam_reauthenticationRequired"));
|
|
@@ -2590,6 +2742,7 @@ function buildAuthnOperations(deps) {
|
|
|
2590
2742
|
}
|
|
2591
2743
|
var TABLE_NAME3 = "hai_iam_permissions";
|
|
2592
2744
|
var PERMISSION_FIELDS = [
|
|
2745
|
+
{ fieldName: "sensitiveOperations", columnName: "sensitive_operations", def: { type: "JSON" }, select: true, create: true, update: true },
|
|
2593
2746
|
{
|
|
2594
2747
|
fieldName: "id",
|
|
2595
2748
|
columnName: "id",
|
|
@@ -2679,6 +2832,32 @@ async function createDbPermissionRepository() {
|
|
|
2679
2832
|
return repo;
|
|
2680
2833
|
}
|
|
2681
2834
|
var DbPermissionRepository = class extends BaseReldbCrudRepository {
|
|
2835
|
+
async getSensitiveOperations(operation) {
|
|
2836
|
+
if (operation !== void 0 && (typeof operation !== "string" || !operation.trim() || operation.length > 128))
|
|
2837
|
+
return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sensitiveOperationsInvalid"));
|
|
2838
|
+
const match = reldb.config?.type === "postgresql" ? "sensitive_operations::jsonb @> CAST(? AS jsonb)" : reldb.config?.type === "mysql" ? "JSON_CONTAINS(sensitive_operations, JSON_QUOTE(?))" : "EXISTS (SELECT 1 FROM json_each(sensitive_operations) WHERE type = 'text' AND value = ?)";
|
|
2839
|
+
const params = operation === void 0 ? [] : [reldb.config?.type === "postgresql" ? JSON.stringify([operation]) : operation];
|
|
2840
|
+
const result = await reldb.sql.query(`SELECT sensitive_operations FROM hai_iam_permissions WHERE sensitive_operations IS NOT NULL${operation === void 0 ? "" : ` AND ${match} LIMIT 1`}`, params);
|
|
2841
|
+
if (!result.success)
|
|
2842
|
+
return result;
|
|
2843
|
+
const operations = /* @__PURE__ */ new Set();
|
|
2844
|
+
try {
|
|
2845
|
+
for (const row of result.data) {
|
|
2846
|
+
const values = typeof row.sensitive_operations === "string" ? JSON.parse(row.sensitive_operations) : row.sensitive_operations;
|
|
2847
|
+
if (!Array.isArray(values) || values.length > 1e3)
|
|
2848
|
+
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_reauthenticationRequired"));
|
|
2849
|
+
for (const value of values) {
|
|
2850
|
+
if (typeof value !== "string" || !value.trim() || value.length > 128)
|
|
2851
|
+
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_reauthenticationRequired"));
|
|
2852
|
+
if (operation === void 0 || value.trim() === operation)
|
|
2853
|
+
operations.add(value.trim());
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return ok([...operations].sort());
|
|
2857
|
+
} catch {
|
|
2858
|
+
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_reauthenticationRequired"));
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2682
2861
|
constructor() {
|
|
2683
2862
|
super(reldb, {
|
|
2684
2863
|
table: TABLE_NAME3,
|
|
@@ -3247,7 +3426,7 @@ var DbRoleRepository = class extends BaseReldbCrudRepository {
|
|
|
3247
3426
|
};
|
|
3248
3427
|
|
|
3249
3428
|
// src/authz/iam-authz-functions.ts
|
|
3250
|
-
var
|
|
3429
|
+
var logger9 = core.logger.child({ module: "iam", scope: "authz" });
|
|
3251
3430
|
async function createAuthzOperations(deps) {
|
|
3252
3431
|
try {
|
|
3253
3432
|
const { config, session } = deps;
|
|
@@ -3269,10 +3448,23 @@ async function createAuthzOperations(deps) {
|
|
|
3269
3448
|
userRoleRepository: userRoleResult.data,
|
|
3270
3449
|
session
|
|
3271
3450
|
});
|
|
3272
|
-
|
|
3273
|
-
return ok(
|
|
3451
|
+
logger9.info("Authz sub-feature initialized");
|
|
3452
|
+
return ok({
|
|
3453
|
+
...manager,
|
|
3454
|
+
assignRole: guardIamMutation(manager.assignRole, 2),
|
|
3455
|
+
removeRole: guardIamMutation(manager.removeRole, 2),
|
|
3456
|
+
syncRoles: guardIamMutation(manager.syncRoles, 2),
|
|
3457
|
+
createRole: guardIamMutation(manager.createRole, 1),
|
|
3458
|
+
updateRole: guardIamMutation(manager.updateRole, 2),
|
|
3459
|
+
deleteRole: guardIamMutation(manager.deleteRole, 1),
|
|
3460
|
+
createPermission: guardIamMutation(manager.createPermission, 1),
|
|
3461
|
+
deletePermission: guardIamMutation(manager.deletePermission, 1),
|
|
3462
|
+
assignPermissionToRole: guardIamMutation(manager.assignPermissionToRole, 2),
|
|
3463
|
+
removePermissionFromRole: guardIamMutation(manager.removePermissionFromRole, 2),
|
|
3464
|
+
setPermissionSensitiveOperations: guardIamMutation(manager.setPermissionSensitiveOperations)
|
|
3465
|
+
});
|
|
3274
3466
|
} catch (error) {
|
|
3275
|
-
|
|
3467
|
+
logger9.error("Authz sub-feature initialization failed", { error });
|
|
3276
3468
|
return err(
|
|
3277
3469
|
HaiIamError.CONFIG_ERROR,
|
|
3278
3470
|
iamM("iam_initComponentFailed"),
|
|
@@ -3289,18 +3481,6 @@ function createRbacManager(config) {
|
|
|
3289
3481
|
userRoleRepository,
|
|
3290
3482
|
session
|
|
3291
3483
|
} = config;
|
|
3292
|
-
let superAdminRoleId;
|
|
3293
|
-
async function resolveSuperAdminRoleId() {
|
|
3294
|
-
if (superAdminRoleId !== void 0) {
|
|
3295
|
-
return ok(superAdminRoleId);
|
|
3296
|
-
}
|
|
3297
|
-
const roleResult = await roleRepository.findByCode(rbacConfig.superAdminRole);
|
|
3298
|
-
if (!roleResult.success) {
|
|
3299
|
-
return mapRepositoryError2("iam_queryRoleFailed", roleResult.error.message);
|
|
3300
|
-
}
|
|
3301
|
-
superAdminRoleId = roleResult.data?.id ?? null;
|
|
3302
|
-
return ok(superAdminRoleId);
|
|
3303
|
-
}
|
|
3304
3484
|
function mapRepositoryError2(messageKey, message) {
|
|
3305
3485
|
return err(
|
|
3306
3486
|
HaiIamError.REPOSITORY_ERROR,
|
|
@@ -3317,16 +3497,6 @@ function createRbacManager(config) {
|
|
|
3317
3497
|
}
|
|
3318
3498
|
return false;
|
|
3319
3499
|
}
|
|
3320
|
-
async function hasPermissionInRoles(roleIds, permission) {
|
|
3321
|
-
const codesResult = await rolePermissionRepository.getPermissionCodesForRoles(roleIds);
|
|
3322
|
-
if (!codesResult.success)
|
|
3323
|
-
return codesResult;
|
|
3324
|
-
for (const code of codesResult.data) {
|
|
3325
|
-
if (matchesPermission(permission, code))
|
|
3326
|
-
return ok(true);
|
|
3327
|
-
}
|
|
3328
|
-
return ok(false);
|
|
3329
|
-
}
|
|
3330
3500
|
async function getUserPermissionsInternal(userId) {
|
|
3331
3501
|
const roleIdsResult = await userRoleRepository.getRoleIds(userId);
|
|
3332
3502
|
if (!roleIdsResult.success)
|
|
@@ -3368,19 +3538,19 @@ function createRbacManager(config) {
|
|
|
3368
3538
|
async function syncSessionPermissionsForRole(roleId) {
|
|
3369
3539
|
const userIdsResult = await userRoleRepository.getUserIdsByRoleId(roleId);
|
|
3370
3540
|
if (!userIdsResult.success) {
|
|
3371
|
-
|
|
3541
|
+
logger9.error("Failed to query users for permission sync", { roleId, error: userIdsResult.error.message });
|
|
3372
3542
|
return;
|
|
3373
3543
|
}
|
|
3374
3544
|
await Promise.allSettled(
|
|
3375
3545
|
userIdsResult.data.map(async (userId) => {
|
|
3376
3546
|
const permResult = await resolveUserPermissionCodes(userId);
|
|
3377
3547
|
if (!permResult.success) {
|
|
3378
|
-
|
|
3548
|
+
logger9.error("Failed to resolve permissions for session sync", { userId, roleId, error: permResult.error.message });
|
|
3379
3549
|
return;
|
|
3380
3550
|
}
|
|
3381
3551
|
const syncResult = await session.patchUserSessions(userId, { permissions: permResult.data });
|
|
3382
3552
|
if (!syncResult.success) {
|
|
3383
|
-
|
|
3553
|
+
logger9.error("Failed to sync session permissions", { userId, roleId, error: syncResult.error.message });
|
|
3384
3554
|
}
|
|
3385
3555
|
})
|
|
3386
3556
|
);
|
|
@@ -3394,17 +3564,17 @@ function createRbacManager(config) {
|
|
|
3394
3564
|
if (roleCodesResult.success) {
|
|
3395
3565
|
updates.roles = roleCodesResult.data;
|
|
3396
3566
|
} else {
|
|
3397
|
-
|
|
3567
|
+
logger9.error("Failed to resolve roles for session sync", { userId, error: roleCodesResult.error.message });
|
|
3398
3568
|
}
|
|
3399
3569
|
if (permCodesResult.success) {
|
|
3400
3570
|
updates.permissions = permCodesResult.data;
|
|
3401
3571
|
} else {
|
|
3402
|
-
|
|
3572
|
+
logger9.error("Failed to resolve permissions for session sync", { userId, error: permCodesResult.error.message });
|
|
3403
3573
|
}
|
|
3404
3574
|
if (updates.roles !== void 0 || updates.permissions !== void 0) {
|
|
3405
3575
|
const syncResult = await session.patchUserSessions(userId, updates);
|
|
3406
3576
|
if (!syncResult.success) {
|
|
3407
|
-
|
|
3577
|
+
logger9.error("Failed to patch user sessions", { userId, error: syncResult.error.message });
|
|
3408
3578
|
}
|
|
3409
3579
|
}
|
|
3410
3580
|
}
|
|
@@ -3427,38 +3597,42 @@ function createRbacManager(config) {
|
|
|
3427
3597
|
}
|
|
3428
3598
|
return ok(void 0);
|
|
3429
3599
|
}
|
|
3600
|
+
async function getUserAuthorization(userId) {
|
|
3601
|
+
return readIamCache(`authorization:${userId}`, async () => {
|
|
3602
|
+
const rows = await reldb.sql.query("SELECT r.code AS role_code, p.code AS permission_code FROM hai_iam_user_roles ur JOIN hai_iam_roles r ON r.id = ur.role_id LEFT JOIN hai_iam_role_permissions rp ON rp.role_id = r.id LEFT JOIN hai_iam_permissions p ON p.id = rp.permission_id WHERE ur.user_id = ?", [userId]);
|
|
3603
|
+
return rows.success ? ok({ roles: [...new Set(rows.data.map((row) => row.role_code))], permissions: [...new Set(rows.data.flatMap((row) => row.permission_code ? [row.permission_code] : []))] }) : rows;
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3430
3606
|
return {
|
|
3607
|
+
getSensitiveOperations: (operation) => rbacConfig.enabled ? readIamCache(`sensitive:${operation ?? "*"}`, () => permissionRepository.getSensitiveOperations(operation)) : Promise.resolve(ok([])),
|
|
3608
|
+
async setPermissionSensitiveOperations(permissionId, operations) {
|
|
3609
|
+
if (!Array.isArray(operations) || operations.length > 1e3 || operations.some((value) => typeof value !== "string" || !value.trim() || value.length > 128))
|
|
3610
|
+
return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sensitiveOperationsInvalid"));
|
|
3611
|
+
const updated = await permissionRepository.updateById(permissionId, { sensitiveOperations: [...new Set(operations.map((value) => value.trim()))] });
|
|
3612
|
+
if (!updated.success)
|
|
3613
|
+
return updated;
|
|
3614
|
+
if (updated.data.changes === 0) {
|
|
3615
|
+
const existing = await permissionRepository.findById(permissionId);
|
|
3616
|
+
if (!existing.success)
|
|
3617
|
+
return existing;
|
|
3618
|
+
if (!existing.data)
|
|
3619
|
+
return err(HaiIamError.PERMISSION_NOT_FOUND, iamM("iam_permissionNotExist"));
|
|
3620
|
+
}
|
|
3621
|
+
return ok(void 0);
|
|
3622
|
+
},
|
|
3431
3623
|
async checkPermission(userId, permission) {
|
|
3432
3624
|
if (!rbacConfig.enabled) {
|
|
3433
3625
|
return ok(true);
|
|
3434
3626
|
}
|
|
3435
|
-
const
|
|
3436
|
-
if (!
|
|
3437
|
-
return
|
|
3438
|
-
|
|
3439
|
-
if (roleIds.length === 0) {
|
|
3440
|
-
return ok(false);
|
|
3441
|
-
}
|
|
3442
|
-
const superAdminResult = await resolveSuperAdminRoleId();
|
|
3443
|
-
if (!superAdminResult.success)
|
|
3444
|
-
return superAdminResult;
|
|
3445
|
-
if (superAdminResult.data && roleIds.includes(superAdminResult.data)) {
|
|
3446
|
-
return ok(true);
|
|
3447
|
-
}
|
|
3448
|
-
return hasPermissionInRoles(roleIds, permission);
|
|
3627
|
+
const authorization = await getUserAuthorization(userId);
|
|
3628
|
+
if (!authorization.success)
|
|
3629
|
+
return authorization;
|
|
3630
|
+
return ok(authorization.data.roles.includes(rbacConfig.superAdminRole) || authorization.data.permissions.some((code) => matchesPermission(permission, code)));
|
|
3449
3631
|
},
|
|
3450
3632
|
async getUserPermissions(userId) {
|
|
3451
3633
|
return getUserPermissionsInternal(userId);
|
|
3452
3634
|
},
|
|
3453
|
-
|
|
3454
|
-
const roles = await userRoleRepository.getRoleSummaries(userId);
|
|
3455
|
-
if (!roles.success)
|
|
3456
|
-
return roles;
|
|
3457
|
-
const permissions = roles.data.length ? await rolePermissionRepository.getPermissionCodesForRoles(roles.data.map((role) => role.id)) : ok([]);
|
|
3458
|
-
if (!permissions.success)
|
|
3459
|
-
return permissions;
|
|
3460
|
-
return ok({ roles: roles.data.map((role) => role.code), permissions: permissions.data });
|
|
3461
|
-
},
|
|
3635
|
+
getUserAuthorization,
|
|
3462
3636
|
async getUserRoles(userId) {
|
|
3463
3637
|
return userRoleRepository.getRoles(userId);
|
|
3464
3638
|
},
|
|
@@ -3472,7 +3646,7 @@ function createRbacManager(config) {
|
|
|
3472
3646
|
}
|
|
3473
3647
|
const result = await userRoleRepository.assign(userId, roleId, tx);
|
|
3474
3648
|
if (result.success) {
|
|
3475
|
-
|
|
3649
|
+
logger9.info("Role assigned to user", { userId, roleId });
|
|
3476
3650
|
if (!tx) {
|
|
3477
3651
|
await syncUserSessionAfterRoleChange(userId);
|
|
3478
3652
|
}
|
|
@@ -3482,7 +3656,7 @@ function createRbacManager(config) {
|
|
|
3482
3656
|
async removeRole(userId, roleId, tx) {
|
|
3483
3657
|
const result = await userRoleRepository.remove(userId, roleId, tx);
|
|
3484
3658
|
if (result.success) {
|
|
3485
|
-
|
|
3659
|
+
logger9.info("Role removed from user", { userId, roleId });
|
|
3486
3660
|
if (!tx) {
|
|
3487
3661
|
await syncUserSessionAfterRoleChange(userId);
|
|
3488
3662
|
}
|
|
@@ -3550,7 +3724,7 @@ function createRbacManager(config) {
|
|
|
3550
3724
|
error
|
|
3551
3725
|
);
|
|
3552
3726
|
}
|
|
3553
|
-
|
|
3727
|
+
logger9.info("Roles synced for user", { userId, added: toAdd.length, removed: toRemove.length });
|
|
3554
3728
|
if (ownTx) {
|
|
3555
3729
|
await syncUserSessionAfterRoleChange(userId);
|
|
3556
3730
|
}
|
|
@@ -3602,10 +3776,7 @@ function createRbacManager(config) {
|
|
|
3602
3776
|
return mapRepositoryError2("iam_createRoleFailed", commitResult.error.message);
|
|
3603
3777
|
}
|
|
3604
3778
|
}
|
|
3605
|
-
|
|
3606
|
-
if (role.code === rbacConfig.superAdminRole) {
|
|
3607
|
-
superAdminRoleId = void 0;
|
|
3608
|
-
}
|
|
3779
|
+
logger9.info("Role created", { roleId: createdResult.data.id, code: role.code });
|
|
3609
3780
|
return ok(createdResult.data);
|
|
3610
3781
|
} catch (error) {
|
|
3611
3782
|
if (ownTx)
|
|
@@ -3691,7 +3862,6 @@ function createRbacManager(config) {
|
|
|
3691
3862
|
return mapRepositoryError2("iam_updateRoleFailed", commitResult.error.message);
|
|
3692
3863
|
}
|
|
3693
3864
|
}
|
|
3694
|
-
superAdminRoleId = void 0;
|
|
3695
3865
|
if (ownTx) {
|
|
3696
3866
|
const affectedUsersResult = await userRoleRepository.getUserIdsByRoleId(roleId);
|
|
3697
3867
|
if (affectedUsersResult.success) {
|
|
@@ -3699,7 +3869,7 @@ function createRbacManager(config) {
|
|
|
3699
3869
|
affectedUsersResult.data.map((userId) => syncUserSessionAfterRoleChange(userId))
|
|
3700
3870
|
);
|
|
3701
3871
|
} else {
|
|
3702
|
-
|
|
3872
|
+
logger9.error("Failed to query affected users after updateRole", { roleId, error: affectedUsersResult.error.message });
|
|
3703
3873
|
}
|
|
3704
3874
|
}
|
|
3705
3875
|
return ok(updatedResult.data);
|
|
@@ -3773,12 +3943,15 @@ function createRbacManager(config) {
|
|
|
3773
3943
|
affectedUserIds.map((userId) => syncUserSessionAfterRoleChange(userId))
|
|
3774
3944
|
);
|
|
3775
3945
|
}
|
|
3776
|
-
|
|
3777
|
-
logger8.info("Role deleted", { roleId });
|
|
3946
|
+
logger9.info("Role deleted", { roleId });
|
|
3778
3947
|
return ok(void 0);
|
|
3779
3948
|
},
|
|
3780
3949
|
// ─── 权限管理 ───
|
|
3781
3950
|
async createPermission(permission, tx) {
|
|
3951
|
+
const operations = permission.sensitiveOperations ?? [];
|
|
3952
|
+
if (!Array.isArray(operations) || operations.length > 1e3 || operations.some((value) => typeof value !== "string" || !value.trim() || value.length > 128))
|
|
3953
|
+
return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sensitiveOperationsInvalid"));
|
|
3954
|
+
permission = { ...permission, sensitiveOperations: [...new Set(operations.map((value) => value.trim()))] };
|
|
3782
3955
|
const ownTx = !tx;
|
|
3783
3956
|
if (!tx) {
|
|
3784
3957
|
const txResult = await reldb.tx.begin();
|
|
@@ -3814,7 +3987,7 @@ function createRbacManager(config) {
|
|
|
3814
3987
|
return mapRepositoryError2("iam_createPermissionFailed", commitResult.error.message);
|
|
3815
3988
|
}
|
|
3816
3989
|
}
|
|
3817
|
-
|
|
3990
|
+
logger9.info("Permission created", { permissionId: createdResult.data.id, code: permission.code });
|
|
3818
3991
|
return ok(createdResult.data);
|
|
3819
3992
|
} catch (error) {
|
|
3820
3993
|
if (ownTx)
|
|
@@ -3918,7 +4091,7 @@ function createRbacManager(config) {
|
|
|
3918
4091
|
affectedRoleIds.map((roleId) => syncSessionPermissionsForRole(roleId))
|
|
3919
4092
|
);
|
|
3920
4093
|
}
|
|
3921
|
-
|
|
4094
|
+
logger9.info("Permission deleted", { permissionId });
|
|
3922
4095
|
return ok(void 0);
|
|
3923
4096
|
},
|
|
3924
4097
|
async assignPermissionToRole(roleId, permissionId, tx) {
|
|
@@ -3940,7 +4113,7 @@ function createRbacManager(config) {
|
|
|
3940
4113
|
}
|
|
3941
4114
|
const assignResult = await rolePermissionRepository.assign(roleId, permissionId, tx);
|
|
3942
4115
|
if (assignResult.success) {
|
|
3943
|
-
|
|
4116
|
+
logger9.info("Permission assigned to role", { roleId, permissionId });
|
|
3944
4117
|
if (!tx) {
|
|
3945
4118
|
await syncSessionPermissionsForRole(roleId);
|
|
3946
4119
|
}
|
|
@@ -3969,7 +4142,7 @@ function createRbacManager(config) {
|
|
|
3969
4142
|
}
|
|
3970
4143
|
const removeResult = await rolePermissionRepository.remove(roleId, permissionId, tx);
|
|
3971
4144
|
if (removeResult.success) {
|
|
3972
|
-
|
|
4145
|
+
logger9.info("Permission removed from role", { roleId, permissionId });
|
|
3973
4146
|
if (!tx) {
|
|
3974
4147
|
await syncSessionPermissionsForRole(roleId);
|
|
3975
4148
|
}
|
|
@@ -3987,14 +4160,16 @@ function createRbacManager(config) {
|
|
|
3987
4160
|
}
|
|
3988
4161
|
};
|
|
3989
4162
|
}
|
|
3990
|
-
var
|
|
4163
|
+
var logger10 = core.logger.child({ module: "iam", scope: "audit" });
|
|
3991
4164
|
async function observe(operation, event, tx) {
|
|
3992
4165
|
const result = await operation();
|
|
4166
|
+
if (!audit.isInitialized)
|
|
4167
|
+
return result;
|
|
3993
4168
|
const fields = typeof event === "function" ? event(result) : event;
|
|
3994
4169
|
if (!fields)
|
|
3995
4170
|
return result;
|
|
3996
4171
|
if (tx && !result.success) {
|
|
3997
|
-
|
|
4172
|
+
logger10.warn("IAM transactional operation failed", { action: fields.action, resource: fields.resource, reasonCode: result.error.code });
|
|
3998
4173
|
return result;
|
|
3999
4174
|
}
|
|
4000
4175
|
const recorded = await audit.capture({
|
|
@@ -4004,7 +4179,7 @@ async function observe(operation, event, tx) {
|
|
|
4004
4179
|
details: { ...fields.details, ...!result.success ? { reasonCode: result.error.code } : {} }
|
|
4005
4180
|
}, { tx });
|
|
4006
4181
|
if (!recorded.success) {
|
|
4007
|
-
|
|
4182
|
+
logger10.error("IAM event audit failed", { action: fields.action, resource: fields.resource, error: recorded.error });
|
|
4008
4183
|
if (tx)
|
|
4009
4184
|
return recorded;
|
|
4010
4185
|
}
|
|
@@ -4065,6 +4240,7 @@ function observeAuthorization(operations) {
|
|
|
4065
4240
|
updateRole: (id, data, tx) => observe(() => operations.updateRole(id, data, tx), { action: "role.update", resource: "iam.roles", resourceId: id, details: { fields: Object.keys(data), permissionIds: data.permissionIds } }, tx),
|
|
4066
4241
|
deleteRole: (id, tx) => observe(() => operations.deleteRole(id, tx), { action: "role.delete", resource: "iam.roles", resourceId: id }, tx),
|
|
4067
4242
|
createPermission: (input, tx) => observe(() => operations.createPermission(input, tx), (result) => ({ action: "permission.create", resource: "iam.permissions", resourceId: result.success ? result.data.id : void 0, details: { code: input.code } }), tx),
|
|
4243
|
+
setPermissionSensitiveOperations: (id, operationIds) => observe(() => operations.setPermissionSensitiveOperations(id, operationIds), { action: "permission.sensitive-operations", resource: "iam.permissions", resourceId: id, details: { operations: operationIds } }),
|
|
4068
4244
|
deletePermission: (id, tx) => observe(() => operations.deletePermission(id, tx), { action: "permission.delete", resource: "iam.permissions", resourceId: id }, tx),
|
|
4069
4245
|
assignPermissionToRole: (id, permissionId, tx) => observe(() => operations.assignPermissionToRole(id, permissionId, tx), { action: "permission.assign", resource: "iam.role-permissions", resourceId: id, details: { permissionId } }, tx),
|
|
4070
4246
|
removePermissionFromRole: (id, permissionId, tx) => observe(() => operations.removePermissionFromRole(id, permissionId, tx), { action: "permission.remove", resource: "iam.role-permissions", resourceId: id, details: { permissionId } }, tx)
|
|
@@ -4151,6 +4327,17 @@ var steps = [{
|
|
|
4151
4327
|
const added = await sql.execute("ALTER TABLE hai_iam_users ADD COLUMN first_login_failed_at TIMESTAMP");
|
|
4152
4328
|
return added.success ? ok(void 0) : added;
|
|
4153
4329
|
}
|
|
4330
|
+
}, {
|
|
4331
|
+
version: 5,
|
|
4332
|
+
name: "permission-sensitive-operations",
|
|
4333
|
+
async needed(sql) {
|
|
4334
|
+
const found = await schemaColumns(reldb, "hai_iam_permissions", sql);
|
|
4335
|
+
return found.success ? ok(found.data.length > 0 && !found.data.includes("sensitive_operations")) : found;
|
|
4336
|
+
},
|
|
4337
|
+
async apply(sql) {
|
|
4338
|
+
const added = await sql.execute("ALTER TABLE hai_iam_permissions ADD COLUMN sensitive_operations JSON");
|
|
4339
|
+
return added.success ? ok(void 0) : added;
|
|
4340
|
+
}
|
|
4154
4341
|
}];
|
|
4155
4342
|
function migrateIam(options = {}) {
|
|
4156
4343
|
return migrateSchema(reldb, "iam", steps, options);
|
|
@@ -4215,7 +4402,29 @@ function buildIamPublicConfig(config, runtime) {
|
|
|
4215
4402
|
initialCredentials: { enabled: Boolean(config.initialCredentials?.enabled && config.passwordReset?.enabled !== false && runtime.onPasswordResetRequest) }
|
|
4216
4403
|
};
|
|
4217
4404
|
}
|
|
4218
|
-
|
|
4405
|
+
async function readIamUserSecurity(repository, userId) {
|
|
4406
|
+
const found = await readIamCache(`user:${userId}`, async () => {
|
|
4407
|
+
const row = await repository.findById(userId);
|
|
4408
|
+
if (!row.success)
|
|
4409
|
+
return row;
|
|
4410
|
+
const user = row.data;
|
|
4411
|
+
return ok(user ? {
|
|
4412
|
+
username: user.username,
|
|
4413
|
+
displayName: user.displayName,
|
|
4414
|
+
avatarUrl: user.avatarUrl,
|
|
4415
|
+
enabled: user.enabled,
|
|
4416
|
+
accountType: user.accountType,
|
|
4417
|
+
accountStatus: user.accountStatus,
|
|
4418
|
+
accountExpiresAt: user.accountExpiresAt ? new Date(user.accountExpiresAt).getTime() : null,
|
|
4419
|
+
credentialVersion: user.credentialVersion ?? "",
|
|
4420
|
+
mustChangePassword: user.mustChangePassword,
|
|
4421
|
+
hasPassword: !!user.passwordHash,
|
|
4422
|
+
passwordUpdatedAt: user.passwordUpdatedAt?.getTime() ?? null
|
|
4423
|
+
} : null);
|
|
4424
|
+
});
|
|
4425
|
+
return found.success ? ok(found.data ? { ...found.data, accountExpiresAt: found.data.accountExpiresAt === null ? null : new Date(found.data.accountExpiresAt), passwordUpdatedAt: found.data.passwordUpdatedAt === null ? void 0 : new Date(found.data.passwordUpdatedAt), passwordHash: found.data.hasPassword ? "present" : void 0 } : null) : found;
|
|
4426
|
+
}
|
|
4427
|
+
var logger11 = core.logger.child({ module: "iam", scope: "seed" });
|
|
4219
4428
|
var DEFAULT_ROLES = [
|
|
4220
4429
|
{ code: "admin", name: () => iamM("iam_seedRoleAdminName"), description: () => iamM("iam_seedRoleAdminDesc"), isSystem: true },
|
|
4221
4430
|
{ code: "user", name: () => iamM("iam_seedRoleUserName"), description: () => iamM("iam_seedRoleUserDesc"), isSystem: true },
|
|
@@ -4259,12 +4468,11 @@ var USER_ROLE_PERMISSIONS = ["dashboard:view", "profile:read"];
|
|
|
4259
4468
|
async function seedIamData(authz) {
|
|
4260
4469
|
try {
|
|
4261
4470
|
const existingRoles = await authz.getAllRoles({ page: 1, pageSize: 1e3 });
|
|
4471
|
+
if (!existingRoles.success)
|
|
4472
|
+
return existingRoles;
|
|
4262
4473
|
const existingRoleMap = /* @__PURE__ */ new Map();
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
existingRoleMap.set(role.code, role.id);
|
|
4266
|
-
}
|
|
4267
|
-
}
|
|
4474
|
+
for (const role of existingRoles.data.items)
|
|
4475
|
+
existingRoleMap.set(role.code, role.id);
|
|
4268
4476
|
const roleMap = /* @__PURE__ */ new Map();
|
|
4269
4477
|
for (const role of DEFAULT_ROLES) {
|
|
4270
4478
|
const existingId = existingRoleMap.get(role.code);
|
|
@@ -4275,17 +4483,23 @@ async function seedIamData(authz) {
|
|
|
4275
4483
|
const result = await authz.createRole({ code: role.code, name: role.name(), description: role.description(), isSystem: role.isSystem });
|
|
4276
4484
|
if (result.success) {
|
|
4277
4485
|
roleMap.set(role.code, result.data.id);
|
|
4486
|
+
} else if (result.error.code === HaiIamError.ROLE_ALREADY_EXISTS.code) {
|
|
4487
|
+
const concurrent = await authz.getRoleByCode(role.code);
|
|
4488
|
+
if (!concurrent.success)
|
|
4489
|
+
return concurrent;
|
|
4490
|
+
if (!concurrent.data)
|
|
4491
|
+
return result;
|
|
4492
|
+
roleMap.set(role.code, concurrent.data.id);
|
|
4278
4493
|
} else {
|
|
4279
4494
|
return result;
|
|
4280
4495
|
}
|
|
4281
4496
|
}
|
|
4282
4497
|
const existingPerms = await authz.getAllPermissions({ page: 1, pageSize: 1e3 });
|
|
4498
|
+
if (!existingPerms.success)
|
|
4499
|
+
return existingPerms;
|
|
4283
4500
|
const existingPermMap = /* @__PURE__ */ new Map();
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
existingPermMap.set(perm.code, perm.id);
|
|
4287
|
-
}
|
|
4288
|
-
}
|
|
4501
|
+
for (const perm of existingPerms.data.items)
|
|
4502
|
+
existingPermMap.set(perm.code, perm.id);
|
|
4289
4503
|
const permMap = /* @__PURE__ */ new Map();
|
|
4290
4504
|
for (const perm of DEFAULT_PERMISSIONS) {
|
|
4291
4505
|
const existingId = existingPermMap.get(perm.code);
|
|
@@ -4296,6 +4510,13 @@ async function seedIamData(authz) {
|
|
|
4296
4510
|
const result = await authz.createPermission({ code: perm.code, name: perm.name(), type: perm.type, resource: perm.resource, action: perm.action });
|
|
4297
4511
|
if (result.success) {
|
|
4298
4512
|
permMap.set(perm.code, result.data.id);
|
|
4513
|
+
} else if (result.error.code === HaiIamError.PERMISSION_ALREADY_EXISTS.code) {
|
|
4514
|
+
const concurrent = await authz.getPermissionByCode(perm.code);
|
|
4515
|
+
if (!concurrent.success)
|
|
4516
|
+
return concurrent;
|
|
4517
|
+
if (!concurrent.data)
|
|
4518
|
+
return result;
|
|
4519
|
+
permMap.set(perm.code, concurrent.data.id);
|
|
4299
4520
|
} else {
|
|
4300
4521
|
return result;
|
|
4301
4522
|
}
|
|
@@ -4303,7 +4524,9 @@ async function seedIamData(authz) {
|
|
|
4303
4524
|
const adminRoleId = roleMap.get("admin");
|
|
4304
4525
|
if (adminRoleId) {
|
|
4305
4526
|
for (const [, permId] of permMap) {
|
|
4306
|
-
await authz.assignPermissionToRole(adminRoleId, permId);
|
|
4527
|
+
const assigned = await authz.assignPermissionToRole(adminRoleId, permId);
|
|
4528
|
+
if (!assigned.success)
|
|
4529
|
+
return assigned;
|
|
4307
4530
|
}
|
|
4308
4531
|
}
|
|
4309
4532
|
const userRoleId = roleMap.get("user");
|
|
@@ -4311,14 +4534,16 @@ async function seedIamData(authz) {
|
|
|
4311
4534
|
for (const permCode of USER_ROLE_PERMISSIONS) {
|
|
4312
4535
|
const permId = permMap.get(permCode);
|
|
4313
4536
|
if (permId) {
|
|
4314
|
-
await authz.assignPermissionToRole(userRoleId, permId);
|
|
4537
|
+
const assigned = await authz.assignPermissionToRole(userRoleId, permId);
|
|
4538
|
+
if (!assigned.success)
|
|
4539
|
+
return assigned;
|
|
4315
4540
|
}
|
|
4316
4541
|
}
|
|
4317
4542
|
}
|
|
4318
|
-
|
|
4543
|
+
logger11.info("IAM seed data initialized");
|
|
4319
4544
|
return ok(void 0);
|
|
4320
4545
|
} catch (error) {
|
|
4321
|
-
|
|
4546
|
+
logger11.error("Failed to seed IAM data", { error });
|
|
4322
4547
|
return err(
|
|
4323
4548
|
HaiIamError.REPOSITORY_ERROR,
|
|
4324
4549
|
iamM("iam_initSeedDataFailed"),
|
|
@@ -4331,7 +4556,7 @@ function retainedUntil(entry) {
|
|
|
4331
4556
|
}
|
|
4332
4557
|
function restoreSession(session) {
|
|
4333
4558
|
const result = { ...session, createdAt: new Date(session.createdAt), startedAt: new Date(session.startedAt ?? session.createdAt), lastActiveAt: new Date(session.lastActiveAt), expiresAt: new Date(session.expiresAt) };
|
|
4334
|
-
if (typeof result.credentialVersion !== "string" || !result.version || !Number.isSafeInteger(result.maxAge) || result.maxAge <= 0 || !Array.isArray(result.roles) || !result.roles.every((role) => typeof role === "string") || !Array.isArray(result.permissions) || !result.permissions.every((permission) => typeof permission === "string") || ![result.createdAt, result.startedAt, result.lastActiveAt, result.expiresAt].every((date) => Number.isFinite(date.getTime()))) {
|
|
4559
|
+
if (result.terminalType !== void 0 && !["web", "app", "desktop", "api"].includes(result.terminalType) || typeof result.credentialVersion !== "string" || !result.version || !Number.isSafeInteger(result.maxAge) || result.maxAge <= 0 || !Array.isArray(result.roles) || !result.roles.every((role) => typeof role === "string") || !Array.isArray(result.permissions) || !result.permissions.every((permission) => typeof permission === "string") || ![result.createdAt, result.startedAt, result.lastActiveAt, result.expiresAt].every((date) => Number.isFinite(date.getTime()))) {
|
|
4335
4560
|
throw new TypeError("Invalid session state");
|
|
4336
4561
|
}
|
|
4337
4562
|
return result;
|
|
@@ -4410,12 +4635,29 @@ function createCacheSessionRepository(refreshTokenMaxAge, policy = {}) {
|
|
|
4410
4635
|
}
|
|
4411
4636
|
return {
|
|
4412
4637
|
save: (session, tokens, singleDevice = false) => mutate(getUserSessionScope(session.userId), (state) => {
|
|
4413
|
-
const
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4638
|
+
const active = Object.entries(state.sessions).filter(([, entry]) => entry.session.credentialVersion === session.credentialVersion);
|
|
4639
|
+
const terminal = session.terminalType ?? "web";
|
|
4640
|
+
const limits = policy.terminalLimits ?? {};
|
|
4641
|
+
const cap = limits[terminal];
|
|
4642
|
+
if (Object.keys(limits).length && cap === void 0)
|
|
4417
4643
|
return err(HaiIamError.SESSION_LIMIT_EXCEEDED, iamM("iam_sessionLimitExceeded"));
|
|
4644
|
+
const removed = /* @__PURE__ */ new Set();
|
|
4645
|
+
if (singleDevice) {
|
|
4646
|
+
if (policy.singleDeviceMode === "reject" && active.length)
|
|
4647
|
+
return err(HaiIamError.SESSION_LIMIT_EXCEEDED, iamM("iam_sessionLimitExceeded"));
|
|
4648
|
+
if (policy.singleDeviceMode !== "reject")
|
|
4649
|
+
Object.keys(state.sessions).forEach((token) => removed.add(token));
|
|
4650
|
+
} else if (cap) {
|
|
4651
|
+
const same = active.filter(([, entry]) => (entry.session.terminalType ?? "web") === terminal).sort(([firstToken, first], [secondToken, second]) => new Date(first.session.startedAt ?? first.session.createdAt).getTime() - new Date(second.session.startedAt ?? second.session.createdAt).getTime() || firstToken.localeCompare(secondToken));
|
|
4652
|
+
if (same.length >= cap) {
|
|
4653
|
+
if (policy.terminalLimitMode !== "replace")
|
|
4654
|
+
return err(HaiIamError.SESSION_LIMIT_EXCEEDED, iamM("iam_sessionLimitExceeded"));
|
|
4655
|
+
same.slice(0, same.length - cap + 1).forEach(([token]) => removed.add(token));
|
|
4656
|
+
}
|
|
4418
4657
|
}
|
|
4658
|
+
if (maxSessions > 0 && active.filter(([token]) => !removed.has(token)).length >= maxSessions)
|
|
4659
|
+
return err(HaiIamError.SESSION_LIMIT_EXCEEDED, iamM("iam_sessionLimitExceeded"));
|
|
4660
|
+
removed.forEach((token) => delete state.sessions[token]);
|
|
4419
4661
|
return storeSession(state, session, tokens);
|
|
4420
4662
|
}),
|
|
4421
4663
|
rotate: (previous, refreshToken, session, tokens) => mutate(getUserSessionScope(previous.userId), (state) => {
|
|
@@ -4466,6 +4708,8 @@ function createCacheSessionRepository(refreshTokenMaxAge, policy = {}) {
|
|
|
4466
4708
|
return err(HaiIamError.SESSION_NOT_FOUND, iamM("iam_sessionNotExist"));
|
|
4467
4709
|
if (!expectedVersion || entry.session.version !== expectedVersion)
|
|
4468
4710
|
return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
|
|
4711
|
+
if (patch.terminalType !== void 0 && patch.terminalType !== (entry.session.terminalType ?? "web"))
|
|
4712
|
+
return err(HaiIamError.CONFLICT, iamM("iam_sessionStateConflict"));
|
|
4469
4713
|
const updated = applySessionPatch(restoreSession(entry.session), patch);
|
|
4470
4714
|
if (![updated.expiresAt, updated.lastActiveAt].every((date) => Number.isFinite(date.getTime())))
|
|
4471
4715
|
return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sessionMaxAgeInvalid"));
|
|
@@ -4545,7 +4789,7 @@ function stripInternalSessionFields(session) {
|
|
|
4545
4789
|
const hasExtraKeys = Object.keys(rest).length > 0;
|
|
4546
4790
|
return { ...session, data: hasExtraKeys ? rest : void 0 };
|
|
4547
4791
|
}
|
|
4548
|
-
var
|
|
4792
|
+
var logger12 = core.logger.child({ module: "iam", scope: "session" });
|
|
4549
4793
|
async function createSessionOperations(deps) {
|
|
4550
4794
|
try {
|
|
4551
4795
|
const { config } = deps;
|
|
@@ -4563,10 +4807,10 @@ async function createSessionOperations(deps) {
|
|
|
4563
4807
|
sessionRepository,
|
|
4564
4808
|
apiKeyRepository: config.login.apikey ? await createDbApiKeyRepository() : void 0
|
|
4565
4809
|
});
|
|
4566
|
-
|
|
4810
|
+
logger12.info("Session sub-feature initialized");
|
|
4567
4811
|
return ok(functions);
|
|
4568
4812
|
} catch (error) {
|
|
4569
|
-
|
|
4813
|
+
logger12.error("Session sub-feature initialization failed", { error });
|
|
4570
4814
|
return err(
|
|
4571
4815
|
HaiIamError.CONFIG_ERROR,
|
|
4572
4816
|
iamM("iam_initComponentFailed"),
|
|
@@ -4580,6 +4824,8 @@ function buildSessionFunctions(config) {
|
|
|
4580
4824
|
const singleDevice = config.singleDevice ?? false;
|
|
4581
4825
|
const repo = config.sessionRepository;
|
|
4582
4826
|
function candidate(options, credentialVersion) {
|
|
4827
|
+
if (options.terminalType !== void 0 && !["web", "app", "desktop", "api"].includes(options.terminalType))
|
|
4828
|
+
return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_terminalTypeInvalid"));
|
|
4583
4829
|
const now = /* @__PURE__ */ new Date();
|
|
4584
4830
|
const ttl = options.maxAge ?? maxAge;
|
|
4585
4831
|
if (!Number.isSafeInteger(ttl) || ttl <= 0 || !Number.isFinite(new Date(now.getTime() + ttl * 1e3).getTime()))
|
|
@@ -4599,7 +4845,7 @@ function buildSessionFunctions(config) {
|
|
|
4599
4845
|
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
4600
4846
|
if (typeof current.data.version !== "string" || expected !== void 0 && current.data.version !== expected)
|
|
4601
4847
|
return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
|
|
4602
|
-
return ok(current.data
|
|
4848
|
+
return ok(current.data);
|
|
4603
4849
|
} catch {
|
|
4604
4850
|
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_sessionExpired"));
|
|
4605
4851
|
}
|
|
@@ -4609,7 +4855,10 @@ function buildSessionFunctions(config) {
|
|
|
4609
4855
|
return ok(session);
|
|
4610
4856
|
if (!config.apiKeyRepository)
|
|
4611
4857
|
return err(HaiIamError.APIKEY_INVALID, iamM("iam_apikeyInvalid"));
|
|
4612
|
-
const found = await
|
|
4858
|
+
const found = await readIamCache(`apikey:${session.apiKeyId}`, async () => {
|
|
4859
|
+
const row = await config.apiKeyRepository.findOneById(session.apiKeyId);
|
|
4860
|
+
return row.success ? ok(row.data ? { userId: row.data.userId, enabled: row.data.enabled, expiresAt: row.data.expiresAt?.getTime() ?? null, scopes: row.data.scopes } : null) : row;
|
|
4861
|
+
});
|
|
4613
4862
|
if (!found.success)
|
|
4614
4863
|
return found;
|
|
4615
4864
|
const key = found.data;
|
|
@@ -4617,7 +4866,7 @@ function buildSessionFunctions(config) {
|
|
|
4617
4866
|
return err(HaiIamError.APIKEY_INVALID, iamM("iam_apikeyInvalid"));
|
|
4618
4867
|
if (!key.enabled)
|
|
4619
4868
|
return err(HaiIamError.APIKEY_DISABLED, iamM("iam_apikeyDisabled"));
|
|
4620
|
-
if (key.expiresAt &&
|
|
4869
|
+
if (key.expiresAt && Date.now() >= key.expiresAt)
|
|
4621
4870
|
return err(HaiIamError.APIKEY_EXPIRED, iamM("iam_apikeyExpired"));
|
|
4622
4871
|
return ok(applyApiKeyScopes({ ...session, apiKeyScopes: key.scopes }, key.scopes));
|
|
4623
4872
|
}
|
|
@@ -4627,7 +4876,7 @@ function buildSessionFunctions(config) {
|
|
|
4627
4876
|
const bound = await credential(options.userId, options.expectedCredentialVersion);
|
|
4628
4877
|
if (!bound.success)
|
|
4629
4878
|
return bound;
|
|
4630
|
-
const built = candidate(options, bound.data);
|
|
4879
|
+
const built = candidate(options, bound.data.version);
|
|
4631
4880
|
if (!built.success)
|
|
4632
4881
|
return built;
|
|
4633
4882
|
const { session, tokens } = built.data;
|
|
@@ -4635,7 +4884,7 @@ function buildSessionFunctions(config) {
|
|
|
4635
4884
|
if (!saveResult.success) {
|
|
4636
4885
|
return saveResult;
|
|
4637
4886
|
}
|
|
4638
|
-
|
|
4887
|
+
logger12.debug("Session created", { userId: options.userId });
|
|
4639
4888
|
return ok(session);
|
|
4640
4889
|
} catch (error) {
|
|
4641
4890
|
return err(
|
|
@@ -4666,7 +4915,7 @@ function buildSessionFunctions(config) {
|
|
|
4666
4915
|
const bound = await credential(session.userId, session.credentialVersion);
|
|
4667
4916
|
if (!bound.success)
|
|
4668
4917
|
return bound;
|
|
4669
|
-
const validated = await validateApiKey(session);
|
|
4918
|
+
const validated = await validateApiKey({ ...session, ...bound.data.profile });
|
|
4670
4919
|
if (!validated.success)
|
|
4671
4920
|
return validated;
|
|
4672
4921
|
let output = validated.data;
|
|
@@ -4696,7 +4945,7 @@ function buildSessionFunctions(config) {
|
|
|
4696
4945
|
return repo.updateByAccessToken(accessToken, data, expectedVersion);
|
|
4697
4946
|
},
|
|
4698
4947
|
async delete(accessToken) {
|
|
4699
|
-
|
|
4948
|
+
logger12.debug("Session delete requested");
|
|
4700
4949
|
return repo.removeByAccessToken(accessToken);
|
|
4701
4950
|
},
|
|
4702
4951
|
async deleteByUserId(userId) {
|
|
@@ -4738,6 +4987,7 @@ function buildSessionFunctions(config) {
|
|
|
4738
4987
|
apiKeyId: effectiveSession.apiKeyId,
|
|
4739
4988
|
apiKeyScopes: effectiveSession.apiKeyScopes,
|
|
4740
4989
|
source: oldSession.source,
|
|
4990
|
+
terminalType: oldSession.terminalType ?? "web",
|
|
4741
4991
|
maxAge: oldSession.maxAge,
|
|
4742
4992
|
data: oldSession.data ? { ...oldSession.data, _tokenPair: void 0 } : void 0
|
|
4743
4993
|
}, oldSession.credentialVersion);
|
|
@@ -4749,7 +4999,7 @@ function buildSessionFunctions(config) {
|
|
|
4749
4999
|
const current = await credential(oldSession.userId, oldSession.credentialVersion);
|
|
4750
5000
|
if (!current.success)
|
|
4751
5001
|
return current;
|
|
4752
|
-
|
|
5002
|
+
logger12.debug("Token refreshed", { userId: oldSession.userId });
|
|
4753
5003
|
return ok(next.data.tokens);
|
|
4754
5004
|
} catch (error) {
|
|
4755
5005
|
return err(
|
|
@@ -4767,7 +5017,7 @@ function buildSessionFunctions(config) {
|
|
|
4767
5017
|
}
|
|
4768
5018
|
};
|
|
4769
5019
|
}
|
|
4770
|
-
var
|
|
5020
|
+
var logger13 = core.logger.child({ module: "iam", scope: "user" });
|
|
4771
5021
|
var USER_SORT_COLUMNS = {
|
|
4772
5022
|
/** 用户名列。 */
|
|
4773
5023
|
username: "username",
|
|
@@ -4792,10 +5042,10 @@ async function createUserOperations(deps) {
|
|
|
4792
5042
|
config,
|
|
4793
5043
|
onPasswordResetRequest
|
|
4794
5044
|
});
|
|
4795
|
-
|
|
5045
|
+
logger13.info("User sub-feature initialized");
|
|
4796
5046
|
return ok(functions);
|
|
4797
5047
|
} catch (error) {
|
|
4798
|
-
|
|
5048
|
+
logger13.error("User sub-feature initialization failed", { error });
|
|
4799
5049
|
return err(
|
|
4800
5050
|
HaiIamError.CONFIG_ERROR,
|
|
4801
5051
|
iamM("iam_initComponentFailed"),
|
|
@@ -4964,7 +5214,7 @@ function buildRegistrationOps(ctx) {
|
|
|
4964
5214
|
await userRepository.deleteById(createdUser.id);
|
|
4965
5215
|
return assigned;
|
|
4966
5216
|
}
|
|
4967
|
-
|
|
5217
|
+
logger13.info("User registered", { userId: createdUser.id, username: options.username });
|
|
4968
5218
|
return ok({
|
|
4969
5219
|
user: toUser(createdUser),
|
|
4970
5220
|
agreements: buildAgreementDisplay(agreementConfig, agreementConfig.showOnRegister)
|
|
@@ -5213,7 +5463,7 @@ function buildUserMutationOps(ctx) {
|
|
|
5213
5463
|
if (tx) {
|
|
5214
5464
|
const revoked = await sessionFunctions.deleteByUserId(userId);
|
|
5215
5465
|
if (!revoked.success)
|
|
5216
|
-
|
|
5466
|
+
logger13.warn("Account credentials revoked; session cache cleanup failed", { userId, error: revoked.error });
|
|
5217
5467
|
}
|
|
5218
5468
|
const updatedResult = await userRepository.findById(userId);
|
|
5219
5469
|
if (!updatedResult.success) {
|
|
@@ -5228,7 +5478,7 @@ function buildUserMutationOps(ctx) {
|
|
|
5228
5478
|
return ok(toUser(updatedResult.data));
|
|
5229
5479
|
},
|
|
5230
5480
|
async deleteUser(userId) {
|
|
5231
|
-
|
|
5481
|
+
logger13.debug("Deleting user", { userId });
|
|
5232
5482
|
const userResult = await userRepository.findById(userId);
|
|
5233
5483
|
if (!userResult.success) {
|
|
5234
5484
|
return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
|
|
@@ -5268,7 +5518,7 @@ function buildUserMutationOps(ctx) {
|
|
|
5268
5518
|
);
|
|
5269
5519
|
}
|
|
5270
5520
|
await sessionFunctions.deleteByUserId(userId);
|
|
5271
|
-
|
|
5521
|
+
logger13.info("User deleted", { userId });
|
|
5272
5522
|
return ok(void 0);
|
|
5273
5523
|
}
|
|
5274
5524
|
};
|
|
@@ -5278,7 +5528,7 @@ function buildPasswordChangeOps(ctx) {
|
|
|
5278
5528
|
const { hashPassword } = ctx;
|
|
5279
5529
|
return {
|
|
5280
5530
|
async adminResetPassword(userId, newPassword) {
|
|
5281
|
-
|
|
5531
|
+
logger13.debug("Admin resetting user password", { userId });
|
|
5282
5532
|
const userResult = await userRepository.findById(userId);
|
|
5283
5533
|
if (!userResult.success) {
|
|
5284
5534
|
return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
|
|
@@ -5303,7 +5553,7 @@ function buildPasswordChangeOps(ctx) {
|
|
|
5303
5553
|
const revoked = await sessionFunctions.deleteByUserId(userId);
|
|
5304
5554
|
if (!revoked.success)
|
|
5305
5555
|
return revoked;
|
|
5306
|
-
|
|
5556
|
+
logger13.info("Admin reset password", { userId });
|
|
5307
5557
|
return ok(void 0);
|
|
5308
5558
|
},
|
|
5309
5559
|
async changePassword(userId, oldPassword, newPassword) {
|
|
@@ -5357,7 +5607,7 @@ function buildPasswordChangeOps(ctx) {
|
|
|
5357
5607
|
const revoked = await sessionFunctions.deleteByUserId(userId);
|
|
5358
5608
|
if (!revoked.success)
|
|
5359
5609
|
return revoked;
|
|
5360
|
-
|
|
5610
|
+
logger13.info("Password changed", { userId });
|
|
5361
5611
|
return ok(void 0);
|
|
5362
5612
|
},
|
|
5363
5613
|
/**
|
|
@@ -5395,10 +5645,10 @@ async function issuePasswordReset(ctx, stored) {
|
|
|
5395
5645
|
try {
|
|
5396
5646
|
await onPasswordResetRequest(user, token, { expiresAt, idempotencyKey: `iam-reset:${tokenHash.data}` });
|
|
5397
5647
|
} catch {
|
|
5398
|
-
|
|
5648
|
+
logger13.error("Password reset callback failed", { userId: user.id });
|
|
5399
5649
|
return err(HaiIamError.INTERNAL_ERROR, iamM("iam_notificationDeliveryUnknown"));
|
|
5400
5650
|
}
|
|
5401
|
-
|
|
5651
|
+
logger13.info("Password reset token generated", { userId: user.id });
|
|
5402
5652
|
return ok(void 0);
|
|
5403
5653
|
}
|
|
5404
5654
|
function buildPasswordResetOps(ctx) {
|
|
@@ -5409,17 +5659,17 @@ function buildPasswordResetOps(ctx) {
|
|
|
5409
5659
|
const captcha = await verifyIamCaptcha(ctx.config.captcha, "passwordReset", options?.captcha);
|
|
5410
5660
|
if (!captcha.success)
|
|
5411
5661
|
return captcha;
|
|
5412
|
-
|
|
5662
|
+
logger13.debug("Password reset requested", { identifier });
|
|
5413
5663
|
const resetConfig = ctx.resetConfig;
|
|
5414
5664
|
if (!resetConfig.enabled || !onPasswordResetRequest)
|
|
5415
5665
|
return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
|
|
5416
5666
|
const userResult = await userRepository.findByIdentifier(identifier);
|
|
5417
5667
|
if (!userResult.success) {
|
|
5418
|
-
|
|
5668
|
+
logger13.warn("Failed to look up user for password reset", { identifier });
|
|
5419
5669
|
return ok(void 0);
|
|
5420
5670
|
}
|
|
5421
5671
|
if (!userResult.data) {
|
|
5422
|
-
|
|
5672
|
+
logger13.debug("User not found for password reset, returning ok to prevent enumeration", { identifier });
|
|
5423
5673
|
return ok(void 0);
|
|
5424
5674
|
}
|
|
5425
5675
|
const user = toUser(userResult.data);
|
|
@@ -5428,7 +5678,7 @@ function buildPasswordResetOps(ctx) {
|
|
|
5428
5678
|
return issuePasswordReset(ctx, userResult.data);
|
|
5429
5679
|
},
|
|
5430
5680
|
async confirmPasswordReset(token, newPassword) {
|
|
5431
|
-
|
|
5681
|
+
logger13.debug("Confirming password reset");
|
|
5432
5682
|
const resetConfig = ctx.resetConfig;
|
|
5433
5683
|
if (!resetConfig.enabled)
|
|
5434
5684
|
return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
|
|
@@ -5452,7 +5702,7 @@ function buildPasswordResetOps(ctx) {
|
|
|
5452
5702
|
const revoked = await sessionFunctions.deleteByUserId(claimed.data.id);
|
|
5453
5703
|
if (!revoked.success)
|
|
5454
5704
|
return revoked;
|
|
5455
|
-
|
|
5705
|
+
logger13.info("Password reset confirmed", { userId: claimed.data.id });
|
|
5456
5706
|
return ok(void 0);
|
|
5457
5707
|
}
|
|
5458
5708
|
};
|
|
@@ -5517,7 +5767,7 @@ function buildUserFunctions(deps) {
|
|
|
5517
5767
|
}
|
|
5518
5768
|
|
|
5519
5769
|
// src/iam-main.ts
|
|
5520
|
-
var
|
|
5770
|
+
var logger14 = core.logger.child({ module: "iam", scope: "main" });
|
|
5521
5771
|
var initInProgress = false;
|
|
5522
5772
|
var currentConfig = null;
|
|
5523
5773
|
var currentPublicConfig = null;
|
|
@@ -5548,7 +5798,7 @@ var notInitializedUser = new Proxy({}, {
|
|
|
5548
5798
|
var iam = {
|
|
5549
5799
|
async init(config) {
|
|
5550
5800
|
if (initInProgress) {
|
|
5551
|
-
|
|
5801
|
+
logger14.warn("IAM init already in progress, skipping concurrent call");
|
|
5552
5802
|
return err(
|
|
5553
5803
|
HaiIamError.CONFIG_ERROR,
|
|
5554
5804
|
iamM("iam_initInProgress")
|
|
@@ -5557,11 +5807,11 @@ var iam = {
|
|
|
5557
5807
|
initInProgress = true;
|
|
5558
5808
|
try {
|
|
5559
5809
|
if (currentConfig !== null) {
|
|
5560
|
-
|
|
5810
|
+
logger14.warn("IAM module is already initialized, reinitializing");
|
|
5561
5811
|
await iam.close();
|
|
5562
5812
|
}
|
|
5563
5813
|
const { ldapClientFactory, ldapSyncUser } = config;
|
|
5564
|
-
|
|
5814
|
+
logger14.info("Initializing IAM module");
|
|
5565
5815
|
if (!reldb.isInitialized) {
|
|
5566
5816
|
return err(
|
|
5567
5817
|
HaiIamError.CONFIG_ERROR,
|
|
@@ -5586,7 +5836,7 @@ var iam = {
|
|
|
5586
5836
|
}
|
|
5587
5837
|
const parseResult = IamConfigSchema.safeParse(config);
|
|
5588
5838
|
if (!parseResult.success) {
|
|
5589
|
-
|
|
5839
|
+
logger14.error("IAM config validation failed", { error: parseResult.error.message });
|
|
5590
5840
|
return err(
|
|
5591
5841
|
HaiIamError.CONFIG_ERROR,
|
|
5592
5842
|
iamM("iam_configError", { params: { error: parseResult.error.message } }),
|
|
@@ -5595,7 +5845,7 @@ var iam = {
|
|
|
5595
5845
|
}
|
|
5596
5846
|
const parsed = parseResult.data;
|
|
5597
5847
|
const { onPasswordResetRequest, onOtpSendEmail, onOtpSendSms } = await resolveIamNotifications(config);
|
|
5598
|
-
|
|
5848
|
+
logger14.info("IAM notification channels resolved", { email: Boolean(onOtpSendEmail), sms: Boolean(onOtpSendSms), passwordReset: Boolean(onPasswordResetRequest) });
|
|
5599
5849
|
const migration = await migrateIam();
|
|
5600
5850
|
if (!migration.success)
|
|
5601
5851
|
return migration;
|
|
@@ -5603,7 +5853,7 @@ var iam = {
|
|
|
5603
5853
|
const sessionResult = await createSessionOperations({
|
|
5604
5854
|
config: parsed,
|
|
5605
5855
|
readCredential: async (userId) => {
|
|
5606
|
-
const found = await credentialUsers
|
|
5856
|
+
const found = await readIamUserSecurity(credentialUsers, userId);
|
|
5607
5857
|
if (!found.success)
|
|
5608
5858
|
return found;
|
|
5609
5859
|
if (found.data) {
|
|
@@ -5611,7 +5861,7 @@ var iam = {
|
|
|
5611
5861
|
if (!passwordState.success)
|
|
5612
5862
|
return passwordState;
|
|
5613
5863
|
}
|
|
5614
|
-
return ok(found.data ? { version: found.data.credentialVersion ?? "", enabled: isUserActive(found.data) } : null);
|
|
5864
|
+
return ok(found.data ? { version: found.data.credentialVersion ?? "", enabled: isUserActive(found.data), profile: { username: found.data.username, displayName: found.data.displayName, avatarUrl: found.data.avatarUrl } } : null);
|
|
5615
5865
|
}
|
|
5616
5866
|
});
|
|
5617
5867
|
if (!sessionResult.success) {
|
|
@@ -5676,19 +5926,19 @@ var iam = {
|
|
|
5676
5926
|
return;
|
|
5677
5927
|
accountScanRunning = users.scanInactiveAccounts().then((result) => {
|
|
5678
5928
|
if (!result.success)
|
|
5679
|
-
|
|
5929
|
+
logger14.error("Inactive account scan failed", { error: result.error });
|
|
5680
5930
|
}).catch((error) => {
|
|
5681
|
-
|
|
5931
|
+
logger14.error("Inactive account scan failed", { error });
|
|
5682
5932
|
}).finally(() => {
|
|
5683
5933
|
accountScanRunning = void 0;
|
|
5684
5934
|
});
|
|
5685
5935
|
}, (parsed.accounts?.scanIntervalSeconds ?? 3600) * 1e3);
|
|
5686
5936
|
accountScanTimer.unref();
|
|
5687
5937
|
}
|
|
5688
|
-
|
|
5938
|
+
logger14.info("IAM module initialized");
|
|
5689
5939
|
return ok(void 0);
|
|
5690
5940
|
} catch (error) {
|
|
5691
|
-
|
|
5941
|
+
logger14.error("IAM module initialization failed", { error });
|
|
5692
5942
|
return err(
|
|
5693
5943
|
HaiIamError.CONFIG_ERROR,
|
|
5694
5944
|
iamM("iam_initFailed"),
|
|
@@ -5701,6 +5951,18 @@ var iam = {
|
|
|
5701
5951
|
get auth() {
|
|
5702
5952
|
return currentAuth ?? notInitializedAuth;
|
|
5703
5953
|
},
|
|
5954
|
+
async getPublicConfig() {
|
|
5955
|
+
const snapshot = currentPublicConfig ? structuredClone(currentPublicConfig) : null;
|
|
5956
|
+
const authz = currentAuthz;
|
|
5957
|
+
if (!snapshot || !authz)
|
|
5958
|
+
return notInitialized.result();
|
|
5959
|
+
if (!snapshot.reauthentication.enabled)
|
|
5960
|
+
return ok(snapshot);
|
|
5961
|
+
const configured = await authz.getSensitiveOperations();
|
|
5962
|
+
if (!configured.success)
|
|
5963
|
+
return configured;
|
|
5964
|
+
return ok({ ...snapshot, reauthentication: { ...snapshot.reauthentication, operations: [.../* @__PURE__ */ new Set([...snapshot.reauthentication.operations, ...configured.data])].sort() } });
|
|
5965
|
+
},
|
|
5704
5966
|
get user() {
|
|
5705
5967
|
return currentUser ?? notInitializedUser;
|
|
5706
5968
|
},
|
|
@@ -5730,10 +5992,10 @@ var iam = {
|
|
|
5730
5992
|
},
|
|
5731
5993
|
async close() {
|
|
5732
5994
|
if (currentConfig === null && currentAuth === null && currentUser === null && currentAuthz === null && currentSession === null) {
|
|
5733
|
-
|
|
5995
|
+
logger14.info("IAM module already closed, skipping");
|
|
5734
5996
|
return;
|
|
5735
5997
|
}
|
|
5736
|
-
|
|
5998
|
+
logger14.info("Closing IAM module");
|
|
5737
5999
|
clearInterval(accountScanTimer);
|
|
5738
6000
|
accountScanTimer = void 0;
|
|
5739
6001
|
await accountScanRunning;
|
|
@@ -5750,7 +6012,7 @@ var iam = {
|
|
|
5750
6012
|
resetUserRepoSingleton();
|
|
5751
6013
|
resetRoleRepoSingleton();
|
|
5752
6014
|
resetPermissionRepoSingleton();
|
|
5753
|
-
|
|
6015
|
+
logger14.info("IAM module closed");
|
|
5754
6016
|
}
|
|
5755
6017
|
};
|
|
5756
6018
|
|