@3sixty/api-auth-entity-storage-service 0.0.0-stage → 0.20.0-next.0

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.
Files changed (133) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +24 -2
  3. package/dist/es/entities/authenticationAuditEntry.js +121 -0
  4. package/dist/es/entities/authenticationAuditEntry.js.map +1 -0
  5. package/dist/es/entities/authenticationRateEntry.js +37 -0
  6. package/dist/es/entities/authenticationRateEntry.js.map +1 -0
  7. package/dist/es/entities/authenticationUser.js +69 -0
  8. package/dist/es/entities/authenticationUser.js.map +1 -0
  9. package/dist/es/index.js +29 -0
  10. package/dist/es/index.js.map +1 -0
  11. package/dist/es/models/IAuthHeaderProcessorConfig.js +4 -0
  12. package/dist/es/models/IAuthHeaderProcessorConfig.js.map +1 -0
  13. package/dist/es/models/IAuthHeaderProcessorConstructorOptions.js +2 -0
  14. package/dist/es/models/IAuthHeaderProcessorConstructorOptions.js.map +1 -0
  15. package/dist/es/models/IAuthTokenContext.js +4 -0
  16. package/dist/es/models/IAuthTokenContext.js.map +1 -0
  17. package/dist/es/models/IEntityStorageAuthenticationAdminServiceConfig.js +4 -0
  18. package/dist/es/models/IEntityStorageAuthenticationAdminServiceConfig.js.map +1 -0
  19. package/dist/es/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js +2 -0
  20. package/dist/es/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js.map +1 -0
  21. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConfig.js +4 -0
  22. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConfig.js.map +1 -0
  23. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.js +2 -0
  24. package/dist/es/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.js.map +1 -0
  25. package/dist/es/models/IEntityStorageAuthenticationRateServiceConfig.js +2 -0
  26. package/dist/es/models/IEntityStorageAuthenticationRateServiceConfig.js.map +1 -0
  27. package/dist/es/models/IEntityStorageAuthenticationRateServiceConstructorOptions.js +2 -0
  28. package/dist/es/models/IEntityStorageAuthenticationRateServiceConstructorOptions.js.map +1 -0
  29. package/dist/es/models/IEntityStorageAuthenticationServiceConfig.js +2 -0
  30. package/dist/es/models/IEntityStorageAuthenticationServiceConfig.js.map +1 -0
  31. package/dist/es/models/IEntityStorageAuthenticationServiceConstructorOptions.js +2 -0
  32. package/dist/es/models/IEntityStorageAuthenticationServiceConstructorOptions.js.map +1 -0
  33. package/dist/es/processors/authHeaderProcessor.js +277 -0
  34. package/dist/es/processors/authHeaderProcessor.js.map +1 -0
  35. package/dist/es/restEntryPoints.js +27 -0
  36. package/dist/es/restEntryPoints.js.map +1 -0
  37. package/dist/es/routes/entityStorageAuthenticationAdminRoutes.js +367 -0
  38. package/dist/es/routes/entityStorageAuthenticationAdminRoutes.js.map +1 -0
  39. package/dist/es/routes/entityStorageAuthenticationAuditRoutes.js +343 -0
  40. package/dist/es/routes/entityStorageAuthenticationAuditRoutes.js.map +1 -0
  41. package/dist/es/routes/entityStorageAuthenticationRoutes.js +247 -0
  42. package/dist/es/routes/entityStorageAuthenticationRoutes.js.map +1 -0
  43. package/dist/es/schema.js +15 -0
  44. package/dist/es/schema.js.map +1 -0
  45. package/dist/es/services/entityStorageAuthenticationAdminService.js +323 -0
  46. package/dist/es/services/entityStorageAuthenticationAdminService.js.map +1 -0
  47. package/dist/es/services/entityStorageAuthenticationAuditService.js +240 -0
  48. package/dist/es/services/entityStorageAuthenticationAuditService.js.map +1 -0
  49. package/dist/es/services/entityStorageAuthenticationRateService.js +213 -0
  50. package/dist/es/services/entityStorageAuthenticationRateService.js.map +1 -0
  51. package/dist/es/services/entityStorageAuthenticationService.js +326 -0
  52. package/dist/es/services/entityStorageAuthenticationService.js.map +1 -0
  53. package/dist/es/utils/passwordHelper.js +58 -0
  54. package/dist/es/utils/passwordHelper.js.map +1 -0
  55. package/dist/es/utils/tokenHelper.js +142 -0
  56. package/dist/es/utils/tokenHelper.js.map +1 -0
  57. package/dist/types/entities/authenticationAuditEntry.d.ts +49 -0
  58. package/dist/types/entities/authenticationRateEntry.d.ts +17 -0
  59. package/dist/types/entities/authenticationUser.d.ts +33 -0
  60. package/dist/types/index.d.ts +26 -0
  61. package/dist/types/models/IAuthHeaderProcessorConfig.d.ts +39 -0
  62. package/dist/types/models/IAuthHeaderProcessorConstructorOptions.d.ts +25 -0
  63. package/dist/types/models/IAuthTokenContext.d.ts +34 -0
  64. package/dist/types/models/IEntityStorageAuthenticationAdminServiceConfig.d.ts +14 -0
  65. package/dist/types/models/IEntityStorageAuthenticationAdminServiceConstructorOptions.d.ts +20 -0
  66. package/dist/types/models/IEntityStorageAuthenticationAuditServiceConfig.d.ts +9 -0
  67. package/dist/types/models/IEntityStorageAuthenticationAuditServiceConstructorOptions.d.ts +15 -0
  68. package/dist/types/models/IEntityStorageAuthenticationRateServiceConfig.d.ts +10 -0
  69. package/dist/types/models/IEntityStorageAuthenticationRateServiceConstructorOptions.d.ts +25 -0
  70. package/dist/types/models/IEntityStorageAuthenticationServiceConfig.d.ts +36 -0
  71. package/dist/types/models/IEntityStorageAuthenticationServiceConstructorOptions.d.ts +35 -0
  72. package/dist/types/processors/authHeaderProcessor.d.ts +57 -0
  73. package/dist/types/restEntryPoints.d.ts +5 -0
  74. package/dist/types/routes/entityStorageAuthenticationAdminRoutes.d.ts +62 -0
  75. package/dist/types/routes/entityStorageAuthenticationAuditRoutes.d.ts +54 -0
  76. package/dist/types/routes/entityStorageAuthenticationRoutes.d.ts +45 -0
  77. package/dist/types/schema.d.ts +4 -0
  78. package/dist/types/services/entityStorageAuthenticationAdminService.d.ts +65 -0
  79. package/dist/types/services/entityStorageAuthenticationAuditService.d.ts +72 -0
  80. package/dist/types/services/entityStorageAuthenticationRateService.d.ts +60 -0
  81. package/dist/types/services/entityStorageAuthenticationService.d.ts +65 -0
  82. package/dist/types/utils/passwordHelper.d.ts +24 -0
  83. package/dist/types/utils/tokenHelper.d.ts +60 -0
  84. package/docs/changelog.md +0 -0
  85. package/docs/examples.md +178 -0
  86. package/docs/open-api/spec.json +2122 -0
  87. package/docs/reference/classes/AuthHeaderProcessor.md +193 -0
  88. package/docs/reference/classes/AuthenticationAuditEntry.md +101 -0
  89. package/docs/reference/classes/AuthenticationRateEntry.md +37 -0
  90. package/docs/reference/classes/AuthenticationUser.md +69 -0
  91. package/docs/reference/classes/EntityStorageAuthenticationAdminService.md +229 -0
  92. package/docs/reference/classes/EntityStorageAuthenticationAuditService.md +241 -0
  93. package/docs/reference/classes/EntityStorageAuthenticationRateService.md +227 -0
  94. package/docs/reference/classes/EntityStorageAuthenticationService.md +221 -0
  95. package/docs/reference/classes/PasswordHelper.md +74 -0
  96. package/docs/reference/classes/TokenHelper.md +205 -0
  97. package/docs/reference/functions/authenticationAdminCreateUser.md +37 -0
  98. package/docs/reference/functions/authenticationAdminGetUser.md +31 -0
  99. package/docs/reference/functions/authenticationAdminGetUserByIdentity.md +31 -0
  100. package/docs/reference/functions/authenticationAdminRemoveUser.md +31 -0
  101. package/docs/reference/functions/authenticationAdminUpdateUser.md +31 -0
  102. package/docs/reference/functions/authenticationAdminUpdateUserPassword.md +31 -0
  103. package/docs/reference/functions/authenticationAuditCreate.md +37 -0
  104. package/docs/reference/functions/authenticationAuditGet.md +31 -0
  105. package/docs/reference/functions/authenticationAuditQuery.md +31 -0
  106. package/docs/reference/functions/authenticationAuditRemove.md +31 -0
  107. package/docs/reference/functions/authenticationAuditUpdate.md +31 -0
  108. package/docs/reference/functions/authenticationLogin.md +31 -0
  109. package/docs/reference/functions/authenticationLogout.md +31 -0
  110. package/docs/reference/functions/authenticationRefreshToken.md +31 -0
  111. package/docs/reference/functions/authenticationUpdatePassword.md +31 -0
  112. package/docs/reference/functions/generateRestRoutesAuthentication.md +25 -0
  113. package/docs/reference/functions/generateRestRoutesAuthenticationAdmin.md +25 -0
  114. package/docs/reference/functions/generateRestRoutesAuthenticationAudit.md +25 -0
  115. package/docs/reference/functions/initSchema.md +9 -0
  116. package/docs/reference/index.md +57 -0
  117. package/docs/reference/interfaces/IAuthHeaderProcessorConfig.md +86 -0
  118. package/docs/reference/interfaces/IAuthHeaderProcessorConstructorOptions.md +53 -0
  119. package/docs/reference/interfaces/IAuthTokenContext.md +60 -0
  120. package/docs/reference/interfaces/IEntityStorageAuthenticationAdminServiceConfig.md +25 -0
  121. package/docs/reference/interfaces/IEntityStorageAuthenticationAdminServiceConstructorOptions.md +39 -0
  122. package/docs/reference/interfaces/IEntityStorageAuthenticationAuditServiceConfig.md +11 -0
  123. package/docs/reference/interfaces/IEntityStorageAuthenticationAuditServiceConstructorOptions.md +25 -0
  124. package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConfig.md +17 -0
  125. package/docs/reference/interfaces/IEntityStorageAuthenticationRateServiceConstructorOptions.md +53 -0
  126. package/docs/reference/interfaces/IEntityStorageAuthenticationServiceConfig.md +87 -0
  127. package/docs/reference/interfaces/IEntityStorageAuthenticationServiceConstructorOptions.md +81 -0
  128. package/docs/reference/variables/restEntryPoints.md +5 -0
  129. package/docs/reference/variables/tagsAuthentication.md +5 -0
  130. package/docs/reference/variables/tagsAuthenticationAdmin.md +5 -0
  131. package/docs/reference/variables/tagsAuthenticationAudit.md +5 -0
  132. package/locales/en.json +52 -0
  133. package/package.json +64 -5
@@ -0,0 +1,15 @@
1
+ // Copyright 2024 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { EntitySchemaFactory, EntitySchemaHelper } from "@3sixty/entity";
4
+ import { AuthenticationAuditEntry } from "./entities/authenticationAuditEntry.js";
5
+ import { AuthenticationRateEntry } from "./entities/authenticationRateEntry.js";
6
+ import { AuthenticationUser } from "./entities/authenticationUser.js";
7
+ /**
8
+ * Initialize the schema for the authentication service.
9
+ */
10
+ export function initSchema() {
11
+ EntitySchemaFactory.register("AuthenticationUser", () => EntitySchemaHelper.getSchema(AuthenticationUser));
12
+ EntitySchemaFactory.register("AuthenticationAuditEntry", () => EntitySchemaHelper.getSchema(AuthenticationAuditEntry));
13
+ EntitySchemaFactory.register("AuthenticationRateEntry", () => EntitySchemaHelper.getSchema(AuthenticationRateEntry));
14
+ }
15
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/schema.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC;AAClF,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE;;GAEG;AACH,MAAM,UAAU,UAAU;IACzB,mBAAmB,CAAC,QAAQ,uBAA+B,GAAG,EAAE,CAC/D,kBAAkB,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAChD,CAAC;IACF,mBAAmB,CAAC,QAAQ,6BAAqC,GAAG,EAAE,CACrE,kBAAkB,CAAC,SAAS,CAAC,wBAAwB,CAAC,CACtD,CAAC;IACF,mBAAmB,CAAC,QAAQ,4BAAoC,GAAG,EAAE,CACpE,kBAAkB,CAAC,SAAS,CAAC,uBAAuB,CAAC,CACrD,CAAC;AACH,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { EntitySchemaFactory, EntitySchemaHelper } from \"@3sixty/entity\";\nimport { nameof } from \"@3sixty/nameof\";\nimport { AuthenticationAuditEntry } from \"./entities/authenticationAuditEntry.js\";\nimport { AuthenticationRateEntry } from \"./entities/authenticationRateEntry.js\";\nimport { AuthenticationUser } from \"./entities/authenticationUser.js\";\n\n/**\n * Initialize the schema for the authentication service.\n */\nexport function initSchema(): void {\n\tEntitySchemaFactory.register(nameof<AuthenticationUser>(), () =>\n\t\tEntitySchemaHelper.getSchema(AuthenticationUser)\n\t);\n\tEntitySchemaFactory.register(nameof<AuthenticationAuditEntry>(), () =>\n\t\tEntitySchemaHelper.getSchema(AuthenticationAuditEntry)\n\t);\n\tEntitySchemaFactory.register(nameof<AuthenticationRateEntry>(), () =>\n\t\tEntitySchemaHelper.getSchema(AuthenticationRateEntry)\n\t);\n}\n"]}
@@ -0,0 +1,323 @@
1
+ import { AuthAuditEvent } from "@3sixty/api-auth-entity-storage-models";
2
+ import { ForbiddenError, HttpContextIdKeys, ScopeHelper } from "@3sixty/api-models";
3
+ import { ContextIdKeys, ContextIdStore } from "@3sixty/context";
4
+ import { BaseError, ComponentFactory, Converter, GeneralError, Guards, Is, NotFoundError, RandomHelper } from "@3sixty/core";
5
+ import { PasswordGenerator, PasswordValidator } from "@3sixty/crypto";
6
+ import { EntityStorageConnectorFactory } from "@3sixty/entity-storage-models";
7
+ import { PasswordHelper } from "../utils/passwordHelper.js";
8
+ /**
9
+ * Implementation of the authentication component using entity storage.
10
+ */
11
+ export class EntityStorageAuthenticationAdminService {
12
+ /**
13
+ * Runtime name for the class.
14
+ */
15
+ static CLASS_NAME = "EntityStorageAuthenticationAdminService";
16
+ /**
17
+ * The scope value that grants permission to assign privileged roles.
18
+ */
19
+ static DEFAULT_ESCALATED_PRIVILEGE_SCOPE = "global-admin";
20
+ /**
21
+ * The entity storage for users.
22
+ * @internal
23
+ */
24
+ _userEntityStorage;
25
+ /**
26
+ * The audit service.
27
+ * @internal
28
+ */
29
+ _authenticationAuditService;
30
+ /**
31
+ * The minimum password length.
32
+ * @internal
33
+ */
34
+ _minPasswordLength;
35
+ /**
36
+ * The scope value that grants permission to assign privileged roles.
37
+ * @internal
38
+ */
39
+ _escalatedPrivilegeScope;
40
+ /**
41
+ * Create a new instance of EntityStorageAuthentication.
42
+ * @param options The dependencies for the identity connector.
43
+ */
44
+ constructor(options) {
45
+ this._userEntityStorage = EntityStorageConnectorFactory.get(options?.userEntityStorageType ?? "authentication-user");
46
+ this._authenticationAuditService = ComponentFactory.getIfExists(options?.authenticationAuditServiceType ?? "authentication-audit");
47
+ this._minPasswordLength = options?.config?.minPasswordLength;
48
+ this._escalatedPrivilegeScope = Is.stringValue(options?.config?.escalatedPrivilegeScope)
49
+ ? options?.config?.escalatedPrivilegeScope
50
+ : EntityStorageAuthenticationAdminService.DEFAULT_ESCALATED_PRIVILEGE_SCOPE;
51
+ }
52
+ /**
53
+ * Returns the class name of the component.
54
+ * @returns The class name of the component.
55
+ */
56
+ className() {
57
+ return EntityStorageAuthenticationAdminService.CLASS_NAME;
58
+ }
59
+ /**
60
+ * Create a login for the user.
61
+ * @param user The user to create.
62
+ * @returns A promise that resolves when the user account has been created and the audit entry recorded.
63
+ */
64
+ async create(user) {
65
+ Guards.object(EntityStorageAuthenticationAdminService.CLASS_NAME, "user", user);
66
+ Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "user.email", user.email);
67
+ Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "user.password", user.password);
68
+ Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "user.userIdentity", user.userIdentity);
69
+ Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "user.organizationIdentity", user.organizationIdentity);
70
+ Guards.array(EntityStorageAuthenticationAdminService.CLASS_NAME, "user.scope", user.scope);
71
+ await this.guardEscalatedPrivilegeScopeAssignment(user.scope);
72
+ try {
73
+ PasswordValidator.validatePassword(user.password, {
74
+ minLength: this._minPasswordLength
75
+ });
76
+ const existingUser = await this._userEntityStorage.get(user.email);
77
+ if (Is.object(existingUser)) {
78
+ throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "userExists");
79
+ }
80
+ const saltBytes = RandomHelper.generate(16);
81
+ const passwordBytes = Converter.utf8ToBytes(user.password);
82
+ const hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);
83
+ const newUser = {
84
+ email: user.email,
85
+ salt: Converter.bytesToBase64(saltBytes),
86
+ password: hashedPassword,
87
+ identity: user.userIdentity,
88
+ organization: user.organizationIdentity,
89
+ scope: ScopeHelper.toString(user.scope),
90
+ passwordVersion: 0
91
+ };
92
+ await this._userEntityStorage.set(newUser);
93
+ const contextIds = await ContextIdStore.getContextIds();
94
+ const requestorTenantId = contextIds?.[ContextIdKeys.Tenant];
95
+ const originalTenantId = contextIds?.[HttpContextIdKeys.OriginalTenant];
96
+ await this._authenticationAuditService?.create({
97
+ actorId: user.email,
98
+ event: AuthAuditEvent.AccountCreated,
99
+ data: {
100
+ userIdentity: user.userIdentity,
101
+ organizationIdentity: user.organizationIdentity,
102
+ tenantId: requestorTenantId,
103
+ originalTenantId,
104
+ scope: user.scope
105
+ }
106
+ });
107
+ }
108
+ catch (error) {
109
+ throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "createUserFailed", undefined, error);
110
+ }
111
+ }
112
+ /**
113
+ * Update a login for the user.
114
+ * @param user The user to update.
115
+ * @returns A promise that resolves when the user account has been updated and the audit entry recorded.
116
+ */
117
+ async update(user) {
118
+ Guards.object(EntityStorageAuthenticationAdminService.CLASS_NAME, "user", user);
119
+ Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "user.email", user.email);
120
+ if (!Is.empty(user.userIdentity)) {
121
+ Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "user.userIdentity", user.userIdentity);
122
+ }
123
+ if (!Is.empty(user.organizationIdentity)) {
124
+ Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "user.organizationIdentity", user.organizationIdentity);
125
+ }
126
+ if (!Is.empty(user.scope)) {
127
+ Guards.array(EntityStorageAuthenticationAdminService.CLASS_NAME, "user.scope", user.scope);
128
+ await this.guardEscalatedPrivilegeScopeAssignment(user.scope);
129
+ }
130
+ try {
131
+ const existingUser = await this._userEntityStorage.get(user.email);
132
+ if (!Is.object(existingUser)) {
133
+ throw new NotFoundError(EntityStorageAuthenticationAdminService.CLASS_NAME, "userNotFound", user.email);
134
+ }
135
+ await this.guardEscalatedPrivilegeTarget(existingUser.scope);
136
+ const updatedFields = [];
137
+ const updatedScope = Is.array(user.scope)
138
+ ? ScopeHelper.toString(user.scope)
139
+ : existingUser.scope;
140
+ if (user.userIdentity !== undefined && user.userIdentity !== existingUser.identity) {
141
+ updatedFields.push("userIdentity");
142
+ }
143
+ if (user.organizationIdentity !== undefined &&
144
+ user.organizationIdentity !== existingUser.organization) {
145
+ updatedFields.push("organizationIdentity");
146
+ }
147
+ if (Is.array(user.scope) && updatedScope !== existingUser.scope) {
148
+ updatedFields.push("scope");
149
+ }
150
+ existingUser.identity = user.userIdentity ?? existingUser.identity;
151
+ existingUser.organization = user.organizationIdentity ?? existingUser.organization;
152
+ existingUser.scope = Is.array(user.scope) ? updatedScope : existingUser.scope;
153
+ await this._userEntityStorage.set(existingUser);
154
+ const contextIds = await ContextIdStore.getContextIds();
155
+ const requestorTenantId = contextIds?.[ContextIdKeys.Tenant];
156
+ const originalTenantId = contextIds?.[HttpContextIdKeys.OriginalTenant];
157
+ await this._authenticationAuditService?.create({
158
+ actorId: existingUser.email,
159
+ event: AuthAuditEvent.AccountUpdated,
160
+ data: {
161
+ updatedFields,
162
+ userIdentity: existingUser.identity,
163
+ organizationIdentity: existingUser.organization,
164
+ tenantId: requestorTenantId,
165
+ originalTenantId,
166
+ scope: ScopeHelper.toArray(existingUser.scope)
167
+ }
168
+ });
169
+ }
170
+ catch (error) {
171
+ if (BaseError.isErrorName(error, ForbiddenError.CLASS_NAME)) {
172
+ throw error;
173
+ }
174
+ throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "updateUserFailed", undefined, error);
175
+ }
176
+ }
177
+ /**
178
+ * Get a user by email.
179
+ * @param email The email address of the user to get.
180
+ * @returns The user details.
181
+ */
182
+ async get(email) {
183
+ Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "email", email);
184
+ try {
185
+ const user = await this._userEntityStorage.get(email);
186
+ if (!Is.object(user)) {
187
+ throw new NotFoundError(EntityStorageAuthenticationAdminService.CLASS_NAME, "userNotFound", email);
188
+ }
189
+ return {
190
+ email: user.email,
191
+ userIdentity: user.identity,
192
+ organizationIdentity: user.organization,
193
+ scope: ScopeHelper.toArray(user.scope)
194
+ };
195
+ }
196
+ catch (error) {
197
+ throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "getUserFailed", undefined, error);
198
+ }
199
+ }
200
+ /**
201
+ * Get a user by identity.
202
+ * @param identity The identity of the user to get.
203
+ * @returns The user details.
204
+ */
205
+ async getByIdentity(identity) {
206
+ Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "identity", identity);
207
+ try {
208
+ const user = await this._userEntityStorage.get(identity, "identity");
209
+ if (!Is.object(user)) {
210
+ throw new NotFoundError(EntityStorageAuthenticationAdminService.CLASS_NAME, "userNotFound", identity);
211
+ }
212
+ return {
213
+ email: user.email,
214
+ userIdentity: user.identity,
215
+ organizationIdentity: user.organization,
216
+ scope: ScopeHelper.toArray(user.scope)
217
+ };
218
+ }
219
+ catch (error) {
220
+ throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "getUserFailed", undefined, error);
221
+ }
222
+ }
223
+ /**
224
+ * Remove the current user.
225
+ * @param email The email address of the user to remove.
226
+ * @returns A promise that resolves when the user account has been removed and the audit entry recorded.
227
+ */
228
+ async remove(email) {
229
+ Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "email", email);
230
+ try {
231
+ const user = await this._userEntityStorage.get(email);
232
+ if (!Is.object(user)) {
233
+ throw new NotFoundError(EntityStorageAuthenticationAdminService.CLASS_NAME, "userNotFound", email);
234
+ }
235
+ await this.guardEscalatedPrivilegeTarget(user.scope);
236
+ await this._userEntityStorage.remove(email);
237
+ const contextIds = await ContextIdStore.getContextIds();
238
+ const requestorTenantId = contextIds?.[ContextIdKeys.Tenant];
239
+ const originalTenantId = contextIds?.[HttpContextIdKeys.OriginalTenant];
240
+ await this._authenticationAuditService?.create({
241
+ actorId: email,
242
+ event: AuthAuditEvent.AccountDeleted,
243
+ data: {
244
+ userIdentity: user.identity,
245
+ organizationIdentity: user.organization,
246
+ tenantId: requestorTenantId,
247
+ originalTenantId,
248
+ scope: ScopeHelper.toArray(user.scope)
249
+ }
250
+ });
251
+ }
252
+ catch (error) {
253
+ if (BaseError.isErrorName(error, ForbiddenError.CLASS_NAME)) {
254
+ throw error;
255
+ }
256
+ throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "removeUserFailed", undefined, error);
257
+ }
258
+ }
259
+ /**
260
+ * Update the user's password.
261
+ * @param email The email address of the user to update.
262
+ * @param newPassword The new password for the user.
263
+ * @param currentPassword The current password, optional, if supplied will check against existing.
264
+ * @returns A promise that resolves when the password has been updated.
265
+ */
266
+ async updatePassword(email, newPassword, currentPassword) {
267
+ Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "email", email);
268
+ Guards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, "newPassword", newPassword);
269
+ try {
270
+ const user = await this._userEntityStorage.get(email);
271
+ if (!Is.object(user)) {
272
+ throw new NotFoundError(EntityStorageAuthenticationAdminService.CLASS_NAME, "userNotFound", email);
273
+ }
274
+ await this.guardEscalatedPrivilegeTarget(user.scope);
275
+ await PasswordHelper.updatePassword(this._userEntityStorage, this._authenticationAuditService, user, newPassword, currentPassword, this._minPasswordLength);
276
+ }
277
+ catch (error) {
278
+ if (BaseError.isErrorName(error, ForbiddenError.CLASS_NAME)) {
279
+ throw error;
280
+ }
281
+ throw new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, "updatePasswordFailed", undefined, error);
282
+ }
283
+ }
284
+ /**
285
+ * Throws ForbiddenError when the given scope list includes the escalated privilege scope but
286
+ * the calling request does not hold that scope itself.
287
+ * @param scopeList The scopes being assigned.
288
+ * @internal
289
+ */
290
+ async guardEscalatedPrivilegeScopeAssignment(scopeList) {
291
+ if (!ScopeHelper.includes(scopeList, this._escalatedPrivilegeScope)) {
292
+ return;
293
+ }
294
+ if (!(await this.callerHasEscalatedPrivilege())) {
295
+ throw new ForbiddenError(EntityStorageAuthenticationAdminService.CLASS_NAME, "insufficientScopeForEscalatedPrivilege", { scopeName: this._escalatedPrivilegeScope });
296
+ }
297
+ }
298
+ /**
299
+ * Throws ForbiddenError when the account being written to already holds the escalated privilege
300
+ * scope but the calling request does not hold that scope itself. Without this an admin with a
301
+ * lesser scope could reset the password of, remove, or demote a privileged account.
302
+ * @param targetScope The scopes currently held by the account being written to.
303
+ * @internal
304
+ */
305
+ async guardEscalatedPrivilegeTarget(targetScope) {
306
+ if (!ScopeHelper.includes(targetScope, this._escalatedPrivilegeScope)) {
307
+ return;
308
+ }
309
+ if (!(await this.callerHasEscalatedPrivilege())) {
310
+ throw new ForbiddenError(EntityStorageAuthenticationAdminService.CLASS_NAME, "insufficientScopeForEscalatedTarget", { scopeName: this._escalatedPrivilegeScope });
311
+ }
312
+ }
313
+ /**
314
+ * Does the calling request hold the escalated privilege scope.
315
+ * @returns True when the caller holds the escalated privilege scope.
316
+ * @internal
317
+ */
318
+ async callerHasEscalatedPrivilege() {
319
+ const contextIds = await ContextIdStore.getContextIds();
320
+ return ScopeHelper.includes(contextIds?.[HttpContextIdKeys.Scope], this._escalatedPrivilegeScope);
321
+ }
322
+ }
323
+ //# sourceMappingURL=entityStorageAuthenticationAdminService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entityStorageAuthenticationAdminService.js","sourceRoot":"","sources":["../../../src/services/entityStorageAuthenticationAdminService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type {\n\tIAuthenticationAdminComponent,\n\tIAuthenticationAuditComponent,\n\tIAuthenticationUser\n} from \"@3sixty/api-auth-entity-storage-models\";\nimport { AuthAuditEvent } from \"@3sixty/api-auth-entity-storage-models\";\nimport { ForbiddenError, HttpContextIdKeys, ScopeHelper } from \"@3sixty/api-models\";\nimport { ContextIdKeys, ContextIdStore } from \"@3sixty/context\";\nimport {\n\tBaseError,\n\tComponentFactory,\n\tConverter,\n\tGeneralError,\n\tGuards,\n\tIs,\n\tNotFoundError,\n\tRandomHelper\n} from \"@3sixty/core\";\nimport { PasswordGenerator, PasswordValidator } from \"@3sixty/crypto\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@3sixty/entity-storage-models\";\nimport { nameof } from \"@3sixty/nameof\";\nimport type { AuthenticationUser } from \"../entities/authenticationUser.js\";\nimport type { IEntityStorageAuthenticationAdminServiceConstructorOptions } from \"../models/IEntityStorageAuthenticationAdminServiceConstructorOptions.js\";\nimport { PasswordHelper } from \"../utils/passwordHelper.js\";\n\n/**\n * Implementation of the authentication component using entity storage.\n */\nexport class EntityStorageAuthenticationAdminService implements IAuthenticationAdminComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntityStorageAuthenticationAdminService>();\n\n\t/**\n\t * The scope value that grants permission to assign privileged roles.\n\t */\n\tpublic static readonly DEFAULT_ESCALATED_PRIVILEGE_SCOPE: string = \"global-admin\";\n\n\t/**\n\t * The entity storage for users.\n\t * @internal\n\t */\n\tprivate readonly _userEntityStorage: IEntityStorageConnector<AuthenticationUser>;\n\n\t/**\n\t * The audit service.\n\t * @internal\n\t */\n\tprivate readonly _authenticationAuditService?: IAuthenticationAuditComponent;\n\n\t/**\n\t * The minimum password length.\n\t * @internal\n\t */\n\tprivate readonly _minPasswordLength?: number;\n\n\t/**\n\t * The scope value that grants permission to assign privileged roles.\n\t * @internal\n\t */\n\tprivate readonly _escalatedPrivilegeScope: string;\n\n\t/**\n\t * Create a new instance of EntityStorageAuthentication.\n\t * @param options The dependencies for the identity connector.\n\t */\n\tconstructor(options?: IEntityStorageAuthenticationAdminServiceConstructorOptions) {\n\t\tthis._userEntityStorage = EntityStorageConnectorFactory.get(\n\t\t\toptions?.userEntityStorageType ?? \"authentication-user\"\n\t\t);\n\n\t\tthis._authenticationAuditService = ComponentFactory.getIfExists<IAuthenticationAuditComponent>(\n\t\t\toptions?.authenticationAuditServiceType ?? \"authentication-audit\"\n\t\t);\n\n\t\tthis._minPasswordLength = options?.config?.minPasswordLength;\n\t\tthis._escalatedPrivilegeScope = Is.stringValue(options?.config?.escalatedPrivilegeScope)\n\t\t\t? options?.config?.escalatedPrivilegeScope\n\t\t\t: EntityStorageAuthenticationAdminService.DEFAULT_ESCALATED_PRIVILEGE_SCOPE;\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn EntityStorageAuthenticationAdminService.CLASS_NAME;\n\t}\n\n\t/**\n\t * Create a login for the user.\n\t * @param user The user to create.\n\t * @returns A promise that resolves when the user account has been created and the audit entry recorded.\n\t */\n\tpublic async create(user: IAuthenticationUser & { password: string }): Promise<void> {\n\t\tGuards.object<IAuthenticationUser>(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user),\n\t\t\tuser\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.email),\n\t\t\tuser.email\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.password),\n\t\t\tuser.password\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.userIdentity),\n\t\t\tuser.userIdentity\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.organizationIdentity),\n\t\t\tuser.organizationIdentity\n\t\t);\n\t\tGuards.array<string>(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.scope),\n\t\t\tuser.scope\n\t\t);\n\n\t\tawait this.guardEscalatedPrivilegeScopeAssignment(user.scope);\n\n\t\ttry {\n\t\t\tPasswordValidator.validatePassword(user.password, {\n\t\t\t\tminLength: this._minPasswordLength\n\t\t\t});\n\n\t\t\tconst existingUser = await this._userEntityStorage.get(user.email);\n\t\t\tif (Is.object<AuthenticationUser>(existingUser)) {\n\t\t\t\tthrow new GeneralError(EntityStorageAuthenticationAdminService.CLASS_NAME, \"userExists\");\n\t\t\t}\n\n\t\t\tconst saltBytes = RandomHelper.generate(16);\n\t\t\tconst passwordBytes = Converter.utf8ToBytes(user.password);\n\n\t\t\tconst hashedPassword = await PasswordGenerator.hashPassword(passwordBytes, saltBytes);\n\n\t\t\tconst newUser: AuthenticationUser = {\n\t\t\t\temail: user.email,\n\t\t\t\tsalt: Converter.bytesToBase64(saltBytes),\n\t\t\t\tpassword: hashedPassword,\n\t\t\t\tidentity: user.userIdentity,\n\t\t\t\torganization: user.organizationIdentity,\n\t\t\t\tscope: ScopeHelper.toString(user.scope),\n\t\t\t\tpasswordVersion: 0\n\t\t\t};\n\n\t\t\tawait this._userEntityStorage.set(newUser);\n\n\t\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\t\tconst requestorTenantId = contextIds?.[ContextIdKeys.Tenant];\n\t\t\tconst originalTenantId = contextIds?.[HttpContextIdKeys.OriginalTenant];\n\t\t\tawait this._authenticationAuditService?.create({\n\t\t\t\tactorId: user.email,\n\t\t\t\tevent: AuthAuditEvent.AccountCreated,\n\t\t\t\tdata: {\n\t\t\t\t\tuserIdentity: user.userIdentity,\n\t\t\t\t\torganizationIdentity: user.organizationIdentity,\n\t\t\t\t\ttenantId: requestorTenantId,\n\t\t\t\t\toriginalTenantId,\n\t\t\t\t\tscope: user.scope\n\t\t\t\t}\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"createUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Update a login for the user.\n\t * @param user The user to update.\n\t * @returns A promise that resolves when the user account has been updated and the audit entry recorded.\n\t */\n\tpublic async update(user: Partial<IAuthenticationUser>): Promise<void> {\n\t\tGuards.object<IAuthenticationUser>(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user),\n\t\t\tuser\n\t\t);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(user.email),\n\t\t\tuser.email\n\t\t);\n\n\t\tif (!Is.empty(user.userIdentity)) {\n\t\t\tGuards.stringValue(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\tnameof(user.userIdentity),\n\t\t\t\tuser.userIdentity\n\t\t\t);\n\t\t}\n\t\tif (!Is.empty(user.organizationIdentity)) {\n\t\t\tGuards.stringValue(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\tnameof(user.organizationIdentity),\n\t\t\t\tuser.organizationIdentity\n\t\t\t);\n\t\t}\n\t\tif (!Is.empty(user.scope)) {\n\t\t\tGuards.array<string>(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\tnameof(user.scope),\n\t\t\t\tuser.scope\n\t\t\t);\n\t\t\tawait this.guardEscalatedPrivilegeScopeAssignment(user.scope);\n\t\t}\n\n\t\ttry {\n\t\t\tconst existingUser = await this._userEntityStorage.get(user.email);\n\t\t\tif (!Is.object<AuthenticationUser>(existingUser)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\tuser.email\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tawait this.guardEscalatedPrivilegeTarget(existingUser.scope);\n\n\t\t\tconst updatedFields: string[] = [];\n\t\t\tconst updatedScope = Is.array(user.scope)\n\t\t\t\t? ScopeHelper.toString(user.scope)\n\t\t\t\t: existingUser.scope;\n\n\t\t\tif (user.userIdentity !== undefined && user.userIdentity !== existingUser.identity) {\n\t\t\t\tupdatedFields.push(\"userIdentity\");\n\t\t\t}\n\t\t\tif (\n\t\t\t\tuser.organizationIdentity !== undefined &&\n\t\t\t\tuser.organizationIdentity !== existingUser.organization\n\t\t\t) {\n\t\t\t\tupdatedFields.push(\"organizationIdentity\");\n\t\t\t}\n\t\t\tif (Is.array(user.scope) && updatedScope !== existingUser.scope) {\n\t\t\t\tupdatedFields.push(\"scope\");\n\t\t\t}\n\n\t\t\texistingUser.identity = user.userIdentity ?? existingUser.identity;\n\t\t\texistingUser.organization = user.organizationIdentity ?? existingUser.organization;\n\t\t\texistingUser.scope = Is.array(user.scope) ? updatedScope : existingUser.scope;\n\n\t\t\tawait this._userEntityStorage.set(existingUser);\n\n\t\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\t\tconst requestorTenantId = contextIds?.[ContextIdKeys.Tenant];\n\t\t\tconst originalTenantId = contextIds?.[HttpContextIdKeys.OriginalTenant];\n\t\t\tawait this._authenticationAuditService?.create({\n\t\t\t\tactorId: existingUser.email,\n\t\t\t\tevent: AuthAuditEvent.AccountUpdated,\n\t\t\t\tdata: {\n\t\t\t\t\tupdatedFields,\n\t\t\t\t\tuserIdentity: existingUser.identity,\n\t\t\t\t\torganizationIdentity: existingUser.organization,\n\t\t\t\t\ttenantId: requestorTenantId,\n\t\t\t\t\toriginalTenantId,\n\t\t\t\t\tscope: ScopeHelper.toArray(existingUser.scope)\n\t\t\t\t}\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (BaseError.isErrorName(error, ForbiddenError.CLASS_NAME)) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"updateUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Get a user by email.\n\t * @param email The email address of the user to get.\n\t * @returns The user details.\n\t */\n\tpublic async get(email: string): Promise<IAuthenticationUser> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, nameof(email), email);\n\n\t\ttry {\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\temail\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\temail: user.email,\n\t\t\t\tuserIdentity: user.identity,\n\t\t\t\torganizationIdentity: user.organization,\n\t\t\t\tscope: ScopeHelper.toArray(user.scope)\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"getUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Get a user by identity.\n\t * @param identity The identity of the user to get.\n\t * @returns The user details.\n\t */\n\tpublic async getByIdentity(identity: string): Promise<IAuthenticationUser> {\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(identity),\n\t\t\tidentity\n\t\t);\n\n\t\ttry {\n\t\t\tconst user = await this._userEntityStorage.get(identity, \"identity\");\n\t\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\tidentity\n\t\t\t\t);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\temail: user.email,\n\t\t\t\tuserIdentity: user.identity,\n\t\t\t\torganizationIdentity: user.organization,\n\t\t\t\tscope: ScopeHelper.toArray(user.scope)\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"getUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Remove the current user.\n\t * @param email The email address of the user to remove.\n\t * @returns A promise that resolves when the user account has been removed and the audit entry recorded.\n\t */\n\tpublic async remove(email: string): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, nameof(email), email);\n\n\t\ttry {\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\temail\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tawait this.guardEscalatedPrivilegeTarget(user.scope);\n\n\t\t\tawait this._userEntityStorage.remove(email);\n\n\t\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\t\tconst requestorTenantId = contextIds?.[ContextIdKeys.Tenant];\n\t\t\tconst originalTenantId = contextIds?.[HttpContextIdKeys.OriginalTenant];\n\t\t\tawait this._authenticationAuditService?.create({\n\t\t\t\tactorId: email,\n\t\t\t\tevent: AuthAuditEvent.AccountDeleted,\n\t\t\t\tdata: {\n\t\t\t\t\tuserIdentity: user.identity,\n\t\t\t\t\torganizationIdentity: user.organization,\n\t\t\t\t\ttenantId: requestorTenantId,\n\t\t\t\t\toriginalTenantId,\n\t\t\t\t\tscope: ScopeHelper.toArray(user.scope)\n\t\t\t\t}\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tif (BaseError.isErrorName(error, ForbiddenError.CLASS_NAME)) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"removeUserFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Update the user's password.\n\t * @param email The email address of the user to update.\n\t * @param newPassword The new password for the user.\n\t * @param currentPassword The current password, optional, if supplied will check against existing.\n\t * @returns A promise that resolves when the password has been updated.\n\t */\n\tpublic async updatePassword(\n\t\temail: string,\n\t\tnewPassword: string,\n\t\tcurrentPassword?: string\n\t): Promise<void> {\n\t\tGuards.stringValue(EntityStorageAuthenticationAdminService.CLASS_NAME, nameof(email), email);\n\t\tGuards.stringValue(\n\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\tnameof(newPassword),\n\t\t\tnewPassword\n\t\t);\n\n\t\ttry {\n\t\t\tconst user = await this._userEntityStorage.get(email);\n\t\t\tif (!Is.object<AuthenticationUser>(user)) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\t\"userNotFound\",\n\t\t\t\t\temail\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tawait this.guardEscalatedPrivilegeTarget(user.scope);\n\n\t\t\tawait PasswordHelper.updatePassword(\n\t\t\t\tthis._userEntityStorage,\n\t\t\t\tthis._authenticationAuditService,\n\t\t\t\tuser,\n\t\t\t\tnewPassword,\n\t\t\t\tcurrentPassword,\n\t\t\t\tthis._minPasswordLength\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (BaseError.isErrorName(error, ForbiddenError.CLASS_NAME)) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tthrow new GeneralError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"updatePasswordFailed\",\n\t\t\t\tundefined,\n\t\t\t\terror\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Throws ForbiddenError when the given scope list includes the escalated privilege scope but\n\t * the calling request does not hold that scope itself.\n\t * @param scopeList The scopes being assigned.\n\t * @internal\n\t */\n\tprivate async guardEscalatedPrivilegeScopeAssignment(scopeList: string[]): Promise<void> {\n\t\tif (!ScopeHelper.includes(scopeList, this._escalatedPrivilegeScope)) {\n\t\t\treturn;\n\t\t}\n\t\tif (!(await this.callerHasEscalatedPrivilege())) {\n\t\t\tthrow new ForbiddenError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"insufficientScopeForEscalatedPrivilege\",\n\t\t\t\t{ scopeName: this._escalatedPrivilegeScope }\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Throws ForbiddenError when the account being written to already holds the escalated privilege\n\t * scope but the calling request does not hold that scope itself. Without this an admin with a\n\t * lesser scope could reset the password of, remove, or demote a privileged account.\n\t * @param targetScope The scopes currently held by the account being written to.\n\t * @internal\n\t */\n\tprivate async guardEscalatedPrivilegeTarget(targetScope: string | string[]): Promise<void> {\n\t\tif (!ScopeHelper.includes(targetScope, this._escalatedPrivilegeScope)) {\n\t\t\treturn;\n\t\t}\n\t\tif (!(await this.callerHasEscalatedPrivilege())) {\n\t\t\tthrow new ForbiddenError(\n\t\t\t\tEntityStorageAuthenticationAdminService.CLASS_NAME,\n\t\t\t\t\"insufficientScopeForEscalatedTarget\",\n\t\t\t\t{ scopeName: this._escalatedPrivilegeScope }\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Does the calling request hold the escalated privilege scope.\n\t * @returns True when the caller holds the escalated privilege scope.\n\t * @internal\n\t */\n\tprivate async callerHasEscalatedPrivilege(): Promise<boolean> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\treturn ScopeHelper.includes(\n\t\t\tcontextIds?.[HttpContextIdKeys.Scope],\n\t\t\tthis._escalatedPrivilegeScope\n\t\t);\n\t}\n}\n"]}
@@ -0,0 +1,240 @@
1
+ import { HttpContextIdKeys } from "@3sixty/api-models";
2
+ import { ContextIdStore, ContextIdKeys } from "@3sixty/context";
3
+ import { Converter, GeneralError, Guards, Is, NotFoundError, RandomHelper, Validation } from "@3sixty/core";
4
+ import { Sha256 } from "@3sixty/crypto";
5
+ import { ComparisonOperator } from "@3sixty/entity";
6
+ import { EntityStorageConnectorFactory } from "@3sixty/entity-storage-models";
7
+ /**
8
+ * Implementation of the authentication audit component using entity storage.
9
+ */
10
+ export class EntityStorageAuthenticationAuditService {
11
+ /**
12
+ * Runtime name for the class.
13
+ */
14
+ static CLASS_NAME = "EntityStorageAuthenticationAuditService";
15
+ /**
16
+ * The entity storage for authentication audit entries.
17
+ * @internal
18
+ */
19
+ _authenticationAuditEntryEntityStorage;
20
+ /**
21
+ * The server-side salt for hashing IP addresses in audit logs, if configured.
22
+ * @internal
23
+ */
24
+ _ipHashSalt;
25
+ /**
26
+ * Create a new instance of EntityStorageAuthenticationAuditService.
27
+ * @param options The dependencies for the identity connector.
28
+ */
29
+ constructor(options) {
30
+ this._authenticationAuditEntryEntityStorage = EntityStorageConnectorFactory.get(options?.authenticationAuditEntryStorageType ?? "authentication-audit-entry");
31
+ const salt = options?.config?.ipHashSalt?.trim();
32
+ if (Is.stringValue(salt)) {
33
+ const validationFailures = [];
34
+ Validation.stringValue("options.config.ipHashSalt", salt, validationFailures, undefined, { minLength: 32 });
35
+ const entropy = new Set(salt).size;
36
+ if (entropy < 8) {
37
+ validationFailures.push({
38
+ property: "options.config.ipHashSalt",
39
+ reason: "validation.saltEntropyTooLow"
40
+ });
41
+ }
42
+ Validation.asValidationError(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.config.ipHashSalt", validationFailures);
43
+ this._ipHashSalt = salt;
44
+ }
45
+ }
46
+ /**
47
+ * Returns the class name of the component.
48
+ * @returns The class name of the component.
49
+ */
50
+ className() {
51
+ return EntityStorageAuthenticationAuditService.CLASS_NAME;
52
+ }
53
+ /**
54
+ * Create a new audit entry.
55
+ * @param entry The audit entry to be logged.
56
+ * @returns The unique identifier of the created audit entry.
57
+ */
58
+ async create(entry) {
59
+ Guards.object(EntityStorageAuthenticationAuditService.CLASS_NAME, "entry", entry);
60
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "entry.event", entry.event);
61
+ const contextIds = await ContextIdStore.getContextIds();
62
+ const newAuditEntry = {
63
+ id: RandomHelper.generateUuidV7("compact"),
64
+ dateCreated: new Date().toISOString(),
65
+ event: entry.event,
66
+ actorId: entry.actorId ?? contextIds?.[ContextIdKeys.User],
67
+ nodeId: entry.nodeId ?? contextIds?.[ContextIdKeys.Node],
68
+ organizationId: entry.organizationId ?? contextIds?.[ContextIdKeys.Organization],
69
+ tenantId: entry.tenantId ?? contextIds?.[ContextIdKeys.Tenant],
70
+ data: entry.data,
71
+ ipAddressHashes: this.hashIpAddresses(contextIds?.[HttpContextIdKeys.IpAddress]?.split("|")),
72
+ userAgent: contextIds?.[HttpContextIdKeys.UserAgent],
73
+ correlationId: contextIds?.[HttpContextIdKeys.CorrelationId]
74
+ };
75
+ try {
76
+ await this._authenticationAuditEntryEntityStorage.set(newAuditEntry);
77
+ }
78
+ catch {
79
+ // Best-effort audit logging: do not interrupt auth/admin flows if persistence fails.
80
+ }
81
+ return newAuditEntry.id;
82
+ }
83
+ /**
84
+ * Get an audit entry by id.
85
+ * @param id The unique identifier of the audit entry.
86
+ * @returns The audit entry.
87
+ */
88
+ async get(id) {
89
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "id", id);
90
+ try {
91
+ const entry = await this._authenticationAuditEntryEntityStorage.get(id);
92
+ if (Is.empty(entry)) {
93
+ throw new NotFoundError(EntityStorageAuthenticationAuditService.CLASS_NAME, "auditEntryNotFound", id);
94
+ }
95
+ return entry;
96
+ }
97
+ catch (error) {
98
+ throw new GeneralError(EntityStorageAuthenticationAuditService.CLASS_NAME, "getAuditEntryFailed", undefined, error);
99
+ }
100
+ }
101
+ /**
102
+ * Update an audit entry.
103
+ * @param id The unique identifier of the audit entry to update.
104
+ * @param entry The fields to update on the audit entry.
105
+ * @returns A promise that resolves when the audit entry has been updated.
106
+ */
107
+ async update(id, entry) {
108
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "id", id);
109
+ Guards.object(EntityStorageAuthenticationAuditService.CLASS_NAME, "entry", entry);
110
+ try {
111
+ const existing = await this._authenticationAuditEntryEntityStorage.get(id);
112
+ if (Is.empty(existing)) {
113
+ throw new NotFoundError(EntityStorageAuthenticationAuditService.CLASS_NAME, "auditEntryNotFound", id);
114
+ }
115
+ await this._authenticationAuditEntryEntityStorage.set({
116
+ ...existing,
117
+ ...entry,
118
+ id,
119
+ dateCreated: existing.dateCreated
120
+ });
121
+ }
122
+ catch (error) {
123
+ throw new GeneralError(EntityStorageAuthenticationAuditService.CLASS_NAME, "updateAuditEntryFailed", undefined, error);
124
+ }
125
+ }
126
+ /**
127
+ * Remove an audit entry.
128
+ * @param id The unique identifier of the audit entry to remove.
129
+ * @returns A promise that resolves when the audit entry has been removed.
130
+ */
131
+ async remove(id) {
132
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "id", id);
133
+ try {
134
+ const existing = await this._authenticationAuditEntryEntityStorage.get(id);
135
+ if (Is.empty(existing)) {
136
+ throw new NotFoundError(EntityStorageAuthenticationAuditService.CLASS_NAME, "auditEntryNotFound", id);
137
+ }
138
+ await this._authenticationAuditEntryEntityStorage.remove(id);
139
+ }
140
+ catch (error) {
141
+ throw new GeneralError(EntityStorageAuthenticationAuditService.CLASS_NAME, "removeAuditEntryFailed", undefined, error);
142
+ }
143
+ }
144
+ /**
145
+ * Query the audit entries.
146
+ * @param options The query options.
147
+ * @param options.actorId The actor identifier to filter the audit entries, optional.
148
+ * @param options.organizationId The organization identifier to filter the audit entries, optional.
149
+ * @param options.tenantId The tenant identifier to filter the audit entries, optional.
150
+ * @param options.nodeId The node identifier to filter the audit entries, optional.
151
+ * @param options.event The audit event to filter the audit entries, optional.
152
+ * @param options.startDate The start date to filter the audit entries, optional.
153
+ * @param options.endDate The end date to filter the audit entries, optional.
154
+ * @param cursor The cursor for pagination.
155
+ * @param limit The maximum number of entries to return.
156
+ * @returns The audit entries.
157
+ */
158
+ async query(options, cursor, limit) {
159
+ const conditions = [];
160
+ if (Is.object(options)) {
161
+ if (!Is.empty(options.actorId)) {
162
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.actorId", options.actorId);
163
+ conditions.push({
164
+ property: "actorId",
165
+ value: options.actorId,
166
+ comparison: ComparisonOperator.Equals
167
+ });
168
+ }
169
+ if (!Is.empty(options.organizationId)) {
170
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.organizationId", options.organizationId);
171
+ conditions.push({
172
+ property: "organizationId",
173
+ value: options.organizationId,
174
+ comparison: ComparisonOperator.Equals
175
+ });
176
+ }
177
+ if (!Is.empty(options.tenantId)) {
178
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.tenantId", options.tenantId);
179
+ conditions.push({
180
+ property: "tenantId",
181
+ value: options.tenantId,
182
+ comparison: ComparisonOperator.Equals
183
+ });
184
+ }
185
+ if (!Is.empty(options.nodeId)) {
186
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.nodeId", options.nodeId);
187
+ conditions.push({
188
+ property: "nodeId",
189
+ value: options.nodeId,
190
+ comparison: ComparisonOperator.Equals
191
+ });
192
+ }
193
+ if (!Is.empty(options.event)) {
194
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.event", options.event);
195
+ conditions.push({
196
+ property: "event",
197
+ value: options.event,
198
+ comparison: ComparisonOperator.Equals
199
+ });
200
+ }
201
+ if (!Is.empty(options.startDate)) {
202
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.startDate", options.startDate);
203
+ conditions.push({
204
+ property: "dateCreated",
205
+ value: options.startDate,
206
+ comparison: ComparisonOperator.GreaterThanOrEqual
207
+ });
208
+ }
209
+ if (!Is.empty(options.endDate)) {
210
+ Guards.stringValue(EntityStorageAuthenticationAuditService.CLASS_NAME, "options.endDate", options.endDate);
211
+ conditions.push({
212
+ property: "dateCreated",
213
+ value: options.endDate,
214
+ comparison: ComparisonOperator.LessThanOrEqual
215
+ });
216
+ }
217
+ }
218
+ const result = await this._authenticationAuditEntryEntityStorage.query(conditions.length > 0 ? { conditions } : undefined, undefined, undefined, cursor, limit);
219
+ return {
220
+ entries: result.entities,
221
+ cursor: result.cursor
222
+ };
223
+ }
224
+ /**
225
+ * Hash a list of IP addresses using SHA-256.
226
+ * @param ipAddresses The IP addresses to hash.
227
+ * @returns The hexadecimal hashes of the salted IPs.
228
+ * @internal
229
+ */
230
+ hashIpAddresses(ipAddresses) {
231
+ if (!Is.stringValue(this._ipHashSalt) || !Is.array(ipAddresses)) {
232
+ return undefined;
233
+ }
234
+ return ipAddresses.map(ip => {
235
+ const hash = Sha256.sum256(Converter.utf8ToBytes(`${this._ipHashSalt}:${ip}`));
236
+ return Converter.bytesToHex(hash);
237
+ });
238
+ }
239
+ }
240
+ //# sourceMappingURL=entityStorageAuthenticationAuditService.js.map