How do I add permissions to a NATS User to allow the User to query & create Jestream keyvalue stores? - nats.io

I have a User that needs to be able to query and create Jetstream keyvalue stores. I attempted to add pub/sub access to $JS.API.STREAM.INFO.* in order to give the User the ability to query and create keyvalue stores:
96f4d12cdd02:~# nsc edit user RequestCacheService --allow-pubsub "$JS.API.STREAM.INFO.*"
[ OK ] added pub pub ".API.STREAM.INFO.*"
[ OK ] added sub ".API.STREAM.INFO.*"
[ OK ] generated user creds file `/nsc/nkeys/creds/Client/Client/RequestCacheService.creds`
[ OK ] edited user "RequestCacheService"
As you can see above, pub sub was added for ".API.STREAM.INFO.", not "$JS.API.STREAM.INFO.".
How do I allow a User permissions to query & create Jetstream keyvalue stores?

Should be:
nsc edit user RequestCacheService --allow-pubsub '$JS.API.STREAM.INFO.*'
With single-quotes around the subject. I was under the impression that double & single quotes would escape the $ but apparently only single-quote will escape special characters in the subject.

Related

Require unique username when Users container has userId as partition key

I am using this article as an example
https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-model-partition-example with a Users container with userId and username and the partition key as userId.
{
"id": "54c7da13-f4b8-4668-90dc-7c1aa968a73e",
"userId": "54c7da13-f4b8-4668-90dc-7c1aa968a73e",
"type": "user",
"username": "jeffw"
}
In my create user page I want to make sure the username is unique before adding a new user. I tried a pre-trigger but found that "You can't run stored procedures or triggers across multiple logical partitions." How do I make sure that when a user is created that they have selected a unique username? I think I could change the partition key to username but why does the article use userId instead?
SOLUTION
See answer from #mark-brown.
Create a unique key on the Users container and /username:
await database.Database.DefineContainer(name: "Users", partitionKeyPath: "/userId")
.WithUniqueKey().Path("/username").Attach()
.CreateIfNotExistsAsync();
Then try to create a new User with userId as "unique_username" and the new username that is attempting to be created:
{
"id": "06af2937-4677-4d27-a167-5517aa6d0ffd",
"userId": "unique_username",
"type": "unique_username",
"username": "jeffw"
}
await _usersContainer.CreateItemAsync(uniqueUser, new PartitionKey("unique_username"));
This will return a Conflict status if the username already exists. Example is here https://github.com/jwidmer/AzureCosmosDbBlogExample/blob/master/BlogWebApp/Services/BlogCosmosDbService.cs
Changing the partition key to username won't help because you can have multiples of that value in your container. One way you could do this is to have a new partition where you store a unique instance for every user name and use a unique index on the container (unique indexes are unique within a logical partition).
Create a new type = "unique_user" and a userId = "unique_user". Then add a new record of that type with the new username as they register. This should get you millions of users without going over the 20GB limit. Then when creating a new user do an insert on the container with the "unique_user" type and id with the new username. If you get a 201 then do another insert with type= "user" and the rest of the user data.
hope that helps.
You can set up an index policy for unique values.

Implement Firestore security rules for a many to many relationship

I'm struggling use Firestore security rules to secure a many to many relationship.
I have the following collections:
Key:
documentId: [field: value, ...]
groups {
group1: [name: Group1]
group2: [name: Group2]
}
users {
bobUser: [name: Bob]
aliceUser: [name: Alice]
fredUser: [name: Fred]
}
// Contains data specific to a user in a particular group.
// Specifically the user's role
userGroups {
userGroup1: [userId: bobUser, groupId: group1, role: "admin"]
userGroup2: [userId: aliceUser, groupId: group1, role: "member"]
userGroup3: [userId: fredUser, groupId: group2, role: "admin"]
}
How can I construct a firestore security rule so that:
A user with role:"admin" can read another user's document if they both are found in the same group
So in the example above, Bob can read Alice's user document as he has an "admin" role but Fred can't as he is an admin for another group.
Or to put it another way:
If bobUser makes the below request, then it should pass security rules:
db.collection("users").doc("aliceUser");
as bob is has an admin role in the same group as Alice
In contrast, if fredUser was logged in, the below request would fail:
db.collection("users").doc("aliceUser");
Fred is an admin user, but not in the same group and so the rule would block the request.
In the security rule I think I need to split into a few stages:
Query userGroups to find all groupIds where requesting userId is role: "admin"
Query userGroups to find all groupIds where requested user exists
Allow write if there is a match of groupIds in both groups
But I'm having trouble getting this logic into the rule. Security rules don't seem be able to filter like this. Any help would be great!
In order to solve this problem, you need to keep in mind that you cannot transfer relational database patterns to a non-relational database. When working with Firestore, you should start by asking "What queries should be possible?" and then structure your data based on that. Building up Security Rules will follow naturally.
I wrote a blogpost about exactly your use case: "How to build a team-based user management system with Firebase", so if anything from the following answer is unclear, go there first to see if it helps.
In your case, you'd probably want the following queries:
Get all users of a group (given the current user is a member of this group and has the correct permission).
Get all groups of a user (given you are only querying groups of the currently authenticated user).
As you noticed, many-to-many relationships are hard to work with through Firestore and Security Rules, because you would need to make additional requests to join the datasets. To avoid that, I recommend renaming the userGroups collection to memberships and turning it into a subcollection of each doc in the groups collection. So your new structure would look like
- collection "groups", a doc contains:
- field "name"
- collection "memberships", a doc contains:
- field "name"
- field "role"
- field "user" → references doc from "users"
- collection "users", a doc contains:
- field "name"
This way you can easily solve the first query "Get all users of a group" by querying the subcollection "memberships" like collection("groups").doc("your-group-id").collection("memberships").get().
Now, to secure that, you can write a helper functions in Security Rules:
function hasAccessToGroup(groupId, role) {
return get(/databases/$(database)/documents/groups/$(groupId)/memberships/$(request.auth.uid)).data.role == role
}
Given a groupId and a role, you can use it allow only users who are a member and have a specific role access to data and subcollections within the group. In order to protect the memberships collection on a group this might look like this:
rules_version = '2'
service cloud.firestore {
match /databases/{database}/documents {
function hasAccessToGroup(groupId, role) {
return get(/databases/$(database)/documents/groups/$(groupId)/memberships/$(request.auth.uid)).data.role == role
}
match /groups/{groupId} {
// Allow users with the role "user" access to read the group doc.
allow get: if
request.auth != null &&
hasAccessToGroup(groupId, "user")
// Allow users with the role "admin" access to read all subcollections, including "memberships".
match /{subcollection}/{document=**} {
allow read: if
request.auth != null &&
hasAccessToGroup(groupId, "admin")
}
}
}
}
Now there is only the second query "Get all groups of a user" left. This can be achieved through a Collection Group Index, which allows to query all subcollections with the same name. You want to create one for the memberships collection. Given a specific user, you can then easily query all of his groups with collectionGroup("memberships").where("user", "==", currentUserRef).get().
In order to secure that, you need to setup a Rule that allows such requests only if the queried user reference equals the currently authenticated user:
function isReferenceTo(field, path) {
return path('/databases/(default)/documents' + path) == field
}
match /{document=**}/memberships/{userId} {
allow read: if
request.auth != null &&
isReferenceTo(resource.data.user, "/users/" + request.auth.uid)
}
One last thing to talk about is how you keep the data in the memberships collection up-to-date with the data in the users doc that it references. The answer are Cloud Functions. Every time a users doc changes, you query all of its memberships and update the data.
As you can see, answering your original question how you can construct a Firestore Rule so that a user with the correct permission can read another user's document if they both are found in the same group, takes a different approach. But after restructuring your data, your Security Rules will be easier to read.
I hope this helped. Cheers!

Get user from identityId

I saved one element in dynamodb using the id with AWS.config.credentials.identityId and restricting with ${cognito-identity.amazonaws.com:sub}
This generates a us-east-1:14b37fe3-xxxx-xxxx-xxxx-xxxxxxxxxxxx record in dynamodb for a user with sub in cognito user pool: 20a3902b-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Now, I need to create an trigger in dynamodb and retrieve some user attributes like name and email to generate a billing, how I can retrieve this info?
I have tried using this function:
response = boto3.client('cognito-idp').list_users(
UserPoolId='us-east-1_rYoyNTF4V',
AttributesToGet=[
'email', 'name', 'middle_name'
],
Filter='sub = "{sub}"'.format(sub=sub)
)
But it's only works for the 20a3902b-xxxx-xxxx-xxxx-xxxxxxxxxxxx sub.

Check if other node has same child node value

I'm trying to implement validation rule that will check when new $resvs is created if any other $resvs contains the same eventKey (which is a child node of that object) as the already existing reservations nodes.
The following code is not working, but can't come up with anything better now:
"reservations": {
"$resvs": {
".write": "!data.exists() || newData.child('eventKey').val() == root.child('reservations/$resvs/eventKey').val()"
}
}
edit:
firebase.database().ref('/reservations').push({
name: name,
number: number,
email: email,
start: start,
end: end,
scheduleId: scheduleId,
scheduleName: scheduleName,
eventKey: eventKey,
placeId: placeId
}).then(function(){
$('#calendar').fullCalendar('refetchEvents'); //confirmaton first? TODO
}).catch(function(error){
alert('err');
});
To solve this problem, i recomand you change the database structure a little bit. Instead of using the event key as a field, try to store the reservation data directly under the event key. In this way you can double check, once in code and once using security rules. If you want to check in your code, just add a listener on the reservations node and use exists() method on the dataSnapshot object. In the same manner use the security rules, because with your actual database structure it's impossible to use wildcards in the way you want.
Your database new structure should look like this:
Firebase-root
|
--- reservations
|
--- eventKey1
| |
| --- //event1 details
|
--- eventKey2
|
--- //event2 details
But remember, this will work only and only if the the event key are unique.
Hope it helps.

Why use an object when denormalising data?

In the recent blog post on denormalising data, it suggests logging all of a user's comments beneath each user like so:
comments: {
comment1: true,
comment2: true
}
Why is this not a list like so:
comments: [
"comment1",
"comment2",
]
What are the advantages? Is there any difference at all? While I'm at it, how would you go about generating unique references for these comments for a distributed app? I was imagining that with a list I'd just push them onto the end and let the array take care of the index.
Firebase only ever stores objects. The JS client converts arrays into objects using the index as a key. So, for instance if you store the following array using set:
comments: [
"comment1",
"comment2"
]
In Forge (the graphical debugger), it will show up as:
comments:
0: comment1
1: comment2
Given this, storing the ID of the comment directly as a key has the advantage that you can refer to it directly in the security rules, for example, with an expression like:
root.child('comments').hasChild($comment)
In order to generate unique references for these comments, please use push (https://www.firebase.com/docs/managing-lists.html):
var commentsRef = new Firebase("https://<example>.firebaseio.com/comments");
var id = commentsRef.push({content: "Hello world!", author: "Alice"});
console.log(id); // Unique identifier for the comment just added.

Resources