private static void ValidateGroupMemberList(ServiceBE groupService, UserBE[] potentialMembers) { //Groups belonging to built-in auth service are allowed to contain users from remote services if (!ServiceBL.IsLocalAuthService(groupService)) { foreach (UserBE u in potentialMembers) { if (u.ServiceId != groupService.Id) { throw new GroupMembersRequireSameAuthInvalidOperationException(); } } } }
public static ServiceBE PostServiceFromXml(XDoc serviceDoc, ServiceBE serviceToProcess) { uint? serviceId; string SID, description, uri; bool? statusEnabled; bool? localInit; ServiceType? type; NameValueCollection config, preferences; ParseServiceXml(serviceDoc, out serviceId, out SID, out description, out statusEnabled, out localInit, out type, out uri, out config, out preferences); //new service if (serviceToProcess == null && (serviceId == null || serviceId == 0)) { //convert XML input to a service object serviceToProcess = NewServiceFromXml(serviceDoc, SID, description, statusEnabled, localInit, type, uri, config, preferences); //insert the service serviceId = DbUtils.CurrentSession.Services_Insert(serviceToProcess); // reload the service serviceToProcess = DbUtils.CurrentSession.Services_GetById(serviceId.Value); } else { //Validate logic of given xml if (ServiceBL.IsLocalAuthService(serviceToProcess)) { throw new ServiceCannotModifyBuiltInAuthInvalidOperationException(); } if (((uri ?? string.Empty) != string.Empty) && (localInit ?? false)) { throw new ServiceCannotSetLocalUriInvalidOperationException(); } //Stop the service before making any changes serviceToProcess = StopService(serviceToProcess); //convert XML input to a service object. serviceToProcess = UpdateServiceFromParsedXml(serviceToProcess, SID, description, statusEnabled, localInit, type, uri, config, preferences); //Update existing service serviceToProcess = UpdateService(serviceToProcess); } return(serviceToProcess); }
private static UserBE RenameUser(UserBE user, string newUserName, string newFullName) { //Renaming requires admin rights. PermissionsBL.CheckUserAllowed(DekiContext.Current.User, Permissions.ADMIN); if (!ServiceBL.IsLocalAuthService(user.ServiceId)) { //TODO MaxM: allow renaming of external users throw new ExternalUserRenameNotImplementedExceptionException(); } //Check for already existing user with same name UserBE existingUser = DbUtils.CurrentSession.Users_GetByName(newUserName); if (existingUser != null) { throw new UserWithIdExistsConflictException(existingUser.Name, existingUser.ID); } PageBE existingTargetUserHomePage = PageBL.GetPageByTitle(Title.FromUIUsername(newUserName)); if (existingTargetUserHomePage != null && existingTargetUserHomePage.ID != 0 && !existingTargetUserHomePage.IsRedirect) { throw new UserHomepageRenameConflictException(); } //Try to move the homepage. PageBE userHomePage = GetHomePage(user); if (userHomePage != null && userHomePage.ID != 0) { Title newTitle = Title.FromUIUsername(newUserName); // new user homepage displayname is the user's full name or rebuilt from the username newTitle.DisplayName = !string.IsNullOrEmpty(newFullName) ? newFullName : newTitle.AsUserFriendlyDisplayName(); PageBL.MovePage(userHomePage, newTitle, true); } //Rename the user user.Name = newUserName; UserBL.UpdateUser(user); return(user); }
public static UserBE CreateOrUpdateUser(UserBE user, string newPassword) { if (user.ID > 0) { UpdateUser(user); } else { //TODO consider logic here to confirm that the user does not yet exist. user = CreateNewUser(user); } if (!string.IsNullOrEmpty(newPassword) && ServiceBL.IsLocalAuthService(user.ServiceId)) { user = UserBL.SetPassword(user, newPassword, false); DekiContext.Current.Instance.EventSink.UserChangePassword(DekiContext.Current.Now, user); } return(user); }
private static UserBE[] ProcessGroupMemberInput(GroupBE group, XDoc userList) { if (!userList.HasName("users")) { throw new GroupExpectedUserRootNodeInvalidArgumentException(); } ServiceBE service = ServiceBL.GetServiceById(group.ServiceId); if (service == null) { throw new GroupServiceNotFoundFatalException(group.ServiceId, group.Name); } //Changing members of an external group is not supported. You may modify members in the external provider instead. if (!ServiceBL.IsLocalAuthService(service)) { throw new ExternalGroupMemberInvalidOperationException(); } UserBE[] members = ReadUserListXml(userList); ValidateGroupMemberList(service, members); return(members); }
//--- Class Methods --- public static UserBE Authenticate(DreamContext context, DreamMessage request, uint serviceId, bool autoCreateExternalUser, bool allowAnon, out bool altPassword) { UserBE user = null; altPassword = false; // Case 1: username/fullname, password, provider (login window) // 1. Validate & retrieve fullname using credentials // Failed -> return null // 2. Populate user object // A. Populates user info // B. Populates group info in user object // 3. Does fullname exist? // Yes -> Update user (email, fullname, ...) // No -> Create user // 4. Update Group information // 5. return user object // // Case 2: fullname, password (http, api, ...) // 1. Lookup full name, exist? // Yes -> return user // No -> return null // // Case 3: auth-token (header auth) // 0. Valid auth token? // No -> return null // 1. Lookup user by name // Found -> return user // Else -> return null string userName = null; string password = null; UserBE userFromToken = null; ServiceBE authService = null; // Extract authtoken and impersonation authtoken from request. // AllowAnon is false when in GET/POST: users/authenticate or when ?authenticate=true. // Standard user authtokens are ignored when AllowAnon=false but impersonation tokens are accepted. bool impersonationOnly = !allowAnon; userFromToken = UserFromAuthTokenInRequest(context, impersonationOnly); if (userFromToken == null) { HttpUtil.GetAuthentication(context.Uri.ToUri(), request.Headers, out userName, out password); } // check if we need to retrieve authentication service information if (serviceId > 0) { authService = ServiceBL.GetServiceById(serviceId); if (authService == null) { throw new AuthServiceIdInvalidArgumentException(serviceId); } if (authService.Type != ServiceType.AUTH) { throw new AuthNotAnAuthServiceInvalidArgumentException(serviceId); } } // check if a username was provided if (!string.IsNullOrEmpty(userName)) { //Case 2: Given username + password if (authService == null) { //Assuming local user or existing external account user = DbUtils.CurrentSession.Users_GetByName(userName); if (user != null) { serviceId = user.ServiceId; authService = ServiceBL.GetServiceById(serviceId); } else { LoginAccessDenied(context, request, userName, null, password); } } if (authService == null) { throw new AuthServiceIdInvalidArgumentException(serviceId); } if (authService.Type != ServiceType.AUTH) { throw new AuthNotAnAuthServiceInvalidArgumentException(serviceId); } if (user == null) { //Performing auth on local account if (ServiceBL.IsLocalAuthService(authService)) { user = DbUtils.CurrentSession.Users_GetByName(userName); } else { //Performing external auth. Lookup by external user name user = DbUtils.CurrentSession.Users_GetByExternalName(userName, authService.Id); } if (user != null && user.ServiceId != authService.Id) { ServiceBE currentUsersAuthService = ServiceBL.GetServiceById(user.ServiceId); if (currentUsersAuthService != null) { throw new AuthLoginExternalUserConflictException(currentUsersAuthService.Description); } throw new LoginExternalUserUnknownConflictException(); } } //Local account in the db. if (user != null && ServiceBL.IsLocalAuthService(authService)) { //Validate password for local account or validate the apikey if (!IsValidAuthenticationForLocalUser(user, password, out altPassword)) { // try impersonation using the ApiKey if (string.IsNullOrEmpty(password) && PermissionsBL.ValidateRequestApiKey()) { DekiContext.Current.Instance.Log.InfoFormat("user '{0}' authenticated via apikey impersonation", userName); } else { LoginAccessDenied(context, request, userName, user.ID, password); } } } // User was not found in the db and not being asked to create it. if (user == null && !autoCreateExternalUser) { LoginAccessDenied(context, request, userName, null, password); } // Creating local account if apikey checks out and our authservice is local if (user == null && string.IsNullOrEmpty(password) && PermissionsBL.ValidateRequestApiKey() && ServiceBL.IsLocalAuthService(authService)) { XDoc newUserDoc = new XDoc("user") .Elem("username", userName); DreamMessage newUserResponse = DekiContext.Current.ApiPlug.At("users") .With("apikey", DreamContext.Current.GetParam("apikey", string.Empty)) .Post(newUserDoc); user = UserBL.GetUserById(newUserResponse.ToDocument()["/user/@id"].AsUInt ?? 0); if (user != null && !string.IsNullOrEmpty(password)) { user = UserBL.SetPassword(user, password, false); } } // Got an external account // Passing in the user object from db if it was found. List <GroupBE> externalGroups = null; if (!ServiceBL.IsLocalAuthService(authService)) { bool bypassAuthentication = false; string externalName; if (user == null || string.IsNullOrEmpty(user.ExternalName)) { externalName = userName; } else { externalName = user.ExternalName; } // If apikey is valid, try to bypass auth with the external provider // and only lookup user/group details. if (string.IsNullOrEmpty(password) && PermissionsBL.ValidateRequestApiKey()) { DekiContext.Current.Instance.Log.InfoFormat("user '{0}' authenticating being bypassed via apikey impersonation", userName); bypassAuthentication = true; } user = ExternalServiceSA.BuildUserFromAuthService(authService, user, userName, bypassAuthentication, externalName, password, out externalGroups); } // User was not found or did not authenticate with external provider if (user == null) { LoginAccessDenied(context, request, userName, null, password); } else { //New user creation from external provider if (user.ID == 0) { if (!autoCreateExternalUser) { LoginAccessDenied(context, request, userName, null, password); } } else { //user exists // TODO (steveb): ??? } if (user.UserActive) { user = UserBL.CreateOrUpdateUser(user); if (externalGroups != null) { UserBL.UpdateUsersGroups(user, externalGroups.ToArray()); } } } } else if (userFromToken != null) { // valid token exists that resolved to a user user = userFromToken; } else if (allowAnon) { // Anonymous user user = DbUtils.CurrentSession.Users_GetByName(DekiWikiService.ANON_USERNAME); } if (user == null) { //No credentials. Or token not provided or is invalid. LoginAccessDenied(context, request, null, null, password); } else if (!user.UserActive && !PermissionsBL.ValidateRequestApiKey()) { //If a valid api key is provided, override the disabled account flag throw new AuthUserDisabledForbiddenException(user.Name); } return(user); }
public static UserBE BuildUserFromAuthService(ServiceBE serviceInfo, UserBE knownUser, string usernameToBuild, bool bypassAuthentication, string authusername, string password, out List <GroupBE> externalGroups) { externalGroups = null; if (serviceInfo == null || string.IsNullOrEmpty(usernameToBuild)) { return(null); } //Dont perform external lookup for disabled users if (knownUser != null && !knownUser.UserActive) { return(knownUser); } var errMsg = DekiResources.UNABLE_TO_AUTH_WITH_SERVICE(serviceInfo.Type, serviceInfo.SID, serviceInfo.Uri); if (knownUser != null && !string.IsNullOrEmpty(knownUser.ExternalName)) { usernameToBuild = knownUser.ExternalName; } UserBE ret = null; DreamMessage response = null; if (serviceInfo.Uri == null) { throw new ExternalServiceNotStartedFatalException(serviceInfo.Type, serviceInfo.SID); } try { Plug dekiExternalAuthPlug; //bypassAuthentication is used when you only need user details but not to necessarily authenticate if (bypassAuthentication) { //An external auth service's GET: user/{username} does not necessarily require authentication to lookup users but it may. It's up to the service //to decide if anon requests are allowed. dekiExternalAuthPlug = Plug.New(serviceInfo.Uri).At(USER_INFO).At(XUri.Encode(usernameToBuild)); } else { //Credentials are always needed for GET: authenticate. The user details of the auth'd user is returned with same format as GET: user/{username} dekiExternalAuthPlug = Plug.New(serviceInfo.Uri).At(AUTHENTICATE_PATH); } //Always include credentials with the request if they're supplied if (!string.IsNullOrEmpty(authusername)) { dekiExternalAuthPlug = dekiExternalAuthPlug.WithCredentials(authusername, password ?? string.Empty); } response = dekiExternalAuthPlug.GetAsync().Wait(); } catch (Exception x) { throw new ExternalServiceResponseException(errMsg, DreamMessage.InternalError(x)); } if (response.IsSuccessful) { XDoc userXml = response.ToDocument(); if (userXml == null || userXml.IsEmpty) { throw new ExternalAuthResponseFatalException(); } string nameFromAuthProvider = userXml["@name"].Contents; if (!nameFromAuthProvider.EqualsInvariantIgnoreCase(usernameToBuild)) { throw new ExternalServiceUnexpecteUsernameFatalException(userXml["@name"].AsText, usernameToBuild); } ret = knownUser ?? new UserBE(); ret.Email = string.IsNullOrEmpty(userXml["email"].AsText) ? (ret.Email ?? string.Empty) : userXml["email"].AsText; //Build the realname (exposed as 'fullname' in user xml) by saving it as '{firstname} {lastname}' string externalFirstName = userXml["firstname"].AsText ?? string.Empty; string externalLastName = userXml["lastname"].AsText ?? string.Empty; string separator = externalLastName.Length > 0 && externalFirstName.Length > 0 ? ", " : string.Empty; // NOTE (maxm): Fullname sync is disabled for now. Refer to bug 7855 #if !DISABLE_REAL_NAME_SYNCHRONIZATION ret.RealName = string.Format("{0}{1}{2}", externalLastName, separator, externalFirstName); #endif ret.ServiceId = serviceInfo.Id; ret.Touched = DateTime.UtcNow; ret.ExternalName = string.IsNullOrEmpty(ret.ExternalName) ? nameFromAuthProvider : ret.ExternalName; ret.Name = string.IsNullOrEmpty(ret.Name) ? nameFromAuthProvider : ret.Name; //For new users, the name must be normalized and unique if (ret.ID == 0) { string nameFromExternalName = string.Empty; //Allow using a displayname from an external provider only for new accounts if (!userXml["@displayname"].IsEmpty) { nameFromExternalName = userXml["@displayname"].AsText; } else { nameFromExternalName = ret.ExternalName; } ret.Name = UserBL.NormalizeExternalNameToWikiUsername(nameFromExternalName); } //Build group objects out of the user's group membership list externalGroups = new List <GroupBE>(); IList <GroupBE> userGroups = DbUtils.CurrentSession.Groups_GetByUser(ret.ID); //Preserve local groups for existing users if (ret.ID != 0 && userGroups != null) { foreach (GroupBE g in userGroups) { if (ServiceBL.IsLocalAuthService(g.ServiceId)) { externalGroups.Add(g); } } } foreach (XDoc group in userXml["groups/group"]) { GroupBE g = new GroupBE(); g.Name = group["@name"].AsText; g.ServiceId = serviceInfo.Id; if (!string.IsNullOrEmpty(g.Name)) { externalGroups.Add(g); } } } else { switch (response.Status) { case DreamStatus.Unauthorized: if (bypassAuthentication) { DekiContext.Current.Instance.Log.Warn(string.Format("Attempted to lookup user info on auth provider '{0}' but failed since it required credentials", serviceInfo.Id)); } throw new ExternalServiceAuthenticationDeniedException(DekiWikiService.AUTHREALM, serviceInfo.Description); default: throw new ExternalServiceResponseException(errMsg, response); } } return(ret); }
private static UserBE UpdateUserFromXml(UserBE userToUpdate, XDoc userDoc, string username, string email, string fullname, ServiceBE authservice, RoleBE role, bool?active, string externalusername, string externalpassword, string language, string timezone, out List <GroupBE> externalGroups) { externalGroups = null; if (userToUpdate.Name != username && !string.IsNullOrEmpty(username)) { if (UserBL.IsAnonymous(userToUpdate)) { throw new UserAnonymousEditInvalidOperationException(); } userToUpdate = RenameUser(userToUpdate, username, fullname ?? userToUpdate.RealName); } //Modify a user's authentication service if (authservice != null && authservice.Id != userToUpdate.ServiceId) { if (UserBL.IsAnonymous(userToUpdate)) { throw new UserAnonymousEditInvalidOperationException(); } if (ServiceBL.IsLocalAuthService(authservice)) { //external to local userToUpdate.ExternalName = null; userToUpdate.ServiceId = authservice.Id; } else { //(local or external) to external userToUpdate = ExternalServiceSA.BuildUserFromAuthService(authservice, userToUpdate, userToUpdate.Name, true, externalusername, externalpassword, out externalGroups); if (userToUpdate == null) { throw new UserAuthChangeFatalException(); } //Does the external account already exist? UserBE matchingExternalAccount = DbUtils.CurrentSession.Users_GetByExternalName(userToUpdate.ExternalName, userToUpdate.ServiceId); if (matchingExternalAccount != null) { throw new ExternalUserExistsConflictException(matchingExternalAccount.Name, matchingExternalAccount.ExternalName, matchingExternalAccount.ServiceId); } } } if (email != null) { if (UserBL.IsAnonymous(userToUpdate) && email != userToUpdate.Email) { throw new UserAnonymousEditInvalidOperationException(); } userToUpdate.Email = email; } if (!string.IsNullOrEmpty(fullname)) { userToUpdate.RealName = fullname; } if (active != null) { // disabling user if (userToUpdate.UserActive && !active.Value) { // cannot disable anonymous user if (UserBL.IsAnonymous(userToUpdate)) { throw new UserAnonymousDeactivationInvalidOperationException(); } // cannot disable owner if (DekiContext.Current.LicenseManager.GetSiteOwnerUserId().GetValueOrDefault(0) == userToUpdate.ID) { throw new UserOwnerDeactivationConflict(); } } //throw exception if licensing does not allow activating a user if (!userToUpdate.UserActive && active.Value) { DekiContext.Current.LicenseManager.IsUserCreationAllowed(true); } userToUpdate.UserActive = active.Value; } if (role != null && role.ID != userToUpdate.RoleId) { PermissionsBL.CheckUserAllowed(DekiContext.Current.User, Permissions.ADMIN); userToUpdate.RoleId = role.ID; } if (language != null) { userToUpdate.Language = language; } if (timezone != null) { userToUpdate.Timezone = timezone; } return(userToUpdate); }
public static UserBE PostUserFromXml(XDoc userDoc, UserBE userToProcess, string accountpassword, string externalusername, string externalpassword) { List <GroupBE> externalGroups = null; uint? id; bool? active; string username, fullname, email, language, timezone; ServiceBE authService; RoleBE role; // parse the standard user XML doc ParseUserXml(userDoc, out id, out username, out email, out fullname, out authService, out role, out active, out language, out timezone); // new user if ((userToProcess == null) && ((id == null) || (id == 0))) { userToProcess = ReadUserXml(userDoc, username, email, fullname, authService, role, language, timezone); // external accounts should be confirmed, username normalized, groups retrieved if ((authService != null) && !ServiceBL.IsLocalAuthService(authService)) { // check if a local-account password was provided if (!string.IsNullOrEmpty(accountpassword)) { throw new ExternalUserPasswordInvalidOperationException(); } // only admins can create external accounts for others. Anyone can create their own external account if (externalusername != userToProcess.Name || externalusername == string.Empty) { PermissionsBL.CheckUserAllowed(DekiContext.Current.User, Permissions.ADMIN); } // username+password from request query params are used here userToProcess = ExternalServiceSA.BuildUserFromAuthService(authService, userToProcess, username, true, externalusername, externalpassword, out externalGroups); if (userToProcess == null) { throw new ExternalUserNotFoundException(username); } // does the external account already exist? UserBE matchingExternalAccount = DbUtils.CurrentSession.Users_GetByExternalName(userToProcess.ExternalName, userToProcess.ServiceId); if (matchingExternalAccount != null) { throw new ExternalUserExistsConflictException(matchingExternalAccount.Name, userToProcess.ExternalName, userToProcess.ServiceId); } } else { // creating local account // user creation requires admin rights unless the config flag allows it // anonymous users are not allowed to set role if (!DekiContext.Current.Instance.AllowAnonymousLocalAccountCreation || role != null) { PermissionsBL.CheckUserAllowed(DekiContext.Current.User, Permissions.ADMIN); } } // sanity check for already existing user UserBE existingUser = DbUtils.CurrentSession.Users_GetByName(userToProcess.Name); if (existingUser != null) { throw new UserWithIdExistsConflictException(existingUser.Name, existingUser.ID); } //if (UserDA.RetrieveUserRegistrations(userToProcess.Name)) { // throw new DreamAbortException(DreamMessage.Conflict(string.Format("User '{0}' has been reserved", userToProcess.Name))); //} userToProcess = CreateOrUpdateUser(userToProcess, accountpassword); if (null != externalGroups) { UpdateUsersGroups(userToProcess, externalGroups.ToArray()); } } else { // update existing user if (userToProcess == null) { userToProcess = GetUserById(id.Value); if (userToProcess == null) { throw new UserIdNotFoundException(id.Value); } } // TODO (steveb): either this needs to go or the definition for PUT:user/{userid} should not mention the 'accountpassword' parameter if (!string.IsNullOrEmpty(accountpassword)) { throw new UserUsePutToChangePasswordInvalidOperationException(); } // permission check if not modifying self if (userToProcess.ID != DekiContext.Current.User.ID) { PermissionsBL.CheckUserAllowed(DekiContext.Current.User, Permissions.ADMIN); } userToProcess = UpdateUserFromXml(userToProcess, userDoc, username, email, fullname, authService, role, active, externalusername, externalpassword, language, timezone, out externalGroups); userToProcess = CreateOrUpdateUser(userToProcess); if (null != externalGroups) { UpdateUsersGroups(userToProcess, externalGroups.ToArray()); } if (IsAnonymous(userToProcess) && DekiContext.Current.Instance.CacheAnonymousOutput) { DekiContext.Current.Deki.EmptyResponseCacheInternal(); } try { if (!userToProcess.UserActive && userToProcess.LicenseSeat && DekiContext.Current.LicenseManager.IsSeatLicensingEnabled()) { DekiContext.Current.LicenseManager.RemoveSeatFromUser(userToProcess); } } catch (Exception x) { _log.WarnExceptionFormat(x, "Unable to remove license seat for disabled user. user id: {0}", userToProcess.ID); } } return(userToProcess); }
public static GroupBE PostGroupFromXml(XDoc groupDoc, GroupBE groupToProcess, string externalusername, string externalpassword) { GroupBE group = null; string groupName = string.Empty; ServiceBE groupService = null; RoleBE groupRole = null; UserBE[] groupMembers = null; uint? groupId = null; ParseGroupXml(groupDoc, out groupId, out groupName, out groupService, out groupRole, out groupMembers); //Create new group if (groupToProcess == null && (groupId == null || groupId == 0)) { if (groupService == null) { groupService = ServiceBL.RetrieveLocalAuthService(); } //External groups should be confirmed with the auth provider if (groupService != null && !ServiceBL.IsLocalAuthService(groupService)) { //username+password from request query params are used here group = ExternalServiceSA.BuildGroupFromAuthService(groupService, groupToProcess, groupName, externalusername, externalpassword); if (group == null) { throw new ExternalGroupNotFoundException(groupName); } } //Does this group already exist? GroupBE tempGroup = GetGroupByName(groupName); if (tempGroup != null) { throw new GroupExistsWithServiceConflictException(groupName, tempGroup.ServiceId); } ValidateGroupMemberList(groupService, groupMembers); // Insert the group GroupBE newGroup = new GroupBE(); newGroup.Name = groupName; newGroup.RoleId = groupRole.ID; newGroup.ServiceId = groupService.Id; newGroup.CreatorUserId = DekiContext.Current.User.ID; newGroup.TimeStamp = DateTime.UtcNow; uint newGroupId = DbUtils.CurrentSession.Groups_Insert(newGroup); if (newGroupId == 0) { group = null; } else { DbUtils.CurrentSession.GroupMembers_UpdateUsersInGroup(newGroupId, groupMembers.Select(e => e.ID).ToList(), newGroup.TimeStamp); // reload the group to ensure group members are set group = GetGroupById(newGroupId); } } //Edit existing group else { if (groupId != null) { groupToProcess = GetGroupById(groupId.Value); } if (groupToProcess == null) { throw new GroupIdNotFoundException(groupId); } group = groupToProcess; //Change the role? if (group.RoleId != groupRole.ID) { group.RoleId = groupRole.ID; } //Rename the group? if (group.Name != groupName && !string.IsNullOrEmpty(groupName)) { GroupBE tempGroup = GetGroupByName(groupName); if (tempGroup != null) { throw new GroupExistsWithServiceConflictException(groupName, tempGroup.ServiceId); } if (!ServiceBL.IsLocalAuthService(group.ServiceId)) { //TODO MaxM: allow renaming of external groups throw new ExternalGroupRenameNotImplementedException(); } //Set the new name of the group. group.Name = groupName; } DbUtils.CurrentSession.Groups_Update(group); //TODO (MaxM): Update group list as well? group = GetGroupById(group.Id); } if (group == null) { throw new GroupCreateUpdateFatalException(); } return(group); }