@h-ai/iam 0.1.0-alpha.74 → 0.1.0-alpha.76
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 +20 -12
- package/README.ja.md +19 -11
- package/README.md +19 -11
- package/dist/browser.d.ts +3 -3
- package/dist/index.js +332 -181
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ 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';
|
|
@@ -641,6 +641,118 @@ var iamM = core.i18n.createMessageGetter({
|
|
|
641
641
|
"ja-JP": ja_JP_default
|
|
642
642
|
});
|
|
643
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
|
+
|
|
644
756
|
// src/authn/apikey/iam-authn-apikey-repository.ts
|
|
645
757
|
var TABLE_NAME = "hai_iam_api_keys";
|
|
646
758
|
var API_KEY_SORT_COLUMNS = {
|
|
@@ -742,6 +854,10 @@ async function createDbApiKeyRepository() {
|
|
|
742
854
|
return apiKeyRepoInstance;
|
|
743
855
|
const repo = new DbApiKeyRepository();
|
|
744
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);
|
|
745
861
|
apiKeyRepoInstance = repo;
|
|
746
862
|
apiKeyRepoSqlOps = reldb.sql;
|
|
747
863
|
return repo;
|
|
@@ -928,7 +1044,7 @@ function applySessionPatch(session, patch) {
|
|
|
928
1044
|
}
|
|
929
1045
|
|
|
930
1046
|
// src/ticket/iam-ticket-functions.ts
|
|
931
|
-
var
|
|
1047
|
+
var logger2 = core.logger.child({ module: "iam", scope: "ticket" });
|
|
932
1048
|
var TICKET_KEY_PREFIX = "hai:iam:ticket:";
|
|
933
1049
|
var DEFAULT_TICKET_TTL_MS = 3e4;
|
|
934
1050
|
function buildTicketKey(ticket) {
|
|
@@ -948,7 +1064,7 @@ function createTicketOperations() {
|
|
|
948
1064
|
};
|
|
949
1065
|
const saved = await cache.kv.set(buildTicketKey(ticket), record, { px: ttlMs, nx: true });
|
|
950
1066
|
if (!saved.success || !saved.data) {
|
|
951
|
-
|
|
1067
|
+
logger2.warn("Failed to issue ticket", { purpose: options.purpose });
|
|
952
1068
|
return err(HaiIamError.TICKET_ISSUE_FAILED, iamM("iam_ticketIssueFailed"));
|
|
953
1069
|
}
|
|
954
1070
|
return ok({ ticket, expiresAt });
|
|
@@ -1152,6 +1268,12 @@ async function createDbUserRepository() {
|
|
|
1152
1268
|
return userRepoInstance;
|
|
1153
1269
|
const repo = new DbUserRepository();
|
|
1154
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));
|
|
1155
1277
|
userRepoInstance = repo;
|
|
1156
1278
|
userRepoSqlOps = reldb.sql;
|
|
1157
1279
|
return repo;
|
|
@@ -1422,7 +1544,7 @@ async function resetLoginFailures(userRepository, user, policy) {
|
|
|
1422
1544
|
}
|
|
1423
1545
|
|
|
1424
1546
|
// src/authn/apikey/iam-authn-apikey-strategy.ts
|
|
1425
|
-
var
|
|
1547
|
+
var logger3 = core.logger.child({ module: "iam", scope: "apikey-strategy" });
|
|
1426
1548
|
var KEY_PREFIX_LENGTH = 12;
|
|
1427
1549
|
function generateRawKey(prefix2) {
|
|
1428
1550
|
const hex = randomBytes(32).toString("hex");
|
|
@@ -1448,7 +1570,7 @@ function createApiKeyStrategy(config) {
|
|
|
1448
1570
|
const apikeyConfig = config.apikeyConfig ? ApiKeyConfigSchema.parse(config.apikeyConfig) : ApiKeyConfigSchema.parse({});
|
|
1449
1571
|
const { userRepository, apiKeyRepository } = config;
|
|
1450
1572
|
const passwordOps = crypto$1.password;
|
|
1451
|
-
async function
|
|
1573
|
+
async function loadRawKey(rawKey) {
|
|
1452
1574
|
const prefix2 = extractKeyPrefix(rawKey);
|
|
1453
1575
|
const candidatesResult = await apiKeyRepository.findByKeyPrefix(prefix2);
|
|
1454
1576
|
if (!candidatesResult.success) {
|
|
@@ -1467,12 +1589,26 @@ function createApiKeyStrategy(config) {
|
|
|
1467
1589
|
return err(HaiIamError.APIKEY_EXPIRED, iamM("iam_apikeyExpired"));
|
|
1468
1590
|
}
|
|
1469
1591
|
apiKeyRepository.updateFields(candidate.id, { lastUsedAt: /* @__PURE__ */ new Date() }).catch(() => {
|
|
1470
|
-
|
|
1592
|
+
logger3.warn("Failed to update API Key lastUsedAt", { keyId: candidate.id });
|
|
1471
1593
|
});
|
|
1472
1594
|
return ok(toApiKey(candidate));
|
|
1473
1595
|
}
|
|
1474
1596
|
return err(HaiIamError.APIKEY_INVALID, iamM("iam_apikeyInvalid"));
|
|
1475
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
|
+
}
|
|
1476
1612
|
const strategy = {
|
|
1477
1613
|
type: "apikey",
|
|
1478
1614
|
name: "apikey-strategy",
|
|
@@ -1497,7 +1633,7 @@ function createApiKeyStrategy(config) {
|
|
|
1497
1633
|
if (!isUserActive(userResult.data)) {
|
|
1498
1634
|
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
1499
1635
|
}
|
|
1500
|
-
|
|
1636
|
+
logger3.debug("API Key authentication succeeded", { userId: userResult.data.id, keyId: apiKey.id });
|
|
1501
1637
|
return ok({ user: { ...toUser(userResult.data), credentialVersion: userResult.data.credentialVersion ?? "" }, apiKey });
|
|
1502
1638
|
}
|
|
1503
1639
|
};
|
|
@@ -1534,7 +1670,7 @@ function createApiKeyStrategy(config) {
|
|
|
1534
1670
|
const insertResult = await apiKeyRepository.insert(storedApiKey);
|
|
1535
1671
|
if (!insertResult.success)
|
|
1536
1672
|
return insertResult;
|
|
1537
|
-
|
|
1673
|
+
logger3.info("API Key created", { userId, keyId: id, name: options.name });
|
|
1538
1674
|
return ok({ apiKey: toApiKey(storedApiKey), rawKey });
|
|
1539
1675
|
} catch (error) {
|
|
1540
1676
|
return err(HaiIamError.INTERNAL_ERROR, iamM("iam_apikeyCreateFailed", { params: { message: String(error) } }), error);
|
|
@@ -1556,7 +1692,7 @@ function createApiKeyStrategy(config) {
|
|
|
1556
1692
|
const result = await apiKeyRepository.removeById(keyId);
|
|
1557
1693
|
if (!result.success)
|
|
1558
1694
|
return result;
|
|
1559
|
-
|
|
1695
|
+
logger3.info("API Key revoked", { keyId });
|
|
1560
1696
|
return ok(void 0);
|
|
1561
1697
|
},
|
|
1562
1698
|
async verifyApiKey(rawKey) {
|
|
@@ -1607,7 +1743,7 @@ function createLoginAccessCheck(policy) {
|
|
|
1607
1743
|
return ok(void 0);
|
|
1608
1744
|
};
|
|
1609
1745
|
}
|
|
1610
|
-
var
|
|
1746
|
+
var logger4 = core.logger.child({ module: "iam", scope: "captcha" });
|
|
1611
1747
|
var prefix = "hai:iam:captcha:";
|
|
1612
1748
|
var operationSchema = z.enum(["login", "register", "sendOtp", "passwordReset", "renewPassword"]);
|
|
1613
1749
|
var proofSchema = z.object({
|
|
@@ -1639,7 +1775,7 @@ async function createIamCaptcha(config, input) {
|
|
|
1639
1775
|
return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
|
|
1640
1776
|
return ok({ id, challenge });
|
|
1641
1777
|
} catch (error) {
|
|
1642
|
-
|
|
1778
|
+
logger4.error("Captcha challenge generation failed", { operation: operation.data, error });
|
|
1643
1779
|
return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
|
|
1644
1780
|
}
|
|
1645
1781
|
}
|
|
@@ -1675,7 +1811,7 @@ async function verifyIamCaptcha(config, operation, proof) {
|
|
|
1675
1811
|
return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
|
|
1676
1812
|
return removed.data === 1 ? ok(void 0) : err(HaiIamError.CAPTCHA_INVALID, iamM("iam_captchaInvalid"));
|
|
1677
1813
|
} catch (error) {
|
|
1678
|
-
|
|
1814
|
+
logger4.error("Captcha verification failed", { operation, error });
|
|
1679
1815
|
return err(HaiIamError.CAPTCHA_UNAVAILABLE, iamM("iam_captchaUnavailable"));
|
|
1680
1816
|
}
|
|
1681
1817
|
}
|
|
@@ -1733,7 +1869,7 @@ async function loadLdapClientFactory() {
|
|
|
1733
1869
|
}
|
|
1734
1870
|
};
|
|
1735
1871
|
}
|
|
1736
|
-
var
|
|
1872
|
+
var logger5 = core.logger.child({ module: "iam", scope: "ldap-strategy" });
|
|
1737
1873
|
function escapeLdapFilterValue(value) {
|
|
1738
1874
|
return value.replace(/[\\*()/\0]/g, (ch) => {
|
|
1739
1875
|
const hex = ch.charCodeAt(0).toString(16).padStart(2, "0");
|
|
@@ -1836,7 +1972,7 @@ function createLdapStrategy(config) {
|
|
|
1836
1972
|
if (!failureResult.success)
|
|
1837
1973
|
return failureResult;
|
|
1838
1974
|
}
|
|
1839
|
-
|
|
1975
|
+
logger5.warn("LDAP authentication failed", { username });
|
|
1840
1976
|
return err(
|
|
1841
1977
|
HaiIamError.INVALID_CREDENTIALS,
|
|
1842
1978
|
iamM("iam_passwordWrong")
|
|
@@ -1874,7 +2010,7 @@ function createLdapStrategy(config) {
|
|
|
1874
2010
|
return assigned;
|
|
1875
2011
|
}
|
|
1876
2012
|
} catch (callbackError) {
|
|
1877
|
-
|
|
2013
|
+
logger5.warn("onUserAutoRegistered callback failed", { userId: storedUser?.id, error: callbackError });
|
|
1878
2014
|
}
|
|
1879
2015
|
}
|
|
1880
2016
|
} else if (storedUser && syncUser) {
|
|
@@ -1899,7 +2035,7 @@ function createLdapStrategy(config) {
|
|
|
1899
2035
|
const resetResult = await resetLoginFailures(userRepository, storedUser, failurePolicy);
|
|
1900
2036
|
if (!resetResult.success)
|
|
1901
2037
|
return resetResult;
|
|
1902
|
-
|
|
2038
|
+
logger5.debug("LDAP authentication succeeded", { userId: storedUser.id });
|
|
1903
2039
|
return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
|
|
1904
2040
|
} finally {
|
|
1905
2041
|
await client.unbind();
|
|
@@ -1919,7 +2055,7 @@ function createCacheOtpRepository() {
|
|
|
1919
2055
|
if (otpRepoInstance)
|
|
1920
2056
|
return otpRepoInstance;
|
|
1921
2057
|
const keyFor = (identifier) => `hai:iam:otp:${normalizeOtpIdentifier(identifier)}`;
|
|
1922
|
-
const
|
|
2058
|
+
const failure2 = () => err(HaiIamError.REPOSITORY_ERROR, iamM("iam_securityStateFailed"));
|
|
1923
2059
|
const invalid = () => err(HaiIamError.OTP_INVALID, iamM("iam_otpNotExistOrExpired"));
|
|
1924
2060
|
otpRepoInstance = {
|
|
1925
2061
|
async issueOtp(input) {
|
|
@@ -1934,7 +2070,7 @@ function createCacheOtpRepository() {
|
|
|
1934
2070
|
const key = keyFor(input.identifier);
|
|
1935
2071
|
const current = await cache.kv.get(key);
|
|
1936
2072
|
if (!current.success)
|
|
1937
|
-
return
|
|
2073
|
+
return failure2();
|
|
1938
2074
|
if (current.data && current.data.cooldownUntil > Date.now()) {
|
|
1939
2075
|
return err(HaiIamError.OTP_RESEND_TOO_FAST, iamM("iam_otpResendTooFast", { params: { seconds: Math.ceil((current.data.cooldownUntil - Date.now()) / 1e3) } }));
|
|
1940
2076
|
}
|
|
@@ -1942,7 +2078,7 @@ function createCacheOtpRepository() {
|
|
|
1942
2078
|
return invalid();
|
|
1943
2079
|
const published = await cache.kv.compareAndSwap(key, current.data ?? void 0, state, { px: Math.max(state.expiresAt, state.cooldownUntil) - Date.now() });
|
|
1944
2080
|
if (!published.success)
|
|
1945
|
-
return
|
|
2081
|
+
return failure2();
|
|
1946
2082
|
if (!published.data)
|
|
1947
2083
|
return err(HaiIamError.SECURITY_BUSY, iamM("iam_securityBusy"));
|
|
1948
2084
|
if (Date.now() >= state.expiresAt)
|
|
@@ -1955,7 +2091,7 @@ function createCacheOtpRepository() {
|
|
|
1955
2091
|
for (let attempt = 0; attempt < 16; attempt++) {
|
|
1956
2092
|
const current = await cache.kv.get(key);
|
|
1957
2093
|
if (!current.success)
|
|
1958
|
-
return
|
|
2094
|
+
return failure2();
|
|
1959
2095
|
const state = current.data;
|
|
1960
2096
|
if (!state || state.code === null || state.expiresAt <= Date.now() || state.attempts >= maxAttempts || version && state.version !== version)
|
|
1961
2097
|
return invalid();
|
|
@@ -1965,7 +2101,7 @@ function createCacheOtpRepository() {
|
|
|
1965
2101
|
const next = { ...state, attempts, code: correct || attempts >= maxAttempts ? null : state.code };
|
|
1966
2102
|
const committed = await cache.kv.compareAndSwap(key, state, next, { keepTtl: true });
|
|
1967
2103
|
if (!committed.success)
|
|
1968
|
-
return
|
|
2104
|
+
return failure2();
|
|
1969
2105
|
if (!committed.data)
|
|
1970
2106
|
continue;
|
|
1971
2107
|
if (!correct || state.expiresAt <= Date.now())
|
|
@@ -1977,7 +2113,7 @@ function createCacheOtpRepository() {
|
|
|
1977
2113
|
};
|
|
1978
2114
|
return otpRepoInstance;
|
|
1979
2115
|
}
|
|
1980
|
-
var
|
|
2116
|
+
var logger6 = core.logger.child({ module: "iam", scope: "otp-strategy" });
|
|
1981
2117
|
var PHONE_IDENTIFIER_REGEX = /^\+?\d{8,}$/;
|
|
1982
2118
|
var PHONE_SEPARATOR_REGEX = /[\s-]/g;
|
|
1983
2119
|
function generateOtpCode(length) {
|
|
@@ -2075,7 +2211,7 @@ function createOtpStrategy(config) {
|
|
|
2075
2211
|
return assigned;
|
|
2076
2212
|
}
|
|
2077
2213
|
} catch (callbackError) {
|
|
2078
|
-
|
|
2214
|
+
logger6.warn("onUserAutoRegistered callback failed", { userId: storedUser.id, error: callbackError });
|
|
2079
2215
|
}
|
|
2080
2216
|
}
|
|
2081
2217
|
}
|
|
@@ -2087,7 +2223,7 @@ function createOtpStrategy(config) {
|
|
|
2087
2223
|
const resetResult = await resetLoginFailures(config.userRepository, storedUser, failurePolicy);
|
|
2088
2224
|
if (!resetResult.success)
|
|
2089
2225
|
return resetResult;
|
|
2090
|
-
|
|
2226
|
+
logger6.debug("OTP authentication succeeded", { userId: storedUser.id });
|
|
2091
2227
|
return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
|
|
2092
2228
|
}
|
|
2093
2229
|
};
|
|
@@ -2131,12 +2267,12 @@ function createOtpStrategy(config) {
|
|
|
2131
2267
|
);
|
|
2132
2268
|
}
|
|
2133
2269
|
const result = { expiresAt };
|
|
2134
|
-
|
|
2270
|
+
logger6.debug("OTP challenge sent", { identifier, expiresAt });
|
|
2135
2271
|
return ok(result);
|
|
2136
2272
|
}
|
|
2137
2273
|
return { strategy, challenge };
|
|
2138
2274
|
}
|
|
2139
|
-
var
|
|
2275
|
+
var logger7 = core.logger.child({ module: "iam", scope: "password-strategy" });
|
|
2140
2276
|
var PASSWORD_UPPERCASE_REGEX = /[A-Z]/;
|
|
2141
2277
|
var PASSWORD_LOWERCASE_REGEX = /[a-z]/;
|
|
2142
2278
|
var PASSWORD_DIGIT_REGEX = /\d/;
|
|
@@ -2216,7 +2352,7 @@ function createPasswordStrategy(config) {
|
|
|
2216
2352
|
const failureResult = await recordLoginFailure(config.userRepository, user, failurePolicy);
|
|
2217
2353
|
if (!failureResult.success)
|
|
2218
2354
|
return failureResult;
|
|
2219
|
-
|
|
2355
|
+
logger7.warn("Password verification failed", { userId: user.id });
|
|
2220
2356
|
return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
|
|
2221
2357
|
}
|
|
2222
2358
|
return ok(void 0);
|
|
@@ -2250,7 +2386,7 @@ function createPasswordStrategy(config) {
|
|
|
2250
2386
|
const resetResult = await resetLoginFailures(config.userRepository, storedUser, failurePolicy);
|
|
2251
2387
|
if (!resetResult.success)
|
|
2252
2388
|
return resetResult;
|
|
2253
|
-
|
|
2389
|
+
logger7.debug("Password authentication succeeded", { userId: storedUser.id });
|
|
2254
2390
|
return ok({ ...toUser(storedUser), credentialVersion: storedUser.credentialVersion ?? "" });
|
|
2255
2391
|
}
|
|
2256
2392
|
};
|
|
@@ -2270,7 +2406,7 @@ function createPasswordStrategy(config) {
|
|
|
2270
2406
|
}
|
|
2271
2407
|
|
|
2272
2408
|
// src/authn/iam-authn-functions.ts
|
|
2273
|
-
var
|
|
2409
|
+
var logger8 = core.logger.child({ module: "iam", scope: "authn" });
|
|
2274
2410
|
async function createAuthnOperations(deps) {
|
|
2275
2411
|
try {
|
|
2276
2412
|
const { config, sessionFunctions, authzFunctions, ldapClientFactory, ldapSyncUser, onOtpSendEmail, onOtpSendSms } = deps;
|
|
@@ -2354,10 +2490,10 @@ async function createAuthnOperations(deps) {
|
|
|
2354
2490
|
userRepository,
|
|
2355
2491
|
config
|
|
2356
2492
|
});
|
|
2357
|
-
|
|
2493
|
+
logger8.info("Authn sub-feature initialized");
|
|
2358
2494
|
return ok({ authn: operations.authn, loginRegisteredUser: operations.loginRegisteredUser, passwordStrategyResult: passwordResult, apiKeyFunctions });
|
|
2359
2495
|
} catch (error) {
|
|
2360
|
-
|
|
2496
|
+
logger8.error("Authn sub-feature initialization failed", { error });
|
|
2361
2497
|
return err(
|
|
2362
2498
|
HaiIamError.CONFIG_ERROR,
|
|
2363
2499
|
iamM("iam_initComponentFailed"),
|
|
@@ -2452,7 +2588,7 @@ function buildAuthnOperations(deps) {
|
|
|
2452
2588
|
if (!recorded.success) {
|
|
2453
2589
|
const revoked = await sessionFunctions.delete(session.accessToken);
|
|
2454
2590
|
if (!revoked.success)
|
|
2455
|
-
|
|
2591
|
+
logger8.error("Failed to revoke session after login timestamp write failure", { userId: user.id, error: revoked.error });
|
|
2456
2592
|
return recorded;
|
|
2457
2593
|
}
|
|
2458
2594
|
const { credentialVersion, ...publicUser } = user;
|
|
@@ -2481,14 +2617,14 @@ function buildAuthnOperations(deps) {
|
|
|
2481
2617
|
...credentials
|
|
2482
2618
|
});
|
|
2483
2619
|
if (!authResult.success) {
|
|
2484
|
-
|
|
2620
|
+
logger8.warn("Login failed", { type, reason: authResult.error.code });
|
|
2485
2621
|
return authResult;
|
|
2486
2622
|
}
|
|
2487
2623
|
if (credentials.expectedUserId && credentials.expectedUserId !== authResult.data.id)
|
|
2488
2624
|
return err(HaiIamError.INVALID_CREDENTIALS, iamM("iam_passwordWrong"));
|
|
2489
2625
|
const result = await buildAuthResult(authResult.data, void 0, context);
|
|
2490
2626
|
if (result.success) {
|
|
2491
|
-
|
|
2627
|
+
logger8.info("Login succeeded", { type, userId: authResult.data.id });
|
|
2492
2628
|
}
|
|
2493
2629
|
return result;
|
|
2494
2630
|
}
|
|
@@ -2496,19 +2632,10 @@ function buildAuthnOperations(deps) {
|
|
|
2496
2632
|
const session = await sessionFunctions.verifyToken(accessToken);
|
|
2497
2633
|
if (!session.success)
|
|
2498
2634
|
return session;
|
|
2499
|
-
const
|
|
2500
|
-
userRepository.findById(session.data.userId),
|
|
2501
|
-
authzFunctions.getUserAuthorization(session.data.userId)
|
|
2502
|
-
]);
|
|
2503
|
-
if (!user.success)
|
|
2504
|
-
return user;
|
|
2505
|
-
if (!user.data?.enabled)
|
|
2506
|
-
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
2507
|
-
if (session.data.credentialVersion !== (user.data.credentialVersion ?? ""))
|
|
2508
|
-
return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
|
|
2635
|
+
const authorization = await authzFunctions.getUserAuthorization(session.data.userId);
|
|
2509
2636
|
if (!authorization.success)
|
|
2510
2637
|
return authorization;
|
|
2511
|
-
const current = { ...session.data,
|
|
2638
|
+
const current = { ...session.data, ...authorization.data };
|
|
2512
2639
|
return ok(current.apiKeyId ? applyApiKeyScopes(current, current.apiKeyScopes ?? []) : current);
|
|
2513
2640
|
}
|
|
2514
2641
|
const tickets = createTicketOperations();
|
|
@@ -2516,7 +2643,9 @@ function buildAuthnOperations(deps) {
|
|
|
2516
2643
|
async function requiresProof(operation) {
|
|
2517
2644
|
if (!sensitive?.enabled)
|
|
2518
2645
|
return ok(false);
|
|
2519
|
-
|
|
2646
|
+
if (sensitive.operations.includes(operation))
|
|
2647
|
+
return ok(true);
|
|
2648
|
+
const configured = await authzFunctions.getSensitiveOperations(operation);
|
|
2520
2649
|
if (!configured.success)
|
|
2521
2650
|
return configured;
|
|
2522
2651
|
return ok(sensitive.operations.includes(operation) || configured.data.includes(operation));
|
|
@@ -2703,8 +2832,12 @@ async function createDbPermissionRepository() {
|
|
|
2703
2832
|
return repo;
|
|
2704
2833
|
}
|
|
2705
2834
|
var DbPermissionRepository = class extends BaseReldbCrudRepository {
|
|
2706
|
-
async getSensitiveOperations() {
|
|
2707
|
-
|
|
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);
|
|
2708
2841
|
if (!result.success)
|
|
2709
2842
|
return result;
|
|
2710
2843
|
const operations = /* @__PURE__ */ new Set();
|
|
@@ -2716,7 +2849,8 @@ var DbPermissionRepository = class extends BaseReldbCrudRepository {
|
|
|
2716
2849
|
for (const value of values) {
|
|
2717
2850
|
if (typeof value !== "string" || !value.trim() || value.length > 128)
|
|
2718
2851
|
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_reauthenticationRequired"));
|
|
2719
|
-
|
|
2852
|
+
if (operation === void 0 || value.trim() === operation)
|
|
2853
|
+
operations.add(value.trim());
|
|
2720
2854
|
}
|
|
2721
2855
|
}
|
|
2722
2856
|
return ok([...operations].sort());
|
|
@@ -3292,7 +3426,7 @@ var DbRoleRepository = class extends BaseReldbCrudRepository {
|
|
|
3292
3426
|
};
|
|
3293
3427
|
|
|
3294
3428
|
// src/authz/iam-authz-functions.ts
|
|
3295
|
-
var
|
|
3429
|
+
var logger9 = core.logger.child({ module: "iam", scope: "authz" });
|
|
3296
3430
|
async function createAuthzOperations(deps) {
|
|
3297
3431
|
try {
|
|
3298
3432
|
const { config, session } = deps;
|
|
@@ -3314,10 +3448,23 @@ async function createAuthzOperations(deps) {
|
|
|
3314
3448
|
userRoleRepository: userRoleResult.data,
|
|
3315
3449
|
session
|
|
3316
3450
|
});
|
|
3317
|
-
|
|
3318
|
-
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
|
+
});
|
|
3319
3466
|
} catch (error) {
|
|
3320
|
-
|
|
3467
|
+
logger9.error("Authz sub-feature initialization failed", { error });
|
|
3321
3468
|
return err(
|
|
3322
3469
|
HaiIamError.CONFIG_ERROR,
|
|
3323
3470
|
iamM("iam_initComponentFailed"),
|
|
@@ -3334,18 +3481,6 @@ function createRbacManager(config) {
|
|
|
3334
3481
|
userRoleRepository,
|
|
3335
3482
|
session
|
|
3336
3483
|
} = config;
|
|
3337
|
-
let superAdminRoleId;
|
|
3338
|
-
async function resolveSuperAdminRoleId() {
|
|
3339
|
-
if (superAdminRoleId !== void 0) {
|
|
3340
|
-
return ok(superAdminRoleId);
|
|
3341
|
-
}
|
|
3342
|
-
const roleResult = await roleRepository.findByCode(rbacConfig.superAdminRole);
|
|
3343
|
-
if (!roleResult.success) {
|
|
3344
|
-
return mapRepositoryError2("iam_queryRoleFailed", roleResult.error.message);
|
|
3345
|
-
}
|
|
3346
|
-
superAdminRoleId = roleResult.data?.id ?? null;
|
|
3347
|
-
return ok(superAdminRoleId);
|
|
3348
|
-
}
|
|
3349
3484
|
function mapRepositoryError2(messageKey, message) {
|
|
3350
3485
|
return err(
|
|
3351
3486
|
HaiIamError.REPOSITORY_ERROR,
|
|
@@ -3362,16 +3497,6 @@ function createRbacManager(config) {
|
|
|
3362
3497
|
}
|
|
3363
3498
|
return false;
|
|
3364
3499
|
}
|
|
3365
|
-
async function hasPermissionInRoles(roleIds, permission) {
|
|
3366
|
-
const codesResult = await rolePermissionRepository.getPermissionCodesForRoles(roleIds);
|
|
3367
|
-
if (!codesResult.success)
|
|
3368
|
-
return codesResult;
|
|
3369
|
-
for (const code of codesResult.data) {
|
|
3370
|
-
if (matchesPermission(permission, code))
|
|
3371
|
-
return ok(true);
|
|
3372
|
-
}
|
|
3373
|
-
return ok(false);
|
|
3374
|
-
}
|
|
3375
3500
|
async function getUserPermissionsInternal(userId) {
|
|
3376
3501
|
const roleIdsResult = await userRoleRepository.getRoleIds(userId);
|
|
3377
3502
|
if (!roleIdsResult.success)
|
|
@@ -3413,19 +3538,19 @@ function createRbacManager(config) {
|
|
|
3413
3538
|
async function syncSessionPermissionsForRole(roleId) {
|
|
3414
3539
|
const userIdsResult = await userRoleRepository.getUserIdsByRoleId(roleId);
|
|
3415
3540
|
if (!userIdsResult.success) {
|
|
3416
|
-
|
|
3541
|
+
logger9.error("Failed to query users for permission sync", { roleId, error: userIdsResult.error.message });
|
|
3417
3542
|
return;
|
|
3418
3543
|
}
|
|
3419
3544
|
await Promise.allSettled(
|
|
3420
3545
|
userIdsResult.data.map(async (userId) => {
|
|
3421
3546
|
const permResult = await resolveUserPermissionCodes(userId);
|
|
3422
3547
|
if (!permResult.success) {
|
|
3423
|
-
|
|
3548
|
+
logger9.error("Failed to resolve permissions for session sync", { userId, roleId, error: permResult.error.message });
|
|
3424
3549
|
return;
|
|
3425
3550
|
}
|
|
3426
3551
|
const syncResult = await session.patchUserSessions(userId, { permissions: permResult.data });
|
|
3427
3552
|
if (!syncResult.success) {
|
|
3428
|
-
|
|
3553
|
+
logger9.error("Failed to sync session permissions", { userId, roleId, error: syncResult.error.message });
|
|
3429
3554
|
}
|
|
3430
3555
|
})
|
|
3431
3556
|
);
|
|
@@ -3439,17 +3564,17 @@ function createRbacManager(config) {
|
|
|
3439
3564
|
if (roleCodesResult.success) {
|
|
3440
3565
|
updates.roles = roleCodesResult.data;
|
|
3441
3566
|
} else {
|
|
3442
|
-
|
|
3567
|
+
logger9.error("Failed to resolve roles for session sync", { userId, error: roleCodesResult.error.message });
|
|
3443
3568
|
}
|
|
3444
3569
|
if (permCodesResult.success) {
|
|
3445
3570
|
updates.permissions = permCodesResult.data;
|
|
3446
3571
|
} else {
|
|
3447
|
-
|
|
3572
|
+
logger9.error("Failed to resolve permissions for session sync", { userId, error: permCodesResult.error.message });
|
|
3448
3573
|
}
|
|
3449
3574
|
if (updates.roles !== void 0 || updates.permissions !== void 0) {
|
|
3450
3575
|
const syncResult = await session.patchUserSessions(userId, updates);
|
|
3451
3576
|
if (!syncResult.success) {
|
|
3452
|
-
|
|
3577
|
+
logger9.error("Failed to patch user sessions", { userId, error: syncResult.error.message });
|
|
3453
3578
|
}
|
|
3454
3579
|
}
|
|
3455
3580
|
}
|
|
@@ -3472,8 +3597,14 @@ function createRbacManager(config) {
|
|
|
3472
3597
|
}
|
|
3473
3598
|
return ok(void 0);
|
|
3474
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
|
+
}
|
|
3475
3606
|
return {
|
|
3476
|
-
getSensitiveOperations: () => rbacConfig.enabled ? permissionRepository.getSensitiveOperations() : Promise.resolve(ok([])),
|
|
3607
|
+
getSensitiveOperations: (operation) => rbacConfig.enabled ? readIamCache(`sensitive:${operation ?? "*"}`, () => permissionRepository.getSensitiveOperations(operation)) : Promise.resolve(ok([])),
|
|
3477
3608
|
async setPermissionSensitiveOperations(permissionId, operations) {
|
|
3478
3609
|
if (!Array.isArray(operations) || operations.length > 1e3 || operations.some((value) => typeof value !== "string" || !value.trim() || value.length > 128))
|
|
3479
3610
|
return err(HaiIamError.INVALID_ARGUMENT, iamM("iam_sensitiveOperationsInvalid"));
|
|
@@ -3493,33 +3624,15 @@ function createRbacManager(config) {
|
|
|
3493
3624
|
if (!rbacConfig.enabled) {
|
|
3494
3625
|
return ok(true);
|
|
3495
3626
|
}
|
|
3496
|
-
const
|
|
3497
|
-
if (!
|
|
3498
|
-
return
|
|
3499
|
-
|
|
3500
|
-
if (roleIds.length === 0) {
|
|
3501
|
-
return ok(false);
|
|
3502
|
-
}
|
|
3503
|
-
const superAdminResult = await resolveSuperAdminRoleId();
|
|
3504
|
-
if (!superAdminResult.success)
|
|
3505
|
-
return superAdminResult;
|
|
3506
|
-
if (superAdminResult.data && roleIds.includes(superAdminResult.data)) {
|
|
3507
|
-
return ok(true);
|
|
3508
|
-
}
|
|
3509
|
-
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)));
|
|
3510
3631
|
},
|
|
3511
3632
|
async getUserPermissions(userId) {
|
|
3512
3633
|
return getUserPermissionsInternal(userId);
|
|
3513
3634
|
},
|
|
3514
|
-
|
|
3515
|
-
const roles = await userRoleRepository.getRoleSummaries(userId);
|
|
3516
|
-
if (!roles.success)
|
|
3517
|
-
return roles;
|
|
3518
|
-
const permissions = roles.data.length ? await rolePermissionRepository.getPermissionCodesForRoles(roles.data.map((role) => role.id)) : ok([]);
|
|
3519
|
-
if (!permissions.success)
|
|
3520
|
-
return permissions;
|
|
3521
|
-
return ok({ roles: roles.data.map((role) => role.code), permissions: permissions.data });
|
|
3522
|
-
},
|
|
3635
|
+
getUserAuthorization,
|
|
3523
3636
|
async getUserRoles(userId) {
|
|
3524
3637
|
return userRoleRepository.getRoles(userId);
|
|
3525
3638
|
},
|
|
@@ -3533,7 +3646,7 @@ function createRbacManager(config) {
|
|
|
3533
3646
|
}
|
|
3534
3647
|
const result = await userRoleRepository.assign(userId, roleId, tx);
|
|
3535
3648
|
if (result.success) {
|
|
3536
|
-
|
|
3649
|
+
logger9.info("Role assigned to user", { userId, roleId });
|
|
3537
3650
|
if (!tx) {
|
|
3538
3651
|
await syncUserSessionAfterRoleChange(userId);
|
|
3539
3652
|
}
|
|
@@ -3543,7 +3656,7 @@ function createRbacManager(config) {
|
|
|
3543
3656
|
async removeRole(userId, roleId, tx) {
|
|
3544
3657
|
const result = await userRoleRepository.remove(userId, roleId, tx);
|
|
3545
3658
|
if (result.success) {
|
|
3546
|
-
|
|
3659
|
+
logger9.info("Role removed from user", { userId, roleId });
|
|
3547
3660
|
if (!tx) {
|
|
3548
3661
|
await syncUserSessionAfterRoleChange(userId);
|
|
3549
3662
|
}
|
|
@@ -3611,7 +3724,7 @@ function createRbacManager(config) {
|
|
|
3611
3724
|
error
|
|
3612
3725
|
);
|
|
3613
3726
|
}
|
|
3614
|
-
|
|
3727
|
+
logger9.info("Roles synced for user", { userId, added: toAdd.length, removed: toRemove.length });
|
|
3615
3728
|
if (ownTx) {
|
|
3616
3729
|
await syncUserSessionAfterRoleChange(userId);
|
|
3617
3730
|
}
|
|
@@ -3663,10 +3776,7 @@ function createRbacManager(config) {
|
|
|
3663
3776
|
return mapRepositoryError2("iam_createRoleFailed", commitResult.error.message);
|
|
3664
3777
|
}
|
|
3665
3778
|
}
|
|
3666
|
-
|
|
3667
|
-
if (role.code === rbacConfig.superAdminRole) {
|
|
3668
|
-
superAdminRoleId = void 0;
|
|
3669
|
-
}
|
|
3779
|
+
logger9.info("Role created", { roleId: createdResult.data.id, code: role.code });
|
|
3670
3780
|
return ok(createdResult.data);
|
|
3671
3781
|
} catch (error) {
|
|
3672
3782
|
if (ownTx)
|
|
@@ -3752,7 +3862,6 @@ function createRbacManager(config) {
|
|
|
3752
3862
|
return mapRepositoryError2("iam_updateRoleFailed", commitResult.error.message);
|
|
3753
3863
|
}
|
|
3754
3864
|
}
|
|
3755
|
-
superAdminRoleId = void 0;
|
|
3756
3865
|
if (ownTx) {
|
|
3757
3866
|
const affectedUsersResult = await userRoleRepository.getUserIdsByRoleId(roleId);
|
|
3758
3867
|
if (affectedUsersResult.success) {
|
|
@@ -3760,7 +3869,7 @@ function createRbacManager(config) {
|
|
|
3760
3869
|
affectedUsersResult.data.map((userId) => syncUserSessionAfterRoleChange(userId))
|
|
3761
3870
|
);
|
|
3762
3871
|
} else {
|
|
3763
|
-
|
|
3872
|
+
logger9.error("Failed to query affected users after updateRole", { roleId, error: affectedUsersResult.error.message });
|
|
3764
3873
|
}
|
|
3765
3874
|
}
|
|
3766
3875
|
return ok(updatedResult.data);
|
|
@@ -3834,8 +3943,7 @@ function createRbacManager(config) {
|
|
|
3834
3943
|
affectedUserIds.map((userId) => syncUserSessionAfterRoleChange(userId))
|
|
3835
3944
|
);
|
|
3836
3945
|
}
|
|
3837
|
-
|
|
3838
|
-
logger8.info("Role deleted", { roleId });
|
|
3946
|
+
logger9.info("Role deleted", { roleId });
|
|
3839
3947
|
return ok(void 0);
|
|
3840
3948
|
},
|
|
3841
3949
|
// ─── 权限管理 ───
|
|
@@ -3879,7 +3987,7 @@ function createRbacManager(config) {
|
|
|
3879
3987
|
return mapRepositoryError2("iam_createPermissionFailed", commitResult.error.message);
|
|
3880
3988
|
}
|
|
3881
3989
|
}
|
|
3882
|
-
|
|
3990
|
+
logger9.info("Permission created", { permissionId: createdResult.data.id, code: permission.code });
|
|
3883
3991
|
return ok(createdResult.data);
|
|
3884
3992
|
} catch (error) {
|
|
3885
3993
|
if (ownTx)
|
|
@@ -3983,7 +4091,7 @@ function createRbacManager(config) {
|
|
|
3983
4091
|
affectedRoleIds.map((roleId) => syncSessionPermissionsForRole(roleId))
|
|
3984
4092
|
);
|
|
3985
4093
|
}
|
|
3986
|
-
|
|
4094
|
+
logger9.info("Permission deleted", { permissionId });
|
|
3987
4095
|
return ok(void 0);
|
|
3988
4096
|
},
|
|
3989
4097
|
async assignPermissionToRole(roleId, permissionId, tx) {
|
|
@@ -4005,7 +4113,7 @@ function createRbacManager(config) {
|
|
|
4005
4113
|
}
|
|
4006
4114
|
const assignResult = await rolePermissionRepository.assign(roleId, permissionId, tx);
|
|
4007
4115
|
if (assignResult.success) {
|
|
4008
|
-
|
|
4116
|
+
logger9.info("Permission assigned to role", { roleId, permissionId });
|
|
4009
4117
|
if (!tx) {
|
|
4010
4118
|
await syncSessionPermissionsForRole(roleId);
|
|
4011
4119
|
}
|
|
@@ -4034,7 +4142,7 @@ function createRbacManager(config) {
|
|
|
4034
4142
|
}
|
|
4035
4143
|
const removeResult = await rolePermissionRepository.remove(roleId, permissionId, tx);
|
|
4036
4144
|
if (removeResult.success) {
|
|
4037
|
-
|
|
4145
|
+
logger9.info("Permission removed from role", { roleId, permissionId });
|
|
4038
4146
|
if (!tx) {
|
|
4039
4147
|
await syncSessionPermissionsForRole(roleId);
|
|
4040
4148
|
}
|
|
@@ -4052,14 +4160,16 @@ function createRbacManager(config) {
|
|
|
4052
4160
|
}
|
|
4053
4161
|
};
|
|
4054
4162
|
}
|
|
4055
|
-
var
|
|
4163
|
+
var logger10 = core.logger.child({ module: "iam", scope: "audit" });
|
|
4056
4164
|
async function observe(operation, event, tx) {
|
|
4057
4165
|
const result = await operation();
|
|
4166
|
+
if (!audit.isInitialized)
|
|
4167
|
+
return result;
|
|
4058
4168
|
const fields = typeof event === "function" ? event(result) : event;
|
|
4059
4169
|
if (!fields)
|
|
4060
4170
|
return result;
|
|
4061
4171
|
if (tx && !result.success) {
|
|
4062
|
-
|
|
4172
|
+
logger10.warn("IAM transactional operation failed", { action: fields.action, resource: fields.resource, reasonCode: result.error.code });
|
|
4063
4173
|
return result;
|
|
4064
4174
|
}
|
|
4065
4175
|
const recorded = await audit.capture({
|
|
@@ -4069,7 +4179,7 @@ async function observe(operation, event, tx) {
|
|
|
4069
4179
|
details: { ...fields.details, ...!result.success ? { reasonCode: result.error.code } : {} }
|
|
4070
4180
|
}, { tx });
|
|
4071
4181
|
if (!recorded.success) {
|
|
4072
|
-
|
|
4182
|
+
logger10.error("IAM event audit failed", { action: fields.action, resource: fields.resource, error: recorded.error });
|
|
4073
4183
|
if (tx)
|
|
4074
4184
|
return recorded;
|
|
4075
4185
|
}
|
|
@@ -4292,7 +4402,29 @@ function buildIamPublicConfig(config, runtime) {
|
|
|
4292
4402
|
initialCredentials: { enabled: Boolean(config.initialCredentials?.enabled && config.passwordReset?.enabled !== false && runtime.onPasswordResetRequest) }
|
|
4293
4403
|
};
|
|
4294
4404
|
}
|
|
4295
|
-
|
|
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" });
|
|
4296
4428
|
var DEFAULT_ROLES = [
|
|
4297
4429
|
{ code: "admin", name: () => iamM("iam_seedRoleAdminName"), description: () => iamM("iam_seedRoleAdminDesc"), isSystem: true },
|
|
4298
4430
|
{ code: "user", name: () => iamM("iam_seedRoleUserName"), description: () => iamM("iam_seedRoleUserDesc"), isSystem: true },
|
|
@@ -4336,12 +4468,11 @@ var USER_ROLE_PERMISSIONS = ["dashboard:view", "profile:read"];
|
|
|
4336
4468
|
async function seedIamData(authz) {
|
|
4337
4469
|
try {
|
|
4338
4470
|
const existingRoles = await authz.getAllRoles({ page: 1, pageSize: 1e3 });
|
|
4471
|
+
if (!existingRoles.success)
|
|
4472
|
+
return existingRoles;
|
|
4339
4473
|
const existingRoleMap = /* @__PURE__ */ new Map();
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
existingRoleMap.set(role.code, role.id);
|
|
4343
|
-
}
|
|
4344
|
-
}
|
|
4474
|
+
for (const role of existingRoles.data.items)
|
|
4475
|
+
existingRoleMap.set(role.code, role.id);
|
|
4345
4476
|
const roleMap = /* @__PURE__ */ new Map();
|
|
4346
4477
|
for (const role of DEFAULT_ROLES) {
|
|
4347
4478
|
const existingId = existingRoleMap.get(role.code);
|
|
@@ -4352,17 +4483,23 @@ async function seedIamData(authz) {
|
|
|
4352
4483
|
const result = await authz.createRole({ code: role.code, name: role.name(), description: role.description(), isSystem: role.isSystem });
|
|
4353
4484
|
if (result.success) {
|
|
4354
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);
|
|
4355
4493
|
} else {
|
|
4356
4494
|
return result;
|
|
4357
4495
|
}
|
|
4358
4496
|
}
|
|
4359
4497
|
const existingPerms = await authz.getAllPermissions({ page: 1, pageSize: 1e3 });
|
|
4498
|
+
if (!existingPerms.success)
|
|
4499
|
+
return existingPerms;
|
|
4360
4500
|
const existingPermMap = /* @__PURE__ */ new Map();
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
existingPermMap.set(perm.code, perm.id);
|
|
4364
|
-
}
|
|
4365
|
-
}
|
|
4501
|
+
for (const perm of existingPerms.data.items)
|
|
4502
|
+
existingPermMap.set(perm.code, perm.id);
|
|
4366
4503
|
const permMap = /* @__PURE__ */ new Map();
|
|
4367
4504
|
for (const perm of DEFAULT_PERMISSIONS) {
|
|
4368
4505
|
const existingId = existingPermMap.get(perm.code);
|
|
@@ -4373,6 +4510,13 @@ async function seedIamData(authz) {
|
|
|
4373
4510
|
const result = await authz.createPermission({ code: perm.code, name: perm.name(), type: perm.type, resource: perm.resource, action: perm.action });
|
|
4374
4511
|
if (result.success) {
|
|
4375
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);
|
|
4376
4520
|
} else {
|
|
4377
4521
|
return result;
|
|
4378
4522
|
}
|
|
@@ -4380,7 +4524,9 @@ async function seedIamData(authz) {
|
|
|
4380
4524
|
const adminRoleId = roleMap.get("admin");
|
|
4381
4525
|
if (adminRoleId) {
|
|
4382
4526
|
for (const [, permId] of permMap) {
|
|
4383
|
-
await authz.assignPermissionToRole(adminRoleId, permId);
|
|
4527
|
+
const assigned = await authz.assignPermissionToRole(adminRoleId, permId);
|
|
4528
|
+
if (!assigned.success)
|
|
4529
|
+
return assigned;
|
|
4384
4530
|
}
|
|
4385
4531
|
}
|
|
4386
4532
|
const userRoleId = roleMap.get("user");
|
|
@@ -4388,14 +4534,16 @@ async function seedIamData(authz) {
|
|
|
4388
4534
|
for (const permCode of USER_ROLE_PERMISSIONS) {
|
|
4389
4535
|
const permId = permMap.get(permCode);
|
|
4390
4536
|
if (permId) {
|
|
4391
|
-
await authz.assignPermissionToRole(userRoleId, permId);
|
|
4537
|
+
const assigned = await authz.assignPermissionToRole(userRoleId, permId);
|
|
4538
|
+
if (!assigned.success)
|
|
4539
|
+
return assigned;
|
|
4392
4540
|
}
|
|
4393
4541
|
}
|
|
4394
4542
|
}
|
|
4395
|
-
|
|
4543
|
+
logger11.info("IAM seed data initialized");
|
|
4396
4544
|
return ok(void 0);
|
|
4397
4545
|
} catch (error) {
|
|
4398
|
-
|
|
4546
|
+
logger11.error("Failed to seed IAM data", { error });
|
|
4399
4547
|
return err(
|
|
4400
4548
|
HaiIamError.REPOSITORY_ERROR,
|
|
4401
4549
|
iamM("iam_initSeedDataFailed"),
|
|
@@ -4641,7 +4789,7 @@ function stripInternalSessionFields(session) {
|
|
|
4641
4789
|
const hasExtraKeys = Object.keys(rest).length > 0;
|
|
4642
4790
|
return { ...session, data: hasExtraKeys ? rest : void 0 };
|
|
4643
4791
|
}
|
|
4644
|
-
var
|
|
4792
|
+
var logger12 = core.logger.child({ module: "iam", scope: "session" });
|
|
4645
4793
|
async function createSessionOperations(deps) {
|
|
4646
4794
|
try {
|
|
4647
4795
|
const { config } = deps;
|
|
@@ -4659,10 +4807,10 @@ async function createSessionOperations(deps) {
|
|
|
4659
4807
|
sessionRepository,
|
|
4660
4808
|
apiKeyRepository: config.login.apikey ? await createDbApiKeyRepository() : void 0
|
|
4661
4809
|
});
|
|
4662
|
-
|
|
4810
|
+
logger12.info("Session sub-feature initialized");
|
|
4663
4811
|
return ok(functions);
|
|
4664
4812
|
} catch (error) {
|
|
4665
|
-
|
|
4813
|
+
logger12.error("Session sub-feature initialization failed", { error });
|
|
4666
4814
|
return err(
|
|
4667
4815
|
HaiIamError.CONFIG_ERROR,
|
|
4668
4816
|
iamM("iam_initComponentFailed"),
|
|
@@ -4697,7 +4845,7 @@ function buildSessionFunctions(config) {
|
|
|
4697
4845
|
return err(HaiIamError.USER_DISABLED, iamM("iam_accountDisabled"));
|
|
4698
4846
|
if (typeof current.data.version !== "string" || expected !== void 0 && current.data.version !== expected)
|
|
4699
4847
|
return err(HaiIamError.SESSION_INVALID, iamM("iam_sessionExpired"));
|
|
4700
|
-
return ok(current.data
|
|
4848
|
+
return ok(current.data);
|
|
4701
4849
|
} catch {
|
|
4702
4850
|
return err(HaiIamError.REPOSITORY_ERROR, iamM("iam_sessionExpired"));
|
|
4703
4851
|
}
|
|
@@ -4707,7 +4855,10 @@ function buildSessionFunctions(config) {
|
|
|
4707
4855
|
return ok(session);
|
|
4708
4856
|
if (!config.apiKeyRepository)
|
|
4709
4857
|
return err(HaiIamError.APIKEY_INVALID, iamM("iam_apikeyInvalid"));
|
|
4710
|
-
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
|
+
});
|
|
4711
4862
|
if (!found.success)
|
|
4712
4863
|
return found;
|
|
4713
4864
|
const key = found.data;
|
|
@@ -4715,7 +4866,7 @@ function buildSessionFunctions(config) {
|
|
|
4715
4866
|
return err(HaiIamError.APIKEY_INVALID, iamM("iam_apikeyInvalid"));
|
|
4716
4867
|
if (!key.enabled)
|
|
4717
4868
|
return err(HaiIamError.APIKEY_DISABLED, iamM("iam_apikeyDisabled"));
|
|
4718
|
-
if (key.expiresAt &&
|
|
4869
|
+
if (key.expiresAt && Date.now() >= key.expiresAt)
|
|
4719
4870
|
return err(HaiIamError.APIKEY_EXPIRED, iamM("iam_apikeyExpired"));
|
|
4720
4871
|
return ok(applyApiKeyScopes({ ...session, apiKeyScopes: key.scopes }, key.scopes));
|
|
4721
4872
|
}
|
|
@@ -4725,7 +4876,7 @@ function buildSessionFunctions(config) {
|
|
|
4725
4876
|
const bound = await credential(options.userId, options.expectedCredentialVersion);
|
|
4726
4877
|
if (!bound.success)
|
|
4727
4878
|
return bound;
|
|
4728
|
-
const built = candidate(options, bound.data);
|
|
4879
|
+
const built = candidate(options, bound.data.version);
|
|
4729
4880
|
if (!built.success)
|
|
4730
4881
|
return built;
|
|
4731
4882
|
const { session, tokens } = built.data;
|
|
@@ -4733,7 +4884,7 @@ function buildSessionFunctions(config) {
|
|
|
4733
4884
|
if (!saveResult.success) {
|
|
4734
4885
|
return saveResult;
|
|
4735
4886
|
}
|
|
4736
|
-
|
|
4887
|
+
logger12.debug("Session created", { userId: options.userId });
|
|
4737
4888
|
return ok(session);
|
|
4738
4889
|
} catch (error) {
|
|
4739
4890
|
return err(
|
|
@@ -4764,7 +4915,7 @@ function buildSessionFunctions(config) {
|
|
|
4764
4915
|
const bound = await credential(session.userId, session.credentialVersion);
|
|
4765
4916
|
if (!bound.success)
|
|
4766
4917
|
return bound;
|
|
4767
|
-
const validated = await validateApiKey(session);
|
|
4918
|
+
const validated = await validateApiKey({ ...session, ...bound.data.profile });
|
|
4768
4919
|
if (!validated.success)
|
|
4769
4920
|
return validated;
|
|
4770
4921
|
let output = validated.data;
|
|
@@ -4794,7 +4945,7 @@ function buildSessionFunctions(config) {
|
|
|
4794
4945
|
return repo.updateByAccessToken(accessToken, data, expectedVersion);
|
|
4795
4946
|
},
|
|
4796
4947
|
async delete(accessToken) {
|
|
4797
|
-
|
|
4948
|
+
logger12.debug("Session delete requested");
|
|
4798
4949
|
return repo.removeByAccessToken(accessToken);
|
|
4799
4950
|
},
|
|
4800
4951
|
async deleteByUserId(userId) {
|
|
@@ -4848,7 +4999,7 @@ function buildSessionFunctions(config) {
|
|
|
4848
4999
|
const current = await credential(oldSession.userId, oldSession.credentialVersion);
|
|
4849
5000
|
if (!current.success)
|
|
4850
5001
|
return current;
|
|
4851
|
-
|
|
5002
|
+
logger12.debug("Token refreshed", { userId: oldSession.userId });
|
|
4852
5003
|
return ok(next.data.tokens);
|
|
4853
5004
|
} catch (error) {
|
|
4854
5005
|
return err(
|
|
@@ -4866,7 +5017,7 @@ function buildSessionFunctions(config) {
|
|
|
4866
5017
|
}
|
|
4867
5018
|
};
|
|
4868
5019
|
}
|
|
4869
|
-
var
|
|
5020
|
+
var logger13 = core.logger.child({ module: "iam", scope: "user" });
|
|
4870
5021
|
var USER_SORT_COLUMNS = {
|
|
4871
5022
|
/** 用户名列。 */
|
|
4872
5023
|
username: "username",
|
|
@@ -4891,10 +5042,10 @@ async function createUserOperations(deps) {
|
|
|
4891
5042
|
config,
|
|
4892
5043
|
onPasswordResetRequest
|
|
4893
5044
|
});
|
|
4894
|
-
|
|
5045
|
+
logger13.info("User sub-feature initialized");
|
|
4895
5046
|
return ok(functions);
|
|
4896
5047
|
} catch (error) {
|
|
4897
|
-
|
|
5048
|
+
logger13.error("User sub-feature initialization failed", { error });
|
|
4898
5049
|
return err(
|
|
4899
5050
|
HaiIamError.CONFIG_ERROR,
|
|
4900
5051
|
iamM("iam_initComponentFailed"),
|
|
@@ -5063,7 +5214,7 @@ function buildRegistrationOps(ctx) {
|
|
|
5063
5214
|
await userRepository.deleteById(createdUser.id);
|
|
5064
5215
|
return assigned;
|
|
5065
5216
|
}
|
|
5066
|
-
|
|
5217
|
+
logger13.info("User registered", { userId: createdUser.id, username: options.username });
|
|
5067
5218
|
return ok({
|
|
5068
5219
|
user: toUser(createdUser),
|
|
5069
5220
|
agreements: buildAgreementDisplay(agreementConfig, agreementConfig.showOnRegister)
|
|
@@ -5312,7 +5463,7 @@ function buildUserMutationOps(ctx) {
|
|
|
5312
5463
|
if (tx) {
|
|
5313
5464
|
const revoked = await sessionFunctions.deleteByUserId(userId);
|
|
5314
5465
|
if (!revoked.success)
|
|
5315
|
-
|
|
5466
|
+
logger13.warn("Account credentials revoked; session cache cleanup failed", { userId, error: revoked.error });
|
|
5316
5467
|
}
|
|
5317
5468
|
const updatedResult = await userRepository.findById(userId);
|
|
5318
5469
|
if (!updatedResult.success) {
|
|
@@ -5327,7 +5478,7 @@ function buildUserMutationOps(ctx) {
|
|
|
5327
5478
|
return ok(toUser(updatedResult.data));
|
|
5328
5479
|
},
|
|
5329
5480
|
async deleteUser(userId) {
|
|
5330
|
-
|
|
5481
|
+
logger13.debug("Deleting user", { userId });
|
|
5331
5482
|
const userResult = await userRepository.findById(userId);
|
|
5332
5483
|
if (!userResult.success) {
|
|
5333
5484
|
return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
|
|
@@ -5367,7 +5518,7 @@ function buildUserMutationOps(ctx) {
|
|
|
5367
5518
|
);
|
|
5368
5519
|
}
|
|
5369
5520
|
await sessionFunctions.deleteByUserId(userId);
|
|
5370
|
-
|
|
5521
|
+
logger13.info("User deleted", { userId });
|
|
5371
5522
|
return ok(void 0);
|
|
5372
5523
|
}
|
|
5373
5524
|
};
|
|
@@ -5377,7 +5528,7 @@ function buildPasswordChangeOps(ctx) {
|
|
|
5377
5528
|
const { hashPassword } = ctx;
|
|
5378
5529
|
return {
|
|
5379
5530
|
async adminResetPassword(userId, newPassword) {
|
|
5380
|
-
|
|
5531
|
+
logger13.debug("Admin resetting user password", { userId });
|
|
5381
5532
|
const userResult = await userRepository.findById(userId);
|
|
5382
5533
|
if (!userResult.success) {
|
|
5383
5534
|
return mapRepositoryError("iam_queryUserFailed", userResult.error.message);
|
|
@@ -5402,7 +5553,7 @@ function buildPasswordChangeOps(ctx) {
|
|
|
5402
5553
|
const revoked = await sessionFunctions.deleteByUserId(userId);
|
|
5403
5554
|
if (!revoked.success)
|
|
5404
5555
|
return revoked;
|
|
5405
|
-
|
|
5556
|
+
logger13.info("Admin reset password", { userId });
|
|
5406
5557
|
return ok(void 0);
|
|
5407
5558
|
},
|
|
5408
5559
|
async changePassword(userId, oldPassword, newPassword) {
|
|
@@ -5456,7 +5607,7 @@ function buildPasswordChangeOps(ctx) {
|
|
|
5456
5607
|
const revoked = await sessionFunctions.deleteByUserId(userId);
|
|
5457
5608
|
if (!revoked.success)
|
|
5458
5609
|
return revoked;
|
|
5459
|
-
|
|
5610
|
+
logger13.info("Password changed", { userId });
|
|
5460
5611
|
return ok(void 0);
|
|
5461
5612
|
},
|
|
5462
5613
|
/**
|
|
@@ -5494,10 +5645,10 @@ async function issuePasswordReset(ctx, stored) {
|
|
|
5494
5645
|
try {
|
|
5495
5646
|
await onPasswordResetRequest(user, token, { expiresAt, idempotencyKey: `iam-reset:${tokenHash.data}` });
|
|
5496
5647
|
} catch {
|
|
5497
|
-
|
|
5648
|
+
logger13.error("Password reset callback failed", { userId: user.id });
|
|
5498
5649
|
return err(HaiIamError.INTERNAL_ERROR, iamM("iam_notificationDeliveryUnknown"));
|
|
5499
5650
|
}
|
|
5500
|
-
|
|
5651
|
+
logger13.info("Password reset token generated", { userId: user.id });
|
|
5501
5652
|
return ok(void 0);
|
|
5502
5653
|
}
|
|
5503
5654
|
function buildPasswordResetOps(ctx) {
|
|
@@ -5508,17 +5659,17 @@ function buildPasswordResetOps(ctx) {
|
|
|
5508
5659
|
const captcha = await verifyIamCaptcha(ctx.config.captcha, "passwordReset", options?.captcha);
|
|
5509
5660
|
if (!captcha.success)
|
|
5510
5661
|
return captcha;
|
|
5511
|
-
|
|
5662
|
+
logger13.debug("Password reset requested", { identifier });
|
|
5512
5663
|
const resetConfig = ctx.resetConfig;
|
|
5513
5664
|
if (!resetConfig.enabled || !onPasswordResetRequest)
|
|
5514
5665
|
return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
|
|
5515
5666
|
const userResult = await userRepository.findByIdentifier(identifier);
|
|
5516
5667
|
if (!userResult.success) {
|
|
5517
|
-
|
|
5668
|
+
logger13.warn("Failed to look up user for password reset", { identifier });
|
|
5518
5669
|
return ok(void 0);
|
|
5519
5670
|
}
|
|
5520
5671
|
if (!userResult.data) {
|
|
5521
|
-
|
|
5672
|
+
logger13.debug("User not found for password reset, returning ok to prevent enumeration", { identifier });
|
|
5522
5673
|
return ok(void 0);
|
|
5523
5674
|
}
|
|
5524
5675
|
const user = toUser(userResult.data);
|
|
@@ -5527,7 +5678,7 @@ function buildPasswordResetOps(ctx) {
|
|
|
5527
5678
|
return issuePasswordReset(ctx, userResult.data);
|
|
5528
5679
|
},
|
|
5529
5680
|
async confirmPasswordReset(token, newPassword) {
|
|
5530
|
-
|
|
5681
|
+
logger13.debug("Confirming password reset");
|
|
5531
5682
|
const resetConfig = ctx.resetConfig;
|
|
5532
5683
|
if (!resetConfig.enabled)
|
|
5533
5684
|
return err(HaiIamError.PASSWORD_RESET_DISABLED, iamM("iam_passwordResetDisabled"));
|
|
@@ -5551,7 +5702,7 @@ function buildPasswordResetOps(ctx) {
|
|
|
5551
5702
|
const revoked = await sessionFunctions.deleteByUserId(claimed.data.id);
|
|
5552
5703
|
if (!revoked.success)
|
|
5553
5704
|
return revoked;
|
|
5554
|
-
|
|
5705
|
+
logger13.info("Password reset confirmed", { userId: claimed.data.id });
|
|
5555
5706
|
return ok(void 0);
|
|
5556
5707
|
}
|
|
5557
5708
|
};
|
|
@@ -5616,7 +5767,7 @@ function buildUserFunctions(deps) {
|
|
|
5616
5767
|
}
|
|
5617
5768
|
|
|
5618
5769
|
// src/iam-main.ts
|
|
5619
|
-
var
|
|
5770
|
+
var logger14 = core.logger.child({ module: "iam", scope: "main" });
|
|
5620
5771
|
var initInProgress = false;
|
|
5621
5772
|
var currentConfig = null;
|
|
5622
5773
|
var currentPublicConfig = null;
|
|
@@ -5647,7 +5798,7 @@ var notInitializedUser = new Proxy({}, {
|
|
|
5647
5798
|
var iam = {
|
|
5648
5799
|
async init(config) {
|
|
5649
5800
|
if (initInProgress) {
|
|
5650
|
-
|
|
5801
|
+
logger14.warn("IAM init already in progress, skipping concurrent call");
|
|
5651
5802
|
return err(
|
|
5652
5803
|
HaiIamError.CONFIG_ERROR,
|
|
5653
5804
|
iamM("iam_initInProgress")
|
|
@@ -5656,11 +5807,11 @@ var iam = {
|
|
|
5656
5807
|
initInProgress = true;
|
|
5657
5808
|
try {
|
|
5658
5809
|
if (currentConfig !== null) {
|
|
5659
|
-
|
|
5810
|
+
logger14.warn("IAM module is already initialized, reinitializing");
|
|
5660
5811
|
await iam.close();
|
|
5661
5812
|
}
|
|
5662
5813
|
const { ldapClientFactory, ldapSyncUser } = config;
|
|
5663
|
-
|
|
5814
|
+
logger14.info("Initializing IAM module");
|
|
5664
5815
|
if (!reldb.isInitialized) {
|
|
5665
5816
|
return err(
|
|
5666
5817
|
HaiIamError.CONFIG_ERROR,
|
|
@@ -5685,7 +5836,7 @@ var iam = {
|
|
|
5685
5836
|
}
|
|
5686
5837
|
const parseResult = IamConfigSchema.safeParse(config);
|
|
5687
5838
|
if (!parseResult.success) {
|
|
5688
|
-
|
|
5839
|
+
logger14.error("IAM config validation failed", { error: parseResult.error.message });
|
|
5689
5840
|
return err(
|
|
5690
5841
|
HaiIamError.CONFIG_ERROR,
|
|
5691
5842
|
iamM("iam_configError", { params: { error: parseResult.error.message } }),
|
|
@@ -5694,7 +5845,7 @@ var iam = {
|
|
|
5694
5845
|
}
|
|
5695
5846
|
const parsed = parseResult.data;
|
|
5696
5847
|
const { onPasswordResetRequest, onOtpSendEmail, onOtpSendSms } = await resolveIamNotifications(config);
|
|
5697
|
-
|
|
5848
|
+
logger14.info("IAM notification channels resolved", { email: Boolean(onOtpSendEmail), sms: Boolean(onOtpSendSms), passwordReset: Boolean(onPasswordResetRequest) });
|
|
5698
5849
|
const migration = await migrateIam();
|
|
5699
5850
|
if (!migration.success)
|
|
5700
5851
|
return migration;
|
|
@@ -5702,7 +5853,7 @@ var iam = {
|
|
|
5702
5853
|
const sessionResult = await createSessionOperations({
|
|
5703
5854
|
config: parsed,
|
|
5704
5855
|
readCredential: async (userId) => {
|
|
5705
|
-
const found = await credentialUsers
|
|
5856
|
+
const found = await readIamUserSecurity(credentialUsers, userId);
|
|
5706
5857
|
if (!found.success)
|
|
5707
5858
|
return found;
|
|
5708
5859
|
if (found.data) {
|
|
@@ -5710,7 +5861,7 @@ var iam = {
|
|
|
5710
5861
|
if (!passwordState.success)
|
|
5711
5862
|
return passwordState;
|
|
5712
5863
|
}
|
|
5713
|
-
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);
|
|
5714
5865
|
}
|
|
5715
5866
|
});
|
|
5716
5867
|
if (!sessionResult.success) {
|
|
@@ -5775,19 +5926,19 @@ var iam = {
|
|
|
5775
5926
|
return;
|
|
5776
5927
|
accountScanRunning = users.scanInactiveAccounts().then((result) => {
|
|
5777
5928
|
if (!result.success)
|
|
5778
|
-
|
|
5929
|
+
logger14.error("Inactive account scan failed", { error: result.error });
|
|
5779
5930
|
}).catch((error) => {
|
|
5780
|
-
|
|
5931
|
+
logger14.error("Inactive account scan failed", { error });
|
|
5781
5932
|
}).finally(() => {
|
|
5782
5933
|
accountScanRunning = void 0;
|
|
5783
5934
|
});
|
|
5784
5935
|
}, (parsed.accounts?.scanIntervalSeconds ?? 3600) * 1e3);
|
|
5785
5936
|
accountScanTimer.unref();
|
|
5786
5937
|
}
|
|
5787
|
-
|
|
5938
|
+
logger14.info("IAM module initialized");
|
|
5788
5939
|
return ok(void 0);
|
|
5789
5940
|
} catch (error) {
|
|
5790
|
-
|
|
5941
|
+
logger14.error("IAM module initialization failed", { error });
|
|
5791
5942
|
return err(
|
|
5792
5943
|
HaiIamError.CONFIG_ERROR,
|
|
5793
5944
|
iamM("iam_initFailed"),
|
|
@@ -5841,10 +5992,10 @@ var iam = {
|
|
|
5841
5992
|
},
|
|
5842
5993
|
async close() {
|
|
5843
5994
|
if (currentConfig === null && currentAuth === null && currentUser === null && currentAuthz === null && currentSession === null) {
|
|
5844
|
-
|
|
5995
|
+
logger14.info("IAM module already closed, skipping");
|
|
5845
5996
|
return;
|
|
5846
5997
|
}
|
|
5847
|
-
|
|
5998
|
+
logger14.info("Closing IAM module");
|
|
5848
5999
|
clearInterval(accountScanTimer);
|
|
5849
6000
|
accountScanTimer = void 0;
|
|
5850
6001
|
await accountScanRunning;
|
|
@@ -5861,7 +6012,7 @@ var iam = {
|
|
|
5861
6012
|
resetUserRepoSingleton();
|
|
5862
6013
|
resetRoleRepoSingleton();
|
|
5863
6014
|
resetPermissionRepoSingleton();
|
|
5864
|
-
|
|
6015
|
+
logger14.info("IAM module closed");
|
|
5865
6016
|
}
|
|
5866
6017
|
};
|
|
5867
6018
|
|