backend-manager 4.1.2 → 4.2.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.
- package/package.json +1 -1
- package/src/manager/functions/core/actions/api/admin/get-stats.js +1 -1
- package/src/manager/functions/core/actions/api/admin/send-notification.js +167 -133
- package/src/manager/functions/core/actions/api.js +44 -13
- package/src/manager/functions/core/admin/get-stats.js +1 -1
- package/src/manager/functions/core/events/firestore/{on-subscription.js → notifications/on-write.js} +4 -4
- package/src/manager/helpers/assistant-old-auth.js +116 -0
- package/src/manager/helpers/assistant.js +117 -69
- package/src/manager/helpers/settings.js +3 -3
- package/src/manager/index.js +3 -3
- package/templates/backend-manager-tests.js +4 -4
- package/templates/firestore.rules +4 -4
- package/src/manager/helpers/assistant-new.js +0 -1051
|
@@ -69,7 +69,7 @@ function tryUrl(self) {
|
|
|
69
69
|
: `${protocol}://${host}/${self.meta.name}`;
|
|
70
70
|
}
|
|
71
71
|
} else if (projectType === 'custom') {
|
|
72
|
-
return
|
|
72
|
+
return `@TODO`;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
return '';
|
|
@@ -598,22 +598,44 @@ function _attachHeaderProperties(self, options, error) {
|
|
|
598
598
|
BackendAssistant.prototype.authenticate = async function (options) {
|
|
599
599
|
const self = this;
|
|
600
600
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
601
|
+
// Shortcuts
|
|
602
|
+
const admin = self.ref.admin;
|
|
603
|
+
const functions = self.ref.functions;
|
|
604
|
+
const req = self.ref.req;
|
|
605
|
+
const res = self.ref.res;
|
|
606
|
+
const data = self.request.data;
|
|
607
|
+
|
|
608
|
+
// Get stored backendManagerKey
|
|
609
|
+
const BACKEND_MANAGER_KEY = self.Manager?.config?.backend_manager?.key || '';
|
|
610
|
+
|
|
611
|
+
// Build the ID token from the request
|
|
606
612
|
let idToken;
|
|
613
|
+
let backendManagerKey;
|
|
614
|
+
// let user;
|
|
607
615
|
|
|
616
|
+
// Set options
|
|
608
617
|
options = options || {};
|
|
609
618
|
options.resolve = typeof options.resolve === 'undefined' ? true : options.resolve;
|
|
619
|
+
options.debug = typeof options.debug === 'undefined' ? false : options.debug;
|
|
610
620
|
|
|
611
621
|
function _resolve(user) {
|
|
622
|
+
// Resolve the properties
|
|
612
623
|
user = user || {};
|
|
613
624
|
user.authenticated = typeof user.authenticated === 'undefined'
|
|
614
625
|
? false
|
|
615
626
|
: user.authenticated;
|
|
616
627
|
|
|
628
|
+
// Validate BACKEND_MANAGER_KEY
|
|
629
|
+
if (backendManagerKey && backendManagerKey === BACKEND_MANAGER_KEY) {
|
|
630
|
+
// Update roles
|
|
631
|
+
user.roles = user.roles || {};
|
|
632
|
+
user.roles.admin = true;
|
|
633
|
+
|
|
634
|
+
// Set authenticated
|
|
635
|
+
user.authenticated = true;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Resolve the user
|
|
617
639
|
if (options.resolve) {
|
|
618
640
|
self.request.user = self.resolveAccount(user);
|
|
619
641
|
return self.request.user;
|
|
@@ -622,91 +644,102 @@ BackendAssistant.prototype.authenticate = async function (options) {
|
|
|
622
644
|
}
|
|
623
645
|
}
|
|
624
646
|
|
|
625
|
-
|
|
647
|
+
// Get shortcuts
|
|
648
|
+
const authHeader = req?.headers?.authorization || '';
|
|
649
|
+
|
|
650
|
+
// Extract the BEM token
|
|
651
|
+
// Having this is separate from the ID token allows for the user to be authenticated as an ADMIN
|
|
652
|
+
if (options.backendManagerKey || data.backendManagerKey) {
|
|
653
|
+
// Read token from backendManagerKey or authenticationToken or apiKey
|
|
654
|
+
backendManagerKey = options.backendManagerKey || data.backendManagerKey;
|
|
655
|
+
|
|
656
|
+
// Log the token
|
|
657
|
+
self.log('Found "backendManagerKey" parameter', backendManagerKey);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Extract the token / API key
|
|
661
|
+
// This is the main token that will be used to authenticate the user (it can be a JWT or a user's API key)
|
|
662
|
+
if (authHeader.startsWith('Bearer ')) {
|
|
626
663
|
// Read the ID Token from the Authorization header.
|
|
627
|
-
idToken =
|
|
664
|
+
idToken = authHeader.split('Bearer ')[1];
|
|
665
|
+
|
|
666
|
+
// Log the token
|
|
628
667
|
self.log('Found "Authorization" header', idToken);
|
|
629
668
|
} else if (req?.cookies?.__session) {
|
|
630
669
|
// Read the ID Token from cookie.
|
|
631
670
|
idToken = req.cookies.__session;
|
|
671
|
+
|
|
672
|
+
// Log the token
|
|
632
673
|
self.log('Found "__session" cookie', idToken);
|
|
633
|
-
} else if (
|
|
634
|
-
|
|
635
|
-
|
|
674
|
+
} else if (
|
|
675
|
+
options.authenticationToken || data.authenticationToken
|
|
676
|
+
|| options.apiKey || data.apiKey
|
|
677
|
+
) {
|
|
678
|
+
// Read token OR API Key from options or data
|
|
679
|
+
idToken = options.authenticationToken || data.authenticationToken
|
|
680
|
+
|| options.apiKey || data.apiKey;
|
|
681
|
+
|
|
682
|
+
// Log the token
|
|
683
|
+
self.log('Found "authenticationToken" parameter', idToken);
|
|
684
|
+
} else {
|
|
685
|
+
// No token found
|
|
686
|
+
return _resolve(self.request.user);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Check if the token is a JWT
|
|
690
|
+
if (isJWT(idToken)) {
|
|
691
|
+
// Check with firebase
|
|
636
692
|
try {
|
|
637
|
-
|
|
638
|
-
// const workingConfig = self.Manager?.config || functions.config();
|
|
639
|
-
storedApiKey = self.Manager?.config?.backend_manager?.key || '';
|
|
640
|
-
} catch (e) {
|
|
641
|
-
// Do nothing
|
|
642
|
-
}
|
|
693
|
+
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
|
|
643
694
|
|
|
644
|
-
|
|
645
|
-
|
|
695
|
+
// Log the token
|
|
696
|
+
if (options.debug) {
|
|
697
|
+
self.log('JWT token decoded', decodedIdToken.email, decodedIdToken.user_id);
|
|
698
|
+
}
|
|
646
699
|
|
|
647
|
-
|
|
648
|
-
|
|
700
|
+
// Get the user
|
|
701
|
+
await admin.firestore().doc(`users/${decodedIdToken.user_id}`)
|
|
702
|
+
.get()
|
|
703
|
+
.then((doc) => {
|
|
704
|
+
// Set the user
|
|
705
|
+
if (doc.exists) {
|
|
706
|
+
self.request.user = Object.assign({}, self.request.user, doc.data());
|
|
707
|
+
self.request.user.authenticated = true;
|
|
708
|
+
self.request.user.auth.uid = decodedIdToken.user_id;
|
|
709
|
+
self.request.user.auth.email = decodedIdToken.email;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Log the user
|
|
713
|
+
if (options.debug) {
|
|
714
|
+
self.log('Found user doc', self.request.user)
|
|
715
|
+
}
|
|
716
|
+
})
|
|
649
717
|
|
|
650
|
-
|
|
651
|
-
if (storedApiKey && (storedApiKey === data.backendManagerKey || storedApiKey === data.authenticationToken)) {
|
|
652
|
-
self.request.user.authenticated = true;
|
|
653
|
-
self.request.user.roles.admin = true;
|
|
718
|
+
// Return the user
|
|
654
719
|
return _resolve(self.request.user);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const apiKey = options.apiKey || data.apiKey;
|
|
658
|
-
self.log('Found "options.apiKey"', apiKey);
|
|
720
|
+
} catch (error) {
|
|
721
|
+
self.error('Error while verifying JWT:', error);
|
|
659
722
|
|
|
660
|
-
|
|
723
|
+
// Return the user
|
|
661
724
|
return _resolve(self.request.user);
|
|
662
725
|
}
|
|
663
|
-
|
|
726
|
+
} else {
|
|
727
|
+
// Query by API key
|
|
664
728
|
await admin.firestore().collection(`users`)
|
|
665
|
-
.where('api.privateKey', '==',
|
|
729
|
+
.where('api.privateKey', '==', idToken)
|
|
666
730
|
.get()
|
|
667
|
-
.then(
|
|
668
|
-
querySnapshot.forEach(
|
|
731
|
+
.then((querySnapshot) => {
|
|
732
|
+
querySnapshot.forEach((doc) => {
|
|
669
733
|
self.request.user = doc.data();
|
|
734
|
+
self.request.user = Object.assign({}, self.request.user, doc.data());
|
|
670
735
|
self.request.user.authenticated = true;
|
|
671
736
|
});
|
|
672
737
|
})
|
|
673
|
-
.catch(
|
|
738
|
+
.catch((error) => {
|
|
674
739
|
console.error('Error getting documents: ', error);
|
|
675
740
|
});
|
|
676
741
|
|
|
677
|
-
|
|
678
|
-
} else {
|
|
679
|
-
// self.log('No Firebase ID token was able to be extracted.',
|
|
680
|
-
// 'Make sure you authenticate your request by providing either the following HTTP header:',
|
|
681
|
-
// 'Authorization: Bearer <Firebase ID Token>',
|
|
682
|
-
// 'or by passing a "__session" cookie',
|
|
683
|
-
// 'or by passing backendManagerKey or authenticationToken in the body or query');
|
|
684
|
-
|
|
685
|
-
return _resolve(self.request.user);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Check with firebase
|
|
689
|
-
try {
|
|
690
|
-
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
|
|
691
|
-
if (options.debug) {
|
|
692
|
-
self.log('Token correctly decoded', decodedIdToken.email, decodedIdToken.user_id);
|
|
693
|
-
}
|
|
694
|
-
await admin.firestore().doc(`users/${decodedIdToken.user_id}`)
|
|
695
|
-
.get()
|
|
696
|
-
.then(async function (doc) {
|
|
697
|
-
if (doc.exists) {
|
|
698
|
-
self.request.user = Object.assign({}, self.request.user, doc.data());
|
|
699
|
-
}
|
|
700
|
-
self.request.user.authenticated = true;
|
|
701
|
-
self.request.user.auth.uid = decodedIdToken.user_id;
|
|
702
|
-
self.request.user.auth.email = decodedIdToken.email;
|
|
703
|
-
if (options.debug) {
|
|
704
|
-
self.log('Found user doc', self.request.user)
|
|
705
|
-
}
|
|
706
|
-
})
|
|
707
|
-
return _resolve(self.request.user);
|
|
708
|
-
} catch (error) {
|
|
709
|
-
self.error('Error while verifying Firebase ID token:', error);
|
|
742
|
+
// Return the user
|
|
710
743
|
return _resolve(self.request.user);
|
|
711
744
|
}
|
|
712
745
|
};
|
|
@@ -726,7 +759,7 @@ BackendAssistant.prototype.parseRepo = function (repo) {
|
|
|
726
759
|
}
|
|
727
760
|
|
|
728
761
|
// Remove unnecessary parts
|
|
729
|
-
repoSplit = repoSplit.filter(
|
|
762
|
+
repoSplit = repoSplit.filter((value, index, arr) => {
|
|
730
763
|
return value !== 'http:'
|
|
731
764
|
&& value !== 'https:'
|
|
732
765
|
&& value !== ''
|
|
@@ -1034,6 +1067,21 @@ BackendAssistant.prototype.parseMultipartFormData = function (options) {
|
|
|
1034
1067
|
});
|
|
1035
1068
|
}
|
|
1036
1069
|
|
|
1070
|
+
const isJWT = (token) => {
|
|
1071
|
+
// Ensure the token has three parts separated by dots
|
|
1072
|
+
const parts = token.split('.');
|
|
1073
|
+
|
|
1074
|
+
try {
|
|
1075
|
+
// Decode the header (first part) to verify it is JSON
|
|
1076
|
+
const header = JSON.parse(Buffer.from(parts[0], 'base64').toString('utf8'));
|
|
1077
|
+
// Check for expected JWT keys in the header
|
|
1078
|
+
return header.alg && header.typ === 'JWT';
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
// If parsing fails, it's not a valid JWT
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1037
1085
|
// Not sure what this is for? But it has a good serializer code
|
|
1038
1086
|
// Disabled 2024-03-21 because there was another stringify() function that i was intending to use but it was actually using this
|
|
1039
1087
|
// It was adding escaped quotes to strings
|
|
@@ -62,7 +62,7 @@ Settings.prototype.resolve = function (assistant, schema, settings, options) {
|
|
|
62
62
|
// console.log('---self.settings', self.settings);
|
|
63
63
|
|
|
64
64
|
// Iterate each key and check for some things
|
|
65
|
-
|
|
65
|
+
iterateSchema(schema, (path, schemaNode) => {
|
|
66
66
|
const originalValue = _.get(settings, path);
|
|
67
67
|
const resolvedValue = _.get(self.settings, path);
|
|
68
68
|
let replaceValue = undefined;
|
|
@@ -154,7 +154,7 @@ Settings.prototype.constant = function (name, options) {
|
|
|
154
154
|
}
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
-
function
|
|
157
|
+
function iterateSchema(schema, fn, path) {
|
|
158
158
|
path = path || '';
|
|
159
159
|
|
|
160
160
|
// Base case: Check if the current level has 'types' and 'default', indicating metadata
|
|
@@ -167,7 +167,7 @@ function processSchema(schema, fn, path) {
|
|
|
167
167
|
// Recursive case: Iterate through nested keys if we're not at a metadata node
|
|
168
168
|
Object.keys(schema).forEach(key => {
|
|
169
169
|
const nextPath = path ? `${path}.${key}` : key;
|
|
170
|
-
|
|
170
|
+
iterateSchema(schema[key], fn, nextPath);
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
173
|
|
package/src/manager/index.js
CHANGED
|
@@ -871,11 +871,11 @@ Manager.prototype.setupFunctions = function (exporter, options) {
|
|
|
871
871
|
.auth.user()
|
|
872
872
|
.onDelete(async (user, context) => self._process((new (require(`${core}/events/auth/on-delete.js`))()).init(self, { user: user, context: context})));
|
|
873
873
|
|
|
874
|
-
exporter.
|
|
874
|
+
exporter.bm_notificationsOnWrite =
|
|
875
875
|
self.libraries.functions
|
|
876
876
|
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
877
|
-
.firestore.document('notifications/
|
|
878
|
-
.onWrite(async (change, context) => self._process((new (require(`${core}/events/firestore/on-
|
|
877
|
+
.firestore.document('notifications/{token}')
|
|
878
|
+
.onWrite(async (change, context) => self._process((new (require(`${core}/events/firestore/notifications/on-write.js`))()).init(self, { change: change, context: context, })));
|
|
879
879
|
|
|
880
880
|
// Setup cron jobs
|
|
881
881
|
exporter.bm_cronDaily =
|
|
@@ -126,24 +126,24 @@ describe("BackendManager Tests", () => {
|
|
|
126
126
|
describe("notifications", () => {
|
|
127
127
|
it("unauthenticated can subscribe", async () => {
|
|
128
128
|
const db = auth(accounts.unauthenticated);
|
|
129
|
-
const doc = db.doc(`notifications/
|
|
129
|
+
const doc = db.doc(`notifications/token`);
|
|
130
130
|
await firebase.assertSucceeds(doc.set({token: 'token'}));
|
|
131
131
|
});
|
|
132
132
|
it("authenticated can subscribe", async () => {
|
|
133
133
|
const db = auth(accounts.regular);
|
|
134
|
-
const doc = db.doc(`notifications/
|
|
134
|
+
const doc = db.doc(`notifications/token`);
|
|
135
135
|
await firebase.assertSucceeds(doc.set({token: 'token'}));
|
|
136
136
|
});
|
|
137
137
|
|
|
138
138
|
it("unauthenticated can read subscription by token", async () => {
|
|
139
139
|
const db = auth(accounts.unauthenticated);
|
|
140
|
-
const doc = db.doc(`notifications/
|
|
140
|
+
const doc = db.doc(`notifications/token`);
|
|
141
141
|
await firebase.assertSucceeds(doc.get());
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
it("authenticated can read subscription by token", async () => {
|
|
145
145
|
const db = auth(accounts.regular);
|
|
146
|
-
const doc = db.doc(`notifications/
|
|
146
|
+
const doc = db.doc(`notifications/token`);
|
|
147
147
|
await firebase.assertSucceeds(doc.get());
|
|
148
148
|
});
|
|
149
149
|
|
|
@@ -9,7 +9,7 @@ service cloud.firestore {
|
|
|
9
9
|
match /{document=**} {
|
|
10
10
|
allow read, write: if isAdmin();
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
// Protect user account data
|
|
14
14
|
match /users/{uid} {
|
|
15
15
|
allow read: if belongsTo(uid);
|
|
@@ -17,8 +17,8 @@ service cloud.firestore {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// Protect notification data
|
|
20
|
-
match /notifications/
|
|
21
|
-
allow read: if existingData().token == token || belongsTo(existingData().
|
|
20
|
+
match /notifications/{token} {
|
|
21
|
+
allow read: if existingData().token == token || belongsTo(existingData().owner.uid);
|
|
22
22
|
allow update: if existingData().token == token;
|
|
23
23
|
allow create: if true;
|
|
24
24
|
}
|
|
@@ -40,7 +40,7 @@ service cloud.firestore {
|
|
|
40
40
|
}
|
|
41
41
|
function belongsTo(identity) {
|
|
42
42
|
return isAuthenticated() && (authUid() == identity || authEmail() == identity);
|
|
43
|
-
// eventually include a check for (existingData().
|
|
43
|
+
// eventually include a check for (existingData().owner.uid == identity)...(in case its a doc owned by a user that's not actually user doc)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function getRoles() {
|