@memberjunction/server 6.1.5 → 6.1.6
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/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/SessionManager.d.ts +5 -2
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +17 -8
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
- package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
- package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
- package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
- package/dist/agentSessions/index.d.ts +1 -0
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +1 -0
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/auth/actionAuthorization.d.ts +40 -0
- package/dist/auth/actionAuthorization.d.ts.map +1 -0
- package/dist/auth/actionAuthorization.js +94 -0
- package/dist/auth/actionAuthorization.js.map +1 -0
- package/dist/auth/exampleNewUserSubClass.js +2 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +6 -3
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +15 -11
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +13 -11
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +3417 -3428
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +44 -8
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +59 -19
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +95 -11
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +186 -76
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +12 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/isolatedProvider.d.ts +20 -0
- package/dist/isolatedProvider.d.ts.map +1 -0
- package/dist/isolatedProvider.js +22 -0
- package/dist/isolatedProvider.js.map +1 -0
- package/dist/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +22 -2
- package/dist/multiTenancy/index.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +4 -2
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +4 -2
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +31 -5
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +75 -14
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +28 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +46 -0
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +24 -3
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +57 -8
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +8 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +11 -48
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +2 -2
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +117 -38
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +13 -1
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +2 -1
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +39 -16
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +84 -38
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +12 -11
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts +1 -0
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +5 -0
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +1 -0
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +6 -2
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
- package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.js +14 -10
- package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
- package/dist/rest/ViewOperationsHandler.d.ts +3 -1
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +29 -17
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/package.json +92 -92
- package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +4 -2
- package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
- package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
- package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +106 -41
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +227 -0
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +24 -2
- package/src/__tests__/ResolverBase.isaPromotionCreate.test.ts +412 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
- package/src/__tests__/SessionManager.test.ts +94 -17
- package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +2 -0
- package/src/__tests__/TaskGraphActionRunner.storableOutputs.test.ts +76 -0
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
- package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
- package/src/__tests__/magicLinkService.consume.test.ts +6 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
- package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
- package/src/__tests__/multiTenancy.security.test.ts +51 -0
- package/src/__tests__/multiTenancy.test.ts +1 -1
- package/src/__tests__/newUsers.test.ts +62 -5
- package/src/agentSessions/SessionManager.ts +23 -25
- package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
- package/src/agentSessions/index.ts +1 -0
- package/src/auth/actionAuthorization.ts +119 -0
- package/src/auth/exampleNewUserSubClass.ts +2 -2
- package/src/auth/magicLink/MagicLinkService.ts +6 -3
- package/src/auth/newUsers.ts +15 -11
- package/src/context.ts +14 -10
- package/src/generated/graphql-schemas/__mj.ts +3417 -3428
- package/src/generic/CacheInvalidationResolver.ts +74 -19
- package/src/generic/ResolverBase.ts +207 -80
- package/src/generic/RunViewResolver.ts +17 -0
- package/src/index.ts +9 -1
- package/src/isolatedProvider.ts +22 -0
- package/src/multiTenancy/index.ts +24 -2
- package/src/realtimeWidget/widgetGuestElevation.ts +4 -2
- package/src/resolvers/ActionResolver.ts +84 -21
- package/src/resolvers/AdhocQueryResolver.ts +49 -1
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +11 -52
- package/src/resolvers/FetchEntityVectorsResolver.ts +2 -2
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +14 -1
- package/src/resolvers/QuerySystemUserResolver.ts +2 -1
- package/src/resolvers/RealtimeClientSessionResolver.ts +103 -42
- package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/SearchKnowledgeResolver.ts +4 -0
- package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +1 -0
- package/src/resolvers/SyncDataResolver.ts +6 -2
- package/src/resolvers/VectorizeEntityResolver.ts +16 -11
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/rest/ViewOperationsHandler.ts +37 -18
- package/src/services/TaskGraphActionRunner.ts +29 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { AuthorizationInfo, AuthorizationRoleInfo, AuthorizationRoleType, IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
2
|
+
import { NormalizeUUID, UUIDsEqual } from '@memberjunction/global';
|
|
3
|
+
|
|
4
|
+
/** Authorizations keyed by their normalized ID. */
|
|
5
|
+
type AuthorizationsByID = Map<string, AuthorizationInfo>;
|
|
6
|
+
|
|
7
|
+
/** The outcome of {@link EvaluateActionRunAuthorization}. */
|
|
8
|
+
export interface ActionRunAuthorization {
|
|
9
|
+
/** True when the user may run the action. */
|
|
10
|
+
Allowed: boolean;
|
|
11
|
+
/** Why the run is allowed or refused. */
|
|
12
|
+
Reason: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** One provider's authorization tree and the role grants on it. */
|
|
16
|
+
export type AuthorizationMetadata = Pick<IMetadataProvider, 'Authorizations' | 'AuthorizationRoles'>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* True when the user is an Owner, MJ's superuser type. The column is nchar, so the stored value is
|
|
20
|
+
* padded.
|
|
21
|
+
*/
|
|
22
|
+
export function IsOwner(user: UserInfo): boolean {
|
|
23
|
+
return user.Type?.trim().toLowerCase() === 'owner';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Decides whether a user may run an action through a public entry point such as the `RunAction`
|
|
28
|
+
* mutation. It fails closed:
|
|
29
|
+
*
|
|
30
|
+
* - An Owner may run any action.
|
|
31
|
+
* - An action with `MJ: Action Authorizations` rows runs for a user who holds at least one of the
|
|
32
|
+
* linked authorizations. A linked authorization counts only when it is active and none of the
|
|
33
|
+
* user's roles is denied it; the user may then hold it directly or through an ancestor.
|
|
34
|
+
* - An action with no such rows runs for any user when its class authorizes its caller itself
|
|
35
|
+
* (`BaseAction.AuthorizesCaller`), so the action's own check decides. Otherwise it runs for
|
|
36
|
+
* Owners only.
|
|
37
|
+
*
|
|
38
|
+
* The authorizations, their parents and their role grants are read only from `metadata`, never
|
|
39
|
+
* from the global provider. Pass the provider the `MJ: Action Authorizations` rows were read from.
|
|
40
|
+
*
|
|
41
|
+
* Refuse scope-limited sessions before calling this; see `IsScopeLimitedPrincipal`.
|
|
42
|
+
*
|
|
43
|
+
* @param user the caller
|
|
44
|
+
* @param actionAuthorizationIDs the AuthorizationID of each `MJ: Action Authorizations` row for the action
|
|
45
|
+
* @param metadata the provider whose authorizations and role grants decide
|
|
46
|
+
* @param actionAuthorizesCaller true when the action's class authorizes its caller itself
|
|
47
|
+
* @returns whether the run is allowed, and why
|
|
48
|
+
*/
|
|
49
|
+
export function EvaluateActionRunAuthorization(
|
|
50
|
+
user: UserInfo,
|
|
51
|
+
actionAuthorizationIDs: readonly string[],
|
|
52
|
+
metadata: AuthorizationMetadata,
|
|
53
|
+
actionAuthorizesCaller: boolean
|
|
54
|
+
): ActionRunAuthorization {
|
|
55
|
+
if (IsOwner(user)) {
|
|
56
|
+
return { Allowed: true, Reason: 'The user is an Owner.' };
|
|
57
|
+
}
|
|
58
|
+
if (actionAuthorizationIDs.length === 0) {
|
|
59
|
+
return actionAuthorizesCaller
|
|
60
|
+
? { Allowed: true, Reason: 'The action authorizes its caller itself.' }
|
|
61
|
+
: { Allowed: false, Reason: 'No Action Authorization grants this action to any role, and the action does not authorize its caller itself; only an Owner may run it.' };
|
|
62
|
+
}
|
|
63
|
+
const authorizationsByID: AuthorizationsByID = new Map(metadata.Authorizations.map((auth) => [NormalizeUUID(auth.ID), auth]));
|
|
64
|
+
const userGrants = grantsToUserRoles(user, metadata.AuthorizationRoles);
|
|
65
|
+
const granted = actionAuthorizationIDs
|
|
66
|
+
.map((id) => authorizationsByID.get(NormalizeUUID(id)))
|
|
67
|
+
.filter((auth): auth is AuthorizationInfo => auth !== undefined)
|
|
68
|
+
.find((auth) => holdsLinkedAuthorization(auth, authorizationsByID, userGrants));
|
|
69
|
+
return granted
|
|
70
|
+
? { Allowed: true, Reason: `The user holds the "${granted.Name}" authorization.` }
|
|
71
|
+
: { Allowed: false, Reason: 'None of your roles holds an authorization this action requires.' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The role grants that name one of the user's roles. */
|
|
75
|
+
function grantsToUserRoles(user: UserInfo, grants: readonly AuthorizationRoleInfo[]): AuthorizationRoleInfo[] {
|
|
76
|
+
const userRoleIDs = new Set((user.UserRoles ?? []).map((userRole) => NormalizeUUID(userRole.RoleID)));
|
|
77
|
+
return grants.filter((grant) => userRoleIDs.has(NormalizeUUID(grant.RoleID)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* True when the user holds a linked authorization: it is active, none of the user's roles is denied
|
|
82
|
+
* it, and the user holds it directly or through an ancestor. The first two are checked on the linked
|
|
83
|
+
* authorization itself, because the ancestor walk would otherwise grant it through the parent.
|
|
84
|
+
*/
|
|
85
|
+
function holdsLinkedAuthorization(auth: AuthorizationInfo, authorizationsByID: AuthorizationsByID, userGrants: readonly AuthorizationRoleInfo[]): boolean {
|
|
86
|
+
if (!auth.IsActive || grantTypes(auth, userGrants).includes(AuthorizationRoleType.Deny)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return holdsDirectlyOrThroughAncestor(auth, authorizationsByID, userGrants);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* True when the user holds the authorization or one of its ancestors, following `ParentID` within
|
|
94
|
+
* `authorizationsByID`. This is the walk `AuthorizationEvaluator.UserCanExecuteWithAncestors` does,
|
|
95
|
+
* with the role grants taken from the same provider as the authorizations.
|
|
96
|
+
*/
|
|
97
|
+
function holdsDirectlyOrThroughAncestor(auth: AuthorizationInfo, authorizationsByID: AuthorizationsByID, userGrants: readonly AuthorizationRoleInfo[]): boolean {
|
|
98
|
+
const visited = new Set<string>();
|
|
99
|
+
let node: AuthorizationInfo | undefined = auth;
|
|
100
|
+
while (node && !visited.has(NormalizeUUID(node.ID))) {
|
|
101
|
+
if (holdsDirectly(node, userGrants)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
visited.add(NormalizeUUID(node.ID));
|
|
105
|
+
node = node.ParentID ? authorizationsByID.get(NormalizeUUID(node.ParentID)) : undefined;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** True when the authorization is active and the user's roles have an Allow and no Deny on it. */
|
|
111
|
+
function holdsDirectly(auth: AuthorizationInfo, userGrants: readonly AuthorizationRoleInfo[]): boolean {
|
|
112
|
+
const types = grantTypes(auth, userGrants);
|
|
113
|
+
return auth.IsActive && types.includes(AuthorizationRoleType.Allow) && !types.includes(AuthorizationRoleType.Deny);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Allow or Deny, for each of the user's role grants on the authorization. */
|
|
117
|
+
function grantTypes(auth: AuthorizationInfo, userGrants: readonly AuthorizationRoleInfo[]): AuthorizationRoleType[] {
|
|
118
|
+
return userGrants.filter((grant) => UUIDsEqual(grant.AuthorizationID, auth.ID)).map((grant) => grant.AuthorizationType());
|
|
119
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RegisterClass } from '@memberjunction/global';
|
|
1
|
+
import { EscapeSQLString, RegisterClass } from '@memberjunction/global';
|
|
2
2
|
import { Metadata, RunView, LogError, EntitySaveOptions } from '@memberjunction/core';
|
|
3
3
|
import { NewUserBase } from './newUsers.js';
|
|
4
4
|
import { ResolveConfiguredPrincipal } from './principals.js';
|
|
@@ -43,7 +43,7 @@ export class ExampleNewUserSubClass extends NewUserBase {
|
|
|
43
43
|
const viewResults = await rv.RunView(
|
|
44
44
|
{
|
|
45
45
|
EntityName: 'Persons',
|
|
46
|
-
ExtraFilter: `Email = '${email}'`,
|
|
46
|
+
ExtraFilter: `Email = '${EscapeSQLString(email)}'`,
|
|
47
47
|
},
|
|
48
48
|
contextUser
|
|
49
49
|
);
|
|
@@ -34,6 +34,7 @@ import { ResolveConfiguredPrincipal } from '../principals.js';
|
|
|
34
34
|
import { CommunicationEngine } from '@memberjunction/communication-engine';
|
|
35
35
|
import { Message } from '@memberjunction/communication-types';
|
|
36
36
|
import { configInfo, type MagicLinkConfig } from '../../config.js';
|
|
37
|
+
import { CreateIsolatedProvider } from '../../isolatedProvider.js';
|
|
37
38
|
import { MagicLinkKeyManager } from './MagicLinkKeys.js';
|
|
38
39
|
import { generateRawToken, generateSessionId, hashToken, evaluateInvite, buildSessionClaims, buildConsumeInvitePostgresSQL, canIssueInvites, isRoleGrantable, MAGIC_LINK_TOKEN_PREFIX } from './magicLinkCore.js';
|
|
39
40
|
import type {
|
|
@@ -521,9 +522,11 @@ export class MagicLinkService {
|
|
|
521
522
|
/** Creates a new user with exactly the restricted role + single app, transactionally. */
|
|
522
523
|
private async createScopedUser(invite: MJMagicLinkInviteEntity, role: RoleInfo, contextUser: UserInfo): Promise<ProvisionResult> {
|
|
523
524
|
// Single provider for BOTH the transaction and the entity writes, so the
|
|
524
|
-
// BeginTransaction/Save/Commit are provably on the same connection.
|
|
525
|
-
//
|
|
526
|
-
|
|
525
|
+
// BeginTransaction/Save/Commit are provably on the same connection. Redemption runs
|
|
526
|
+
// pre-auth, with no per-request provider yet, and the global Metadata.Provider is shared
|
|
527
|
+
// by every concurrent request: a transaction opened there nests into anyone else's and
|
|
528
|
+
// swallows their writes. So it gets its own (see CreateIsolatedProvider).
|
|
529
|
+
const provider = await CreateIsolatedProvider();
|
|
527
530
|
const email = invite.Email;
|
|
528
531
|
// The invite does not persist a name; provision a sensible default. (The
|
|
529
532
|
// create-invite params accept first/last name but there is no column to
|
package/src/auth/newUsers.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { ApplicationInfo,
|
|
1
|
+
import { ApplicationInfo, EntitySaveOptions, LogError, LogStatus, RunView, RunViewResult, UserInfo } from "@memberjunction/core";
|
|
2
2
|
import { RegisterClass } from "@memberjunction/global";
|
|
3
3
|
import { ResolveConfiguredPrincipal } from "./principals.js";
|
|
4
4
|
import { configInfo } from "../config.js";
|
|
5
|
+
import { CreateIsolatedProvider } from "../isolatedProvider.js";
|
|
5
6
|
import { MJUserEntity, MJUserRoleEntity, MJUserApplicationEntity, MJUserApplicationEntityEntity, MJApplicationEntityType, MJApplicationEntityEntityType, UserInfoEngine } from "@memberjunction/core-entities";
|
|
6
7
|
|
|
7
8
|
export class NewUserBase {
|
|
@@ -16,8 +17,12 @@ export class NewUserBase {
|
|
|
16
17
|
return null;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
// Everything here, metadata lookups, entity objects and the transaction, goes through ONE
|
|
21
|
+
// isolated provider. This runs in the JWT auth flow, before AppContext.providers exists, and
|
|
22
|
+
// the global Metadata.Provider is shared by every concurrent request: a transaction opened
|
|
23
|
+
// there nests into anyone else's and swallows their writes (see CreateIsolatedProvider).
|
|
24
|
+
const provider = await CreateIsolatedProvider();
|
|
25
|
+
const user = await provider.GetEntityObject<MJUserEntity>('MJ: Users', contextUser) // To-Do - change this to be a different defined user for the user creation process
|
|
21
26
|
user.NewRecord();
|
|
22
27
|
user.Name = email;
|
|
23
28
|
user.IsActive = true;
|
|
@@ -38,7 +43,6 @@ export class NewUserBase {
|
|
|
38
43
|
// Create the user and all of its role/application/app-entity records atomically.
|
|
39
44
|
// If any Save fails partway through, the whole provisioning rolls back so we never
|
|
40
45
|
// leave a half-created user with partial roles/applications behind.
|
|
41
|
-
const provider = Metadata.Provider as DatabaseProviderBase; // global-provider-ok: new-user creation runs in the JWT auth flow, before AppContext.providers is built
|
|
42
46
|
await provider.BeginTransaction();
|
|
43
47
|
try {
|
|
44
48
|
if (!await user.Save()) {
|
|
@@ -48,13 +52,13 @@ export class NewUserBase {
|
|
|
48
52
|
if(configInfo.userHandling && configInfo.userHandling.newUserRoles){
|
|
49
53
|
LogStatus(`User ${user.Email} created, assigning roles`);
|
|
50
54
|
for (const role of configInfo.userHandling.newUserRoles) {
|
|
51
|
-
const userRole =
|
|
55
|
+
const userRole = provider.Roles.find(r => r.Name === role);
|
|
52
56
|
if (!userRole) {
|
|
53
57
|
LogError(`Role ${role} not found in the database, cannot assign to new user ${user.Name}`);
|
|
54
58
|
continue;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
const userRoleEntity: MJUserRoleEntity = await
|
|
61
|
+
const userRoleEntity: MJUserRoleEntity = await provider.GetEntityObject<MJUserRoleEntity>('MJ: User Roles', contextUser);
|
|
58
62
|
userRoleEntity.NewRecord();
|
|
59
63
|
userRoleEntity.UserID = user.ID;
|
|
60
64
|
userRoleEntity.RoleID = userRole.ID;
|
|
@@ -73,7 +77,7 @@ export class NewUserBase {
|
|
|
73
77
|
if (configInfo.userHandling.UserApplications && configInfo.userHandling.UserApplications.length > 0) {
|
|
74
78
|
for (const appName of configInfo.userHandling.UserApplications) {
|
|
75
79
|
const toLowerCase: string = appName.trim().toLocaleLowerCase();
|
|
76
|
-
const application: ApplicationInfo | undefined =
|
|
80
|
+
const application: ApplicationInfo | undefined = provider.Applications.find(a => a.Name.trim().toLocaleLowerCase() === toLowerCase);
|
|
77
81
|
if (application) {
|
|
78
82
|
applicationsToCreate.push(application);
|
|
79
83
|
} else {
|
|
@@ -85,12 +89,12 @@ export class NewUserBase {
|
|
|
85
89
|
// Bug F2: use the shared selector, which filters to Active apps flagged
|
|
86
90
|
// DefaultForNewUser (in DefaultSequence order). This path previously omitted the
|
|
87
91
|
// Status = 'Active' check, so an inactive default app could be provisioned here.
|
|
88
|
-
applicationsToCreate = UserInfoEngine.GetDefaultApplicationsForNewUser(
|
|
92
|
+
applicationsToCreate = UserInfoEngine.GetDefaultApplicationsForNewUser(provider);
|
|
89
93
|
LogStatus(`Found ${applicationsToCreate.length} active applications with DefaultForNewUser=true`);
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
for (const [appIndex, application] of applicationsToCreate.entries()) {
|
|
93
|
-
const userApplication: MJUserApplicationEntity = await
|
|
97
|
+
const userApplication: MJUserApplicationEntity = await provider.GetEntityObject<MJUserApplicationEntity>('MJ: User Applications', contextUser);
|
|
94
98
|
userApplication.NewRecord();
|
|
95
99
|
userApplication.UserID = user.ID;
|
|
96
100
|
userApplication.ApplicationID = application.ID;
|
|
@@ -102,7 +106,7 @@ export class NewUserBase {
|
|
|
102
106
|
}
|
|
103
107
|
LogStatus(`Created User Application ${application.Name} for new user ${user.Name}`);
|
|
104
108
|
|
|
105
|
-
const rv: RunView =
|
|
109
|
+
const rv: RunView = RunView.FromMetadataProvider(provider);
|
|
106
110
|
// Boolean literal goes through the active provider's dialect:
|
|
107
111
|
// SQL Server emits `= 1`, PostgreSQL emits `= TRUE`.
|
|
108
112
|
// This keeps the filter server-side (no client-side `.filter()`
|
|
@@ -125,7 +129,7 @@ export class NewUserBase {
|
|
|
125
129
|
LogStatus(`Creating ${defaultForNewUserEntities.length} User Application Entities for User Application ${application.Name} for new user ${user.Name}`);
|
|
126
130
|
|
|
127
131
|
for(const [index, appEntity] of defaultForNewUserEntities.entries()){
|
|
128
|
-
const userAppEntity: MJUserApplicationEntityEntity = await
|
|
132
|
+
const userAppEntity: MJUserApplicationEntityEntity = await provider.GetEntityObject<MJUserApplicationEntityEntity>('MJ: User Application Entities', contextUser);
|
|
129
133
|
userAppEntity.NewRecord();
|
|
130
134
|
userAppEntity.UserApplicationID = userApplication.ID;
|
|
131
135
|
userAppEntity.EntityID = appEntity.EntityID;
|
package/src/context.ts
CHANGED
|
@@ -798,21 +798,25 @@ async function createPerRequestProviders(
|
|
|
798
798
|
{ provider: p, type: 'Read-Write' }
|
|
799
799
|
];
|
|
800
800
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
providers.push({ provider: rp, type: 'Read-Only' });
|
|
805
|
-
}
|
|
806
|
-
} else {
|
|
807
|
-
const rp = await tryCreateReadOnlyProvider(dataSources);
|
|
808
|
-
if (rp) {
|
|
809
|
-
providers.push({ provider: rp, type: 'Read-Only' });
|
|
810
|
-
}
|
|
801
|
+
const rp = await CreateReadOnlyProvider(dataSources);
|
|
802
|
+
if (rp) {
|
|
803
|
+
providers.push({ provider: rp, type: 'Read-Only' });
|
|
811
804
|
}
|
|
812
805
|
|
|
813
806
|
return providers;
|
|
814
807
|
}
|
|
815
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Opens a provider on the read-only database login, for the current platform. Resolves null when no
|
|
811
|
+
* read-only login is configured; it never falls back to the read-write pool.
|
|
812
|
+
* @param dataSources the data sources MJAPI opened at startup
|
|
813
|
+
*/
|
|
814
|
+
export async function CreateReadOnlyProvider(dataSources: DataSourceInfo[]): Promise<DatabaseProviderBase | null> {
|
|
815
|
+
return resolveDbPlatformFromEnv() === 'postgresql'
|
|
816
|
+
? tryCreateReadOnlyPostgresProvider()
|
|
817
|
+
: tryCreateReadOnlyProvider(dataSources);
|
|
818
|
+
}
|
|
819
|
+
|
|
816
820
|
/**
|
|
817
821
|
* Creates a PostgreSQL per-request provider, sharing the connection pool
|
|
818
822
|
* from the primary provider to avoid pool exhaustion.
|