Consider the following scenario:
There's a collection called Resources and a special user can CRUD it on an admin site.
When a regular user accesses the site, the routes are created dynamically in the browser depending on a subscription to Resources.
We would like to pause the router lookup until the subscription's callback function terminates. Otherwise the user is presented a "404" when accessing /<dynamically-created-route> directly.
Is there an easy way to do this?
Something like this? You create a dynamic route based on a resource id, waitOn a subscription with the id value, and then route based on whether or not you can access the resource with the id. This assumes you have the proper publishing setup in your server code also.
Resources = new Mongo.Collection("resources");
Router.route("/resources/:_id", {
name: "resources",
waitOn: function() {
// Waits on the subscription
return Meteor.subscribe("resources", this.params._id);
},
action: function() {
// Should only return the resource you subscribed to unless you
// subscribe to resources in another part of your application
var resource = Resources.findOne({});
if (resource) {
// The resource exists and the user is able to access it.
this.render("resourceTemplate", {
data: {
resource: resource
}
});
}
else {
Router.go("/404");
}
}
});
Related
I am coding a project similar to patreon.com and my goal is to let multiple members create their own subscription plans and sell it.
I came across a firebase extension for stripe payments - https://firebase.google.com/products/extensions/stripe-firestore-stripe-payments
The problem with this extension is that I can only create a 1 premium membership that adds custom claims to the auth object and I can validate it like this:
export default async function isUserPremium(): Promise<boolean> {
await auth.currentUser?.getIdToken(true);
const decodedToken = await auth.currentUser?.getIdTokenResult();
return decodedToken?.claims?.stripeRole ? true : false;
}
That means that even if I have 100 different subscriptions, I can only attach single boolean value, which is useless.
I went to the source code and I found this snippet:
// Update their custom claims
if (role) {
try {
// Get existing claims for the user
const { customClaims } = await admin.auth().getUser(uid);
// Set new role in custom claims as long as the subs status allows
if (['trialing', 'active'].includes(subscription.status)) {
logs.userCustomClaimSet(uid, 'stripeRole', role);
await admin
.auth()
.setCustomUserClaims(uid, { ...customClaims, stripeRole: role });
} else {
logs.userCustomClaimSet(uid, 'stripeRole', 'null');
await admin
.auth()
.setCustomUserClaims(uid, { ...customClaims, stripeRole: null });
}
} catch (error) {
// User has been deleted, simply return.
return;
}
}
I don't fully understand this code, but I think this is where the boolean value is assigned.
Would it be possible to somehow edit this source code, so that instead of boolean value, I could store subscription plan ids in Array , so that in the front end I could validate and allow customer to access users content only if he has active plan in that array
?
We've been following https://cloud.google.com/pubsub/docs/dead-letter-topics and nodeJS client to create, update our pubsub topics and subscriptions but after following:
async function createSubscriptionWithDeadLetterPolicy() {
// Creates a new subscription
await pubSubClient.topic(topicName).createSubscription(subscriptionName, {
deadLetterPolicy: {
deadLetterTopic: pubSubClient.topic(deadLetterTopicName).name,
maxDeliveryAttempts: 10,
},
});
console.log(
`Created subscription ${subscriptionName} with dead letter topic ${deadLetterTopicName}.`
);
console.log(
'To process dead letter messages, remember to add a subscription to your dead letter topic.'
);
}
We get this in the dead-letter
This Suggests running the command in CLI for each dead-letter but we don't want to do it manually for each subscription is there a way to do this in nodeJS client itself?
Or doing so for all the subscriptions once and for all even for the new subscriptions that will be created in given project later on.
According to this part of the documentation, you need to grant 2 roles to the PubSub service agent service account. And of course, you can do it by API calls. And, it's not so easy!
In fact, it's not difficult, just boring! Why? Because, you can't only "add" a policy, you set the whole policies. To achieve this:
Get all the existing policies
Add your policy in the existing list
Submit the new list of policies.
You need to do this:
Either globally by setting the policies at the project level. Easier but less secure (break the least privilege principle)
Or on each Dead Letter topic and on each subscription with Dead Letter set up.
You have code example in the Client library doc
EDIT1
If you script the grant access mechanism, you don't care to find it or not: it exists, that's all! Maybe you don't view it on the console, but it exists. Only the pattern is important:
service-<project-number>#gcp-sa-pubsub.iam.gserviceaccount.com
If you are looking for it on the console, it's tricky now! You have to go to Access -> IAM. And then click on the check box on the top rigth corner to display the Google technical accounts
In case anyone needs it, here are the functions that I made up from #guillaume blaquiere's answer:
private async bindPolicyToSubscriber(
subscriptionTopicName: string,
subscriptionName: string,
) {
if (process.env.PROJECT_NUMBER) {
try {
const pubSubTopic = this.getClient().topic(subscriptionTopicName);
const myPolicy = {
bindings: [
{
role: 'roles/pubsub.subscriber',
members: [
`serviceAccount:service-${process.env.PROJECT_NUMBER}#gcp-sa-pubsub.iam.gserviceaccount.com`,
],
},
],
};
await pubSubTopic
.subscription(subscriptionName)
.iam.setPolicy(myPolicy);
} catch (e) {
console.error('Error while binding policy.', e);
}
}
}
private async bindPolicyToDeadLetterTopic(deadLetterTopicName: string) {
if (process.env.PROJECT_NUMBER) {
try {
const pubSubTopic = this.getClient().topic(deadLetterTopicName);
const myPolicy = {
bindings: [
{
role: 'roles/pubsub.publisher',
members: [
`serviceAccount:service-${process.env.PROJECT_NUMBER}#gcp-sa-pubsub.iam.gserviceaccount.com`,
],
},
],
};
await pubSubTopic.iam.setPolicy(myPolicy);
} catch (e) {
console.error('Error while binding policy.', e);
}
}
}
Using the Node.js SDK, I've created a user and a resource group. How can I now assign the user to the resource group as an owner?
You could refer to this example.
authzClient.roleAssignments.create(scope, assignmentGuid, roleCreateParams, function (err, roleAssignment, req, res) {
if (err) {
console.log('\nError occured while creating the roleAssignment: \n' + util.inspect(err, { depth: null }));
return;
}
According to your need, you need the example.
Owner role id is 8e3af657-a8ff-443c-a75c-2fe8c4bcb635.
scope should be like this /subscriptions/{subscription-id}/resourceGroups/myresourcegroup1.
Replace application id to your user object id.
Also, you could use Azure rest API to do this, please refer to this link.
I am using sample from: https://github.com/Azure-Samples/active-directory-b2c-javascript-msal-singlepageapp as a base to implement B2C signup.
How do I pass the state parameter in the example? I saw there was an issue about the state, so i guess it is possible to use state in the example. But, I can't figure out how to use it and how to retrieve it after token is returned.
I use state in my loginRedirect() method.. so I'll post my code here which should help you enough to make this work. I'm using MSAL in angular but the methods that I call should be the same.
In this example user clicks on a login button which calls a login method:
{
const args: AuthenticationParameters = {
state: "some string" //set state parameter (type: string)
};
this.msalService.loginRedirect(args);
}
This code will then redirect user to login.. and then back to your website (your redirectURI).. On this page you should implement handleRedirectCallback method which will be triggered after user is redirected. In this callback you will also get a response (or error) from login process which will include your state string.
this.msalService.handleRedirectCallback((authError, response) => {
if (response) {
const state = this.msalService.getAccountState(response.accountState);
// you don't need to use "this.msalService.getAccountState()" I think
...
do something with state. I use it to redirect back to initial page url.
...
}
});
In reviewing the source code for MSAL.js, I don't see how you can control the value of state. AuthenticationRequestParameters is not exposed and the value of state is set to a new guid when AuthenticationRequestParameters is constructed.
Example:
In the following code of MSAL.js, we have no control over the authenticationRequest variable.
loginRedirect(scopes? : Array<string> , extraQueryParameters? : string): void {
...
this.authorityInstance.ResolveEndpointsAsync()
.then(() => {
const authenticationRequest = new AuthenticationRequestParameters(this.authorityInstance, this.clientId, scopes, ResponseTypes.id_token, this._redirectUri);
...
});
...
}
You can send the state parameter on the loginRequest:
const loginRequest = {
...
scopes: "your scopes",
state: "my custom state value"
}
Then you capture it in the response on accountState, like this:
clientApplication.loginPopup(loginRequest).then(function (loginResponse) {
if (loginResponse.account) {
// will print: my custom state value
console.log(loginResponse.accountState);
....
}
});
You can find the documentation here: https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-js-pass-custom-state-authentication-request
A REST API is written in ExpressJs 4.x.x / NodeJS.
Let's assume an interface :
app.delete('/api/v1/users/:uid', function (req, res, next) {
...
}
So with that interface users can be deleted.
Let's assume there are 2 Customers in the system, and each Customer has Users. A User can have the privilege of deleting other Users with a role named CustomersAdmin.
But this User should only be able to delete Users which are Users from his Company(Customer).
So, let's get ACL into the scene. Assuming in our ACL we can define roles, resources and permissions. (Code is adopted from http://github.com/OptimalBits/node_acl#middlware.)
app.delete('/api/v1/users/:uid', acl.protect(), function (req, res, next)
{
// ? Delete User with uid = uid or check
// ? first if current user is in same company as user uid
}
There are two things to consider. One is protecting the route from persons without permission to HTTP/DELETE on that route ( /api/v1/users/:uid ) and the other is that a Person with Role CustomersAdmin shall not be allowed to delete Users from another Customer.
Is ACL supposed to do both? Or is it supposed to protect the route /api/v1/users?
So, would I use it like
acl.allow([
{
roles:'CustomersAdmin',
allows:[
{resources:['/api/v1/users', '/api/v1/users'] permissions:'delete'}
}
app.delete('/api/v1/users/:uid',acl.middleware(3), function(req,res,next)
{
Make sure uid is a User from same Customer as request is from(req.session.userid)
}
This would allow every User with Role CustomersAdmin to delete whatever user he wants.
Or is it preferable to define each possible Users route as a Resource and define multiple Roles which can interact with them?
acl.allow([
{
roles:'CustomersAdminOne',
allows:[
{resources:['/api/v1/users/1', '/api/v1/users/2'], permissions:'delete'}]
},
{
roles:'CustomersTwoAdmin',
allows:[
{resources:['/api/v1/users/3','/api/v1/users/4'], permissions:'delete'}
]
}
app.delete('/api/v1/users/:uid',acl.middleware(), function(req,res,next)
{
no logic needed to be sure that request is from a user within the same customer
}
The way I solved this was to create a role per user. I use a mongoose post save hook:
acl.addUserRole(user._id, ['user', user._id]);
Then in a post save hook for a resource I do this:
acl.allow(['admin', doc.user._id], '/album/' + doc._id, ['*']);
acl.allow(['guest', 'user'], '/album/' + doc._id, ['get']);
You can then use the isAllowed method to check if req.user has the right permissions.