/// <summary> /// Changes a user role within the organization /// </summary> /// <param name="dbCtx"></param> /// <param name="user"></param> /// <returns></returns> public async Task ChangeOrganizationUserRoleAsync(DbContext dbCtx, MapHiveUser user) { //this basically needs to remove all the org roles for a user and add the specified one var ownerRole = await GetOrgOwnerRoleAsync(dbCtx); var adminRole = await GetOrgAdminRoleAsync(dbCtx); var memberRole = await GetOrgMemberRoleAsync(dbCtx); var addRole = memberRole; switch (user.OrganizationRole) { case OrganizationRole.Admin: addRole = adminRole; break; case OrganizationRole.Owner: addRole = ownerRole; break; } user.RemoveLink(ownerRole); user.RemoveLink(adminRole); user.RemoveLink(memberRole); if (user.OrganizationRole.HasValue) { user.AddLink(addRole); } await user.UpdateAsync(dbCtx); }
/// <summary> /// Gets a full user name based on user data, so pretty much name && surname? name surname : surname || name || email /// </summary> /// <param name="u"></param> /// <returns></returns> public static string GetFullUserName(this MapHiveUser u) { return(!string.IsNullOrEmpty(u.Forename) && !string.IsNullOrEmpty(u.Surname) ? $"{u.Forename} {u.Surname}" : !string.IsNullOrEmpty(u.Forename) ? u.Forename : !string.IsNullOrEmpty(u.Surname) ? u.Surname : u.Email); }
/// <summary> /// Adds a member to an organization /// </summary> /// <param name="dbCtx"></param> /// <param name="user"></param> /// <param name="role"></param> /// <returns></returns> public async Task AddOrganizationUserAsync(DbContext dbCtx, MapHiveUser user, OrganizationRole role = OrganizationRole.Member) { this.AddLink(user); await this.UpdateAsync(dbCtx); //by default assign a member role to a user var memberRole = await this.GetOrgRoleAsync(dbCtx, role); user.AddLink(memberRole); await user.UpdateAsync <MapHiveUser>(dbCtx); }
//FIXME - make it possible to pass all the org assets as a param, so can avoid re-reading db! /// <summary> /// Gets a list of user's organisations that have access to specified apps /// </summary> /// <param name="dbCtx"></param> /// <param name="uuid"></param> /// <param name="appShortNames">app short names - when provided, only orgs that contain a listed app are returned</param> /// <returns></returns> public static async Task <IEnumerable <Organization> > GetUserOrganizationsAsync(DbContext dbCtx, Guid uuid, IEnumerable <string> appShortNames) { var user = new MapHiveUser(); user = await user.ReadAsync(dbCtx, uuid); if (user == null) { return(null); } var organizations = await user.GetParentsAsync <MapHiveUser, Organization>(dbCtx, detached : true); if (organizations == null || !organizations.Any()) { return(new List <Organization>()); } var filteredOrgs = new List <Organization>(); foreach (var organization in organizations) { //get the apps linked directly organization.Applications = (await organization.GetOrganizationAssetsAsync <Application>((MapHiveDbContext)dbCtx))?.assets.ToList(); if (appShortNames != null && appShortNames.Any()) { if (organization.Applications == null || !organization.Applications.Any(x => appShortNames.Contains(x.ShortName))) { continue; } filteredOrgs.Add(organization); } else { filteredOrgs.Add(organization); } } return(filteredOrgs); }
/// <summary> /// Creates an org owner account - creates a user profile, an organization for a user, ties all the bits and pieces together /// </summary> /// <param name="dbCtx"></param> /// <param name="input"></param> /// <param name="emailSender"></param> /// <returns></returns> public static async Task <AccountCreateOutput> CreateAccountAsync( MapHiveDbContext dbCtx, AccountCreateInput input, IEmailSender emailSender ) { var user = new MapHive.Core.DataModel.MapHiveUser() { Email = input.AccountDetails.Email, Slug = input.AccountDetails.Slug, Forename = input.AccountDetails.Forename, Surname = input.AccountDetails.Surname, Company = input.AccountDetails.Company, Department = input.AccountDetails.Department, ContactPhone = input.AccountDetails.ContactPhone }; //now the org object var orgNameDesc = string.IsNullOrEmpty(input.AccountDetails.Company) ? input.AccountDetails.Email + "-org" : input.AccountDetails.Company; var newOrg = new Organization { DisplayName = orgNameDesc, Description = orgNameDesc, Slug = Utils.Slug.GetOrgSlug(orgNameDesc, user.Slug + "-org"), //push to extra billing info? BillingExtraInfo = new SerializableDictionaryOfString { { nameof(input.AccountDetails.ContactPhone), input.AccountDetails.ContactPhone }, { nameof(input.AccountDetails.Email), input.AccountDetails.Email }, { "ContactPerson", $"{input.AccountDetails.Forename} {input.AccountDetails.Surname}" }, { nameof(input.AccountDetails.Street), input.AccountDetails.Street }, { nameof(input.AccountDetails.HouseNo), input.AccountDetails.HouseNo }, { nameof(input.AccountDetails.FlatNo), input.AccountDetails.FlatNo }, { nameof(input.AccountDetails.Postcode), input.AccountDetails.Postcode }, { nameof(input.AccountDetails.City), input.AccountDetails.City }, { nameof(input.AccountDetails.Country), input.AccountDetails.Country }, { nameof(input.AccountDetails.VatNumber), input.AccountDetails.VatNumber } }, LicenseOptions = new OrganizationLicenseOptions() }; //got the user and org, o need to validate them now in order to ensure they are creatable if (user.Slug == newOrg.Slug) { var ex = new ValidationFailedException(); ex.ValidationErrors.Add(new ValidationError { Message = $"MapHiveUser slug already taken: {user.Slug}", Code = "user_org_slug_duplicate", PropertyName = nameof(MapHiveUser.Slug) }); throw ex; } //validate user and org - both should throw if a slug is already taken await user.ValidateAsync(dbCtx); await newOrg.ValidateAsync(dbCtx); //prepare the email template tokens known at this stage, var tokens = new Dictionary <string, object> { { "UserName", $"{user.GetFullUserName()} ({user.Email})" }, { "Email", user.Email } }; //and create user var accountCreateOutput = await MapHive.Core.DataModel.MapHiveUser.CreateUserAccountAsync(dbCtx, user, emailSender, input.EmailAccount, input.EmailTemplate?.Prepare(tokens)); user = accountCreateOutput.User; //continue with the org setup //see what apps the client api wants to register var appIdentifiers = input.LicenseOptions.Keys.ToArray(); //get them... var apps = await dbCtx.Applications.Where(a => appIdentifiers.Contains(a.ShortName)).ToListAsync(); //always make sure to glue in the core apis and apps apps.AddRange( MapHive.Core.Defaults.Applications.GetDefaultOrgApps() ); foreach (var appShortName in input.LicenseOptions.Keys) { var app = apps.FirstOrDefault(a => a.ShortName == appShortName); if (app == null) { continue; } newOrg.LicenseOptions.Add( new OrganizationLicenseOption { LicensedObjectTypeUuid = app.TypeUuid, LicensedObjectUuid = app.Uuid, LicenseOptions = input.LicenseOptions[appShortName] } ); } //create an org with owner and register the specified apps //make sure though to use the collection that contains the the default org apps! await newOrg.CreateAsync(dbCtx, user, apps); //wire up user with his 'parent org' //this is so it is clear a user has his own org user.UserOrgId = newOrg.Uuid; await user.UpdateAsync(dbCtx); return(new AccountCreateOutput { EmailTemplate = input.EmailTemplate, VerificationKey = accountCreateOutput.VerificationKey, InitialPassword = accountCreateOutput.InitialPassword }); }
/// <summary> /// Checks if a user is an organization admin (user has the org admin role assigned) /// </summary> /// <param name="dbctx"></param> /// <param name="user"></param> /// <returns></returns> public async Task <bool> IsOrgAdminAsync(DbContext dbctx, MapHiveUser user) { return(await user.HasChildLinkAsync(dbctx, await GetOrgAdminRoleAsync(dbctx))); }
/// <summary> /// Determines if a user is an org member (is assigned to an org) /// </summary> /// <param name="dbctx"></param> /// <param name="user"></param> /// <returns></returns> public async Task <bool> IsOrgMemberAsync(DbContext dbctx, MapHiveUser user) { return(await this.HasChildLinkAsync(dbctx, user)); }
/// <summary> /// Creates and organisation account, register a user as an owner and registers the specified apps to be linked to it too /// </summary> /// <param name="org"></param> /// <param name="dbCtx"></param> /// <param name="owner"></param> /// <param name="appsToLink"></param> /// <returns></returns> public static async Task <Organization> CreateAsync(this Organization org, DbContext dbCtx, MapHiveUser owner, IEnumerable <Application> appsToLink) { //first create the org await org.CreateAsync(dbCtx); //take care of assigning the owner role to a user await org.AddOwnerAsync(dbCtx, owner); //finaly grab the apps that should be registered for the org and link them var mhDb = (MapHiveDbContext)dbCtx; foreach (var app in appsToLink) { org.AddLink(app); } await org.UpdateAsync(dbCtx, org.Uuid); return(org); }
/// <summary> /// Creates and organisation account, register a user as an owner and registers the specified apps to be linked to it too /// </summary> /// <param name="org"></param> /// <param name="dbCtx"></param> /// <param name="owner"></param> /// <param name="appsToLink"></param> /// <returns></returns> public static async Task <Organization> CreateAsync(this Organization org, DbContext dbCtx, MapHiveUser owner, IEnumerable <string> appsToLink) { //finaly grab the apps that should be registered for the org and link them var mhDb = (MapHiveDbContext)dbCtx; var apps = await mhDb.Applications.Where(app => appsToLink.Contains(app.ShortName)) .OrderBy(app => app.ShortName) .ToListAsync(); return(await CreateAsync(org, dbCtx, owner, apps)); }
/// <summary> /// Creates a user acount, sends out email, modifies pass if a custom pass is provided; /// this is a simple wrapper over the standard user.CreateAsync that adds an option to provide a specific password /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbCtx"></param> /// <param name="user"></param> /// <param name="emailSender"></param> /// <param name="emailAccount"></param> /// <param name="emailTemplate"></param> /// <param name="password"></param> /// <returns></returns> public static async Task <CreateUserAccountOutput> CreateUserAccountAsync(DbContext dbCtx, MapHiveUser user, IEmailSender emailSender, IEmailAccount emailAccount = null, IEmailTemplate emailTemplate = null, string password = null) { var output = new CreateUserAccountOutput { User = user }; //see if a user has already been created. if so do not attempt to create it; if (user.Uuid != default(Guid)) { return(output); } //need to grab an initial pass to change it if a pass has been provided var initialPass = string.Empty; //wire up an evt listener, so can react to user created evt and send a confirmation email user.UserCreated += (sender, args) => { initialPass = (string)args.OperationFeedback["InitialPassword"]; //output, so can use it in the m2m tests output.InitialPassword = initialPass; output.VerificationKey = Auth.MergeIdWithToken( user.Uuid, (string)args.OperationFeedback["VerificationKey"] ); //prepare email if present emailTemplate?.Prepare(args.OperationFeedback); if (emailTemplate != null && emailAccount != null) { emailSender.Send( emailAccount, emailTemplate, user.Email ); } }; //create user without auto email send here - it's customised and sent via evt handler above var createdUser = await user.CreateAsync(dbCtx); //once user has been created adjust his pass if provided if (!string.IsNullOrEmpty(password)) { //grab user manager var userManager = MapHive.Core.Identity.UserManagerUtils.GetUserManager(); var idUser = await userManager.FindByIdAsync(user.Uuid.ToString()); await userManager.ChangePasswordAsync(idUser, initialPass, password); } return(output); }
/// <summary> /// Returns user application credentials within an organization /// </summary> /// <param name="dbCtx"></param> /// <param name="user"></param> /// <param name="app"></param> /// <returns></returns> public async Task <OrgUserAppAccessCredentials> GetUserAppAccessCredentialsAsync(DbContext dbCtx, MapHiveUser user, Application app) { var output = new OrgUserAppAccessCredentials { User = user, Organization = this, Application = app }; //check if organization can access an application and if not, then good bye if (!await CanUseAppAsync(dbCtx, app)) { return(output); } //ignore api apps; non authorized apps are accessible anyway //and apis just need to be assigned to an organisation in order to be accessible if (app.IsApi) { output.CanUseApp = true; return(output); } //check if user is an org owner or org admin output.IsAppAdmin = await IsOrgOwnerAsync(dbCtx, user) || await IsOrgAdminAsync(dbCtx, user); output.CanUseApp = output.IsAppAdmin; //user is not granted app admin access via org owner / org admin roles, so need to check if user can access the app as a 'normal' user, so via teams //note: teams can also grant app admin role! if (!output.IsAppAdmin) { var orgTeams = await this.GetChildrenAsync <Organization, Team>(dbCtx); foreach (var team in orgTeams) { //get team's app link var teamAppLink = await team.GetChildLinkAsync(dbCtx, app); //make sure team grants access to an app if (teamAppLink == null) { continue; } //get team's user link var teamUserLink = await team.GetChildLinkAsync(dbCtx, user); var userCanUseApp = teamUserLink != null; //if a team grants an access to an app for a user we can test if it also test if it is admin access if (userCanUseApp) { //only apply true assignment here so it does not get reset when searching for app admin credentials! output.CanUseApp = userCanUseApp; //extract app access credentials link data var appAccessCredentials = teamAppLink.LinkData.GetByKey(Team.AppAccessCredentialsLinkDataObject); output.IsAppAdmin = appAccessCredentials != null && appAccessCredentials.ContainsKey(Team.AppAdminAccess) && (bool)appAccessCredentials[Team.AppAdminAccess]; } //no point in testing other teams, as full app access is already granted if (output.IsAppAdmin) { break; } } } //if this is a default (dashboard) app a user should ALWAYS BE ABLE TO use it; the default (dashboard) app will take care of handling the context itself if (app.IsDefault) { output.CanUseApp = true; } return(output); }
/// <summary> /// Removes owner from an organisation; this is done by simply removing a link to the admin role /// </summary> /// <param name="org"></param> /// <param name="dbCtx"></param> /// <param name="u"></param> /// <returns></returns> public static async Task RemoveAdminAsync(this Organization org, DbContext dbCtx, MapHiveUser u) { //first grab the admin role for the org var adminR = await org.GetOrgAdminRoleAsync(dbCtx); //and remove the link var mhDb = (MapHiveDbContext)dbCtx; mhDb.Links.Remove( mhDb.Links.FirstOrDefault(r => r.ParentUuid == u.Uuid && r.ChildUuid == adminR.Uuid)); await mhDb.SaveChangesAsync(); }
/// <summary> /// Adds a user to an org as an admin. /// </summary> /// <param name="org"></param> /// <param name="dbCtx"></param> /// <param name="u"></param> /// <returns></returns> public static async Task AddAdminAsync(this Organization org, DbContext dbCtx, MapHiveUser u) { //first find the admin role - it should always be there as it is created on org create var adminR = await org.GetOrgAdminRoleAsync(dbCtx); //and next link it to a user u.AddLink(adminR); await u.UpdateWithNoIdentityChangesAsync <MapHiveUser>(dbCtx); //and also add a user to an org - this will not change anything if a user is already linked org.AddLink(u); await org.UpdateAsync(dbCtx, org.Uuid); }
/// <summary> /// /// </summary> /// <param name="org"></param> /// <param name="dbCtx"></param> /// <param name="u"></param> /// <returns></returns> public static async Task AddOwnerAsync(this Organization org, DbContext dbCtx, MapHiveUser u) { //first find the owner role - it should always be there as it is created on org create var ownerR = await org.GetOrgOwnerRoleAsync(dbCtx); //and next link it to a user u.AddLink(ownerR); await u.UpdateWithNoIdentityChangesAsync <MapHiveUser>(dbCtx); //and also add a user to an org org.AddLink(u); await org.UpdateAsync(dbCtx); }
/// <summary> /// Gets apps visible by a user /// </summary> /// <param name="dbCtx"></param> /// <param name="userId"></param> /// <param name="orgIdentifier"></param> /// <returns></returns> public static async Task <IEnumerable <Application> > GetUserAppsAsync(DbContext dbCtx, Guid?userId, string orgIdentifier = null) { var appCollector = new List <Application>(); //common apps //do not return hive apps! they should not be listed in user apps even though they will usually be common apps var commonApps = await dbCtx.Set <Application>().Where(a => a.IsCommon && !a.IsHive).ToListAsync(); appCollector.AddRange(commonApps); MapHiveUser user = null; if (userId.HasValue) { user = await dbCtx.Set <MapHiveUser>().FirstOrDefaultAsync(u => u.Uuid == userId); } Organization org = null; if (user != null && !string.IsNullOrEmpty(orgIdentifier)) { org = await dbCtx.Set <Organization>().FirstOrDefaultAsync(o => o.Slug == orgIdentifier); } //get org apps - the apps that are not public, but assigned to orgs directly if (user != null && org != null) { var orgApps = await org.GetChildrenAsync <Organization, Application>(dbCtx); foreach (var app in orgApps) { if (!appCollector.Exists(a => a.Uuid == app.Uuid)) { appCollector.Add(app); } } } var outApps = new List <Application>(); foreach (var a in appCollector) { if ( !a.IsApi && //discard apis, they are not 'user apps' ( a.IsDefault || //always return the dashboard (a.IsCommon && !a.RequiresAuth) || //and the public apps with no auth (org != null && (await org.GetUserAppAccessCredentialsAsync(dbCtx, user, a)).CanUseApp) ) ) { outApps.Add(a); } } //TODO - more ordering - stuff like special apps that are not public, but assigned to orgs directly, etc. Also, maybe some differentiation between freely accessible apps and the apps with auth. //stuff like home & dashboard always at the beginning and such... return(outApps.OrderByDescending(a => a.IsHome).ThenByDescending(a => a.IsDefault).ThenBy(a => a.Name));; }