How to let an admin change user properties in Meteor? - database

Are there any special hoops one has to jump through when modifying user objects in Meteor? I have no problem changing other collections but the users are strangely and persistently resistant to the many suggestions I have found.
I can see that there are some user attributes such as profile that are published and presumably quite easy to change. I need more control over the access so just bunging my data into user.profile won't do. At the moment I'm trying to give users a grant table, so that for example I can write:
var user = Meteor.users.findOne();
var may_eat_popcorn = user.grants.popcorn;
This works:
$ meteor shell
// First check that the user is not allowed to eat popcorn:
> Meteor.users.findOne({_id:"iCTnpqwCR6jj9xxxx"});
....
grants: { popcorn: false } }
// Give the non-gender specific entity access to popcorn:
> Meteor.users.update({_id:"iCTnpqwCR6jj9xxxx"},{$set:{"grants.popcorn":true}}, function(err,res){console.log("grant:",err,res);});
> Meteor.users.findOne({_id:"iCTnpqwCR6jj9xxxx"});
....
grants: { popcorn: true } }
// Hooray.
This doesn't, even though equivalent code works fine with other collections:
Meteor.methods(
{ User_grant_popcorn: function(userId, granted){
// authentication. Then:
var grants = {"grants.popcorn": granted};
console.log(userId,grants);
Meteor.users.update({_id:userId},{$set:grants}, function(err,res){console.log("grant:",err,res);});
// This callback prints that there is no error, yet the database doesn't change on the server.
}
});
// On the client the admin picks the target user and sets their degree of pop:
Meteor.call('User_grant_popcorn', user._id, false);
Do you know how user is different? More importantly, how can I debug issues like this? Winning means getting awesome things done fast. That's meteor's promise. If debugging takes this long the advantage is lost.
Many thanks, Max

Programmatically create $set
Meteor.methods({
User_grant_popcorn: function(userId, granted) {
// authentication. Then:
var grants = {
"grants.popcorn": granted
};
var setHash = {
$set: grants
};
console.log(userId, grants);
Meteor.users.update({_id: userId}, setHash, function(err, res) {
console.log("grant:", err, res);
});
// This callback prints that there is no error, yet the database doesn't change on the server.
}
});

Related

Unreliable Google Firebase transactions

In my (greatly simplified) model I have users, accounts and account_types. Each user can have multiple accounts of each account_type. When an account of type TT is created I'm updating the "users" field of that object so it keeps the users which have accounts of that types, and the number of such accounts they have.
users: {
some fields
},
accounts: {
userID: UU,
type: TT
},
account_type:
users: { UU: 31 }
}
I use the onCreate and onDelete cloud triggers for accounts to update the account_type object. Since multiple accounts can be created simultaneously I have to use transactions:
exports.onCreateAccount = functions.firestore
.document('accounts/{accountID}')
.onCreate((account, context) => {
const acc_user = account.data().userID;
const acc_type = account.data().type;
return admin.firestore().runTransaction(transaction => {
// This code may get re-run multiple times if there are conflicts.
const accountTypeRef = admin.firestore().doc("account_types/"+acc_type);
return transaction.get(accountTypeRef).then(accTypeDoc => {
var users = accTypeDoc.data().users;
if (users === undefined) {
users = {};
}
if (users[acc_user] === undefined) {
users[acc_user] = 1;
} else {
users[acc_user]++;
}
transaction.update(accountTypeRef, {users: users});
return;
})
})
.catch(error => {
console.log("AccountType create transaction failed. Error: "+error);
});
});
In my tests I'm first populating the database with some data so I'm also adding a user and 30 accounts of the same type. With the local emulator this works just fine and at the end of the addition I see that the account_type object contains the user with the counter at 30. But when deployed to Firebase and running the same functions the counter gets to less than 30. My suspicion is that since Firebase is much slower and transactions take longer, more of them are conflicted and fail and eventually don't execute at all. The transaction failure documentation (https://firebase.google.com/docs/firestore/manage-data/transactions) says:
"The transaction read a document that was modified outside of the transaction. In this case, the transaction automatically runs again. The transaction is retried a finite number of times."
So my questions:
What does "finite" mean?
Any way to control this number?
How can I make sure my transactions are executed at some point and don't get dropped like that so my data is consistent?
Any other idea as to why I'm not getting the correct results when deployed to the cloud?
What does "finite" mean?
It's the opposite of "unlimited". It will retry no more than a set number of times.
Any way to control this number?
Other than modifying the source code of the SDK, no. The SDK itself advertise a specific number, as it might change.
How can I make sure my transactions are executed at some point and don't get dropped like that so my data is consistent?
Detect the error and retry in your app. If you aren't seeing the transaction fail with an error, then nothing went wrong.
Any other idea as to why I'm not getting the correct results when deployed to the cloud?
Since we can't see what exactly you're doing to trigger the function, and have no specific expected results to compare to, it's not really possible to say.

.Net Core authorize authenticated users with Azure AD

Hi,
We have a .Net Core 2.0 app, with Azure AD authentication.
At the moment we have some users associated with the app that have been given a role for the app in the Azure AD, and are using policies to check authorisation. Due business requirements we want to open the app to any user on the company, so we only need to check that the user is authenticated.
As an experienced C# developer, I went to the controller that had the following code
[Authorize(Policy = PolicyNames.RequireLinecardsUser)]
public class LinecardController : Controller
{
//controller code here
}
and changed it to this
[Authorize]
public class LinecardController : Controller
{
//controller code here
}
and for may user, that still has the role of LinecardUser, it works. it can access the app (the main entry point is inside that controller). But when we tested it with a user that does not have a role in the app, it gets an access denied exception.
So next step we went into startup, and removed the authorisation configurations ( it only had 2 now unused policies).
public void ConfigureServices(IServiceCollection services)
{
try
{
services.AddDbContext(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("LinecardsContext"), opt => opt.UseRowNumberForPaging());
});
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultForbidScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.AccessDeniedPath = "/Account/AccessDenied/";
options.LoginPath = "/Account/Login/";
})
.AddOpenIdConnect(options =>
{
configuration.GetSection("AzureAd").Bind(options);
});
services.AddAuthorization(options =>
{
options.AddPolicy(PolicyNames.RequireLinecardsUser,
policy =>
{
policy.AddRequirements(new LinecardsWebUserRequirement());
policy.RequireAuthenticatedUser(); // Adds DenyAnonymousAuthorizationRequirement
// By adding the CookieAuthenticationDefaults.AuthenticationScheme, if an authenticated
// user is not in the appropriate role, they will be redirected to a "forbidden" page.
policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
});
options.AddPolicy(PolicyNames.RequireLinecardsAdmin,
policy =>
{
policy.AddRequirements(new LinecardsWebAdminRequirement());
policy.RequireAuthenticatedUser(); // Adds DenyAnonymousAuthorizationRequirement
// By adding the CookieAuthenticationDefaults.AuthenticationScheme, if an authenticated
// user is not in the appropriate role, they will be redirected to a "forbidden" page.
policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
});
});
services.AddScoped();
services.RegisterTypes();
services.AddReact();
services.AddAutoMapper();
services.Configure(x => x.ValueCountLimit = 100000);
services.AddMvc();
}
catch (Exception ex)
{
Log.Error(ex, "Error happened on configuring services");
throw;
}
}
That means that in the previous code we removed the services.AddAuthentication(); block. Same result, users that have roles can access the app, users that don't cannot. (tried with the browser in anonymous mode too, to make sure no caching problems were in action here).
Finally we tried to change the RequireLinecardsUser policy, so it didn't have the policy.AddRequirements(new LinecardsWebUserRequirement()); line (and changed the controller to the original authorization line with the policy.
Once again, same results, works fine for me, access denied for users that don't have a role.
Am I missing something obvious? Something that changed in Core 2.0? Something Azure related?
Because every documentation that I found says that the [Authorize] annotation without any arguments should work and intended, and only verify that user is authenticated before allowing the code to procede...
As some of you may have noticed the app has a react frontend that I haven't touched, feel free to point any problems that may be related with that.
PPS: Sorry if the formatting is not up to standard, but it's my first question
After some more tests with this app, an exception pointed to Azure being the culprit for this.
On the app properties in Azure there is an option asking "User assignment required?". That particular setting was set to yes for this app.
Changing it to no solved the problem.

Random access denied errors on User Extensions

When using extensions in the Graph API:
graphUser = graphClient.Users.Request().AddAsync(graphUser).Result;
OpenTypeExtension newExtension = new OpenTypeExtension()
{
ExtensionName = "CustomName",
AdditionalData = new Dictionary<string, object>
{ { "CustomID", user.CustomID }
}
};
graphClient.Users[graphUser.UserPrincipalName]
.Extensions
.Request()
.AddAsync(newExtension)
.Wait();
I randomly get these errors:
Code: AccessDenied
Message: Access Denied
Sometimes it works, sometimes it doesn't. I can't seem to find a correlation.
When I step trough the code in the debugger it works more often then if I run it without interruption. But if I add a sleep between the lines (to account for processing delay), it doesn't fix the issue.
The application has all the required rights to access the API.
The issue isn't solely in the POST, but also on the GET as illustrated in the code sample below which results in the same error.
User user = graphClient.Users[userName]
.Request()
.GetAsync()
.Result;
user.Extensions = graphClient.Users[userName]
.Extensions
.Request()
.GetAsync()
.Result;
Does anyone have experience with this issue? Thanks in advance!
EDIT:
I figured out that once the errors start showing, the user needs to be deleted. Errors on one user don't necessarily mean errors on another user.
EDIT 2:
I also posted this as an issue on GitHub. Here you can find more information about this problem. It's now labeled as a bug.
It turns out that the User Principal Name is a cached reference.
Since I was running tests, meaning recreating the same test user a lot, the reference UPN was pointing to the old user object resulting in the Access Denied errors.
The issue can be avoided by using the Id of the object, like this:
graphClient.Users[graphUser.Id]
.Extensions
.Request()
.AddAsync(newExtension)
.Wait();
I believe the team is going to fix the reference bug, but I can't speak for them of course. In either case I would recommend using the Id attribute to be sure.
Based on the test, when we create a new user in Azure Active Directory, it seems that there is some delay we can operate for that user. Even the user is returned successfully when I using the Graph to filter the user, it still may failed when I add the extension to that user.
For this issue, I added a line of addition code to make the current thread sleep and then it works.
var userPrincipalName = "userPrincipalName8#adfei.onmicrosoft.com";
var graphUser = new User() { AccountEnabled = true, MailNickname = "MailNickname", UserPrincipalName = userPrincipalName, DisplayName = "userPrincipalName", PasswordProfile = new PasswordProfile() { Password = "islkdifde123!", ForceChangePasswordNextSignIn = false } };
graphUser = graphClient.Users.Request().AddAsync(graphUser).Result;
OpenTypeExtension newExtension = new OpenTypeExtension()
{
ExtensionName = "CustomName",
AdditionalData = new Dictionary<string, object>
{
{ "CustomID", "abc" }
}
};
Thread.Sleep(4000);
graphClient.Users[graphUser.UserPrincipalName]
.Extensions
.Request()
.AddAsync(newExtension)
.Wait();
However the detailed error message for this issue should be code=ResourceNotFound,message=User not found. Please check whether the error is same and this workaround is helpful.

Adding Custom Attributes to Firebase Auth

I have hunted through Firebase's docs and can't seem to find a way to add custom attributes to FIRAuth. I am migrating an app from Parse-Server and I know that I could set a user's username, email, and objectId. No I see that I have the option for email, displayName, and photoURL. I want to be able to add custom attributes like the user's name. For example, I can use:
let user = FIRAuth.auth()?.currentUser
if let user = user {
let changeRequest = user.profileChangeRequest()
changeRequest.displayName = "Jane Q. User"
changeRequest.photoURL =
NSURL(string: "https://example.com/jane-q-user/profile.jpg")
changeRequest.setValue("Test1Name", forKey: "usersName")
changeRequest.commitChangesWithCompletion { error in
if error != nil {
print("\(error!.code): \(error!.localizedDescription)")
} else {
print("User's Display Name: \(user.displayName!)")
print("User's Name: \(user.valueForKey("name"))")
}
}
}
When I run the code, I get an error that "usersName" is not key value compliant. Is this not the right code to use. I can't seem to find another way.
You can't add custom attributes to Firebase Auth. Default attributes have been made available to facilitate access to user information, especially when using a provider (such as Facebook).
If you need to store more information about a user, use the Firebase realtime database. I recommend having a "Users" parent, that will hold all the User children. Also, have a userId key or an email key in order to identify the users and associate them with their respective accounts.
Hope this helps.
While in most cases you cannot add custom information to a user, there are cases where you can.
If you are creating or modifying users using the Admin SDK, you may create custom claims. These custom claims can be used within your client by accessing attributes of the claims object.
Swift code from the Firebase documentation:
user.getIDTokenResult(completion: { (result, error) in
guard let admin = result?.claims?["admin"] as? NSNumber else {
// Show regular user UI.
showRegularUI()
return
}
if admin.boolValue {
// Show admin UI.
showAdminUI()
} else {
// Show regular user UI.
showRegularUI()
}
})
Node.js code for adding the claim:
// Set admin privilege on the user corresponding to uid.
admin.auth().setCustomUserClaims(uid, {admin: true}).then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});

AngularJS: Value arriving as null in service

I am trying to pass an object to my API, but when I do this for some reason the object becomes null. I cannot see the reason for this because I've done a similar process on multiple other occasions.
var promiseGetRole = loginService.GetRole(user);
promiseGetRole.then(function (data, status, headers, config) {
$location.path('/UserManagement').replace();
if (!$scope.$$phase) $scope.$apply()
},
function (errorResult) {
console.log("Unable to log in : " + errorResult);
});
User is correctly populated.
//Get the Role of a given user
this.GetRole = function (user) {
return $http.get(toApiUrl('login'), user);
}
Again, user is correctly populated at this point.
//The user has been validated, now retrieve their role from the server
public string Get(UserLogin user)
{
string role = "";
//TODO: set role
return role;
}
It's at this point when it reaches the API the value for User just becomes null. I am using the same process in a post method (using UserLogin as the object being passed in) and cannot see any difference between them.
I would pass data to the back-end like this(assuming you can retrieve roles by userId).:
var promise = $http.get(toApiUrl('login'), { params:{userId:user.Id}); // Only pass id in queryparams
...
You probably can pass the entire user object in the querystring but it has a length limit.
I don't know what you run back-end either so depending on that you might need to do different things to make it bind to the data.

Resources