From cd55c44c8fd8f61cdb7cbfb57a588be82c7aa0ab Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Fri, 20 Apr 2018 19:49:13 +0100 Subject: MM-8796: Full implementation of "Schemes" in Store/Model/App layers. (#8357) * Add Scheme model and stub store. * Port ChannelStore to be Scheme aware. * Make almost all the API/APP layer work with ChannelSchemes. Only thing still hacky is UpdateChannelMemberRoles(). * Add basic SchemeStore implementation. * Migrate UpdateChannelMemberRoles properly and fix tests. * Update store tests and mocks so they work. * Include creating default roles in Scheme create store function. * Implement role deletion and start scheme deletion. * Only use non-deleted roles for authorization. * Add GetByScheme method to Team store. * Add GetChannelsByScheme. * Update store mocks. * Implement scheme deletion in the store. * Rename is valid function. * Add offset and limit to queries to fetch teams and channels by scheme. * Fix queries. * Implement scheme awareness in Team store and add a migration. * Tidy up ChannelStore mapping functions and add exhaustive unit tests. * Add all missing i18n. * Proper tests for TeamStore internal functions and fix them. * Make additional TeamMember fields nullable. * Make new ChannelMember fields nullable. * Create new nullable columns without defaults. * Make new fields in large tables nullalble. * Fix empty list of TeamMembers. * Deduplicate SQL queries. * Fix spelling. * Fix review comment. * More review fixes. * More review fixes. --- store/sqlstore/scheme_supplier.go | 272 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 store/sqlstore/scheme_supplier.go (limited to 'store/sqlstore/scheme_supplier.go') diff --git a/store/sqlstore/scheme_supplier.go b/store/sqlstore/scheme_supplier.go new file mode 100644 index 000000000..278d1a3c4 --- /dev/null +++ b/store/sqlstore/scheme_supplier.go @@ -0,0 +1,272 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sqlstore + +import ( + "context" + "database/sql" + "fmt" + "net/http" + "strings" + + "github.com/mattermost/gorp" + + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/store" +) + +func initSqlSupplierSchemes(sqlStore SqlStore) { + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.Scheme{}, "Schemes").SetKeys(false, "Id") + table.ColMap("Id").SetMaxSize(26) + table.ColMap("Name").SetMaxSize(64) + table.ColMap("Description").SetMaxSize(1024) + table.ColMap("Scope").SetMaxSize(32) + table.ColMap("DefaultTeamAdminRole").SetMaxSize(64) + table.ColMap("DefaultTeamUserRole").SetMaxSize(64) + table.ColMap("DefaultChannelAdminRole").SetMaxSize(64) + table.ColMap("DefaultChannelUserRole").SetMaxSize(64) + } +} + +func (s *SqlSupplier) SchemeSave(ctx context.Context, scheme *model.Scheme, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult { + result := store.NewSupplierResult() + + if len(scheme.Id) == 0 { + if transaction, err := s.GetMaster().Begin(); err != nil { + result.Err = model.NewAppError("SqlSchemeStore.SaveScheme", "store.sql_scheme.save.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } else { + result = s.createScheme(ctx, scheme, transaction, hints...) + + if result.Err != nil { + transaction.Rollback() + } else if err := transaction.Commit(); err != nil { + result.Err = model.NewAppError("SqlSchemeStore.SchemeSave", "store.sql_scheme.save_scheme.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError) + } + } + } else { + if !scheme.IsValid() { + result.Err = model.NewAppError("SqlSchemeStore.Save", "store.sql_scheme.save.invalid_scheme.app_error", nil, "", http.StatusBadRequest) + return result + } + + scheme.UpdateAt = model.GetMillis() + + if rowsChanged, err := s.GetMaster().Update(scheme); err != nil { + result.Err = model.NewAppError("SqlSchemeStore.Save", "store.sql_scheme.save.update.app_error", nil, err.Error(), http.StatusInternalServerError) + } else if rowsChanged != 1 { + result.Err = model.NewAppError("SqlSchemeStore.Save", "store.sql_scheme.save.update.app_error", nil, "no record to update", http.StatusInternalServerError) + } + + result.Data = scheme + } + + return result +} + +func (s *SqlSupplier) createScheme(ctx context.Context, scheme *model.Scheme, transaction *gorp.Transaction, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult { + result := store.NewSupplierResult() + + // Fetch the default system scheme roles to populate default permissions. + defaultRoleNames := []string{model.TEAM_ADMIN_ROLE_ID, model.TEAM_USER_ROLE_ID, model.CHANNEL_ADMIN_ROLE_ID, model.CHANNEL_USER_ROLE_ID} + defaultRoles := make(map[string]*model.Role) + if rolesResult := s.RoleGetByNames(ctx, defaultRoleNames); rolesResult.Err != nil { + result.Err = rolesResult.Err + return result + } else { + for _, role := range rolesResult.Data.([]*model.Role) { + switch role.Name { + case model.TEAM_ADMIN_ROLE_ID: + defaultRoles[model.TEAM_ADMIN_ROLE_ID] = role + case model.TEAM_USER_ROLE_ID: + defaultRoles[model.TEAM_USER_ROLE_ID] = role + case model.CHANNEL_ADMIN_ROLE_ID: + defaultRoles[model.CHANNEL_ADMIN_ROLE_ID] = role + case model.CHANNEL_USER_ROLE_ID: + defaultRoles[model.CHANNEL_USER_ROLE_ID] = role + } + } + + if len(defaultRoles) != 4 { + result.Err = model.NewAppError("SqlSchemeStore.SaveScheme", "store.sql_scheme.save.retrieve_default_scheme_roles.app_error", nil, "", http.StatusInternalServerError) + return result + } + } + + // Create the appropriate default roles for the scheme. + if scheme.Scope == model.SCHEME_SCOPE_TEAM { + // Team Admin Role + teamAdminRole := &model.Role{ + Name: model.NewId(), + DisplayName: fmt.Sprintf("Team Admin Role for Scheme %s", scheme.Name), + Permissions: defaultRoles[model.TEAM_ADMIN_ROLE_ID].Permissions, + SchemeManaged: true, + } + + if saveRoleResult := s.createRole(ctx, teamAdminRole, transaction); saveRoleResult.Err != nil { + result.Err = saveRoleResult.Err + return result + } else { + scheme.DefaultTeamAdminRole = saveRoleResult.Data.(*model.Role).Id + } + + // Team User Role + teamUserRole := &model.Role{ + Name: model.NewId(), + DisplayName: fmt.Sprintf("Team User Role for Scheme %s", scheme.Name), + Permissions: defaultRoles[model.TEAM_USER_ROLE_ID].Permissions, + SchemeManaged: true, + } + + if saveRoleResult := s.createRole(ctx, teamUserRole, transaction); saveRoleResult.Err != nil { + result.Err = saveRoleResult.Err + return result + } else { + scheme.DefaultTeamUserRole = saveRoleResult.Data.(*model.Role).Id + } + } + if scheme.Scope == model.SCHEME_SCOPE_TEAM || scheme.Scope == model.SCHEME_SCOPE_CHANNEL { + // Channel Admin Role + channelAdminRole := &model.Role{ + Name: model.NewId(), + DisplayName: fmt.Sprintf("Channel Admin Role for Scheme %s", scheme.Name), + Permissions: defaultRoles[model.CHANNEL_ADMIN_ROLE_ID].Permissions, + SchemeManaged: true, + } + + if saveRoleResult := s.createRole(ctx, channelAdminRole, transaction); saveRoleResult.Err != nil { + result.Err = saveRoleResult.Err + return result + } else { + scheme.DefaultChannelAdminRole = saveRoleResult.Data.(*model.Role).Id + } + + // Channel User Role + channelUserRole := &model.Role{ + Name: model.NewId(), + DisplayName: fmt.Sprintf("Channel User Role for Scheme %s", scheme.Name), + Permissions: defaultRoles[model.CHANNEL_USER_ROLE_ID].Permissions, + SchemeManaged: true, + } + + if saveRoleResult := s.createRole(ctx, channelUserRole, transaction); saveRoleResult.Err != nil { + result.Err = saveRoleResult.Err + return result + } else { + scheme.DefaultChannelUserRole = saveRoleResult.Data.(*model.Role).Id + } + } + + scheme.Id = model.NewId() + scheme.CreateAt = model.GetMillis() + scheme.UpdateAt = scheme.CreateAt + + // Validate the scheme + if !scheme.IsValidForCreate() { + result.Err = model.NewAppError("SqlSchemeStore.Save", "store.sql_scheme.save.invalid_scheme.app_error", nil, "", http.StatusBadRequest) + return result + } + + if err := transaction.Insert(scheme); err != nil { + result.Err = model.NewAppError("SqlSchemeStore.Save", "store.sql_scheme.save.insert.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + result.Data = scheme + + return result +} + +func (s *SqlSupplier) SchemeGet(ctx context.Context, schemeId string, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult { + result := store.NewSupplierResult() + + var scheme model.Scheme + + if err := s.GetReplica().SelectOne(&scheme, "SELECT * from Schemes WHERE Id = :Id", map[string]interface{}{"Id": schemeId}); err != nil { + if err == sql.ErrNoRows { + result.Err = model.NewAppError("SqlSchemeStore.Get", "store.sql_scheme.get.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusNotFound) + } else { + result.Err = model.NewAppError("SqlSchemeStore.Get", "store.sql_scheme.get.app_error", nil, err.Error(), http.StatusInternalServerError) + } + } + + result.Data = &scheme + + return result +} + +func (s *SqlSupplier) SchemeDelete(ctx context.Context, schemeId string, hints ...store.LayeredStoreHint) *store.LayeredStoreSupplierResult { + result := store.NewSupplierResult() + + // Get the scheme + var scheme model.Scheme + if err := s.GetReplica().SelectOne(&scheme, "SELECT * from Schemes WHERE Id = :Id", map[string]interface{}{"Id": schemeId}); err != nil { + if err == sql.ErrNoRows { + result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.get.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusNotFound) + } else { + result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.get.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError) + } + + return result + } + + // Check that the scheme isn't being used on any Teams or Channels. + if scheme.Scope == model.SCHEME_SCOPE_TEAM { + if c, err := s.GetReplica().SelectInt("SELECT COUNT(*) FROM Teams WHERE SchemeId = :SchemeId", map[string]interface{}{"SchemeId": schemeId}); err != nil { + result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.team_count.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError) + return result + } else { + if c > 0 { + result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.delete.scheme_in_use.app_error", nil, "Id="+schemeId, http.StatusInternalServerError) + return result + } + } + } else if scheme.Scope == model.SCHEME_SCOPE_CHANNEL { + if c, err := s.GetReplica().SelectInt("SELECT COUNT(*) FROM Channels WHERE SchemeId = :SchemeId", map[string]interface{}{"SchemeId": schemeId}); err != nil { + result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.channel_count.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError) + return result + } else { + if c > 0 { + result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.delete.scheme_in_use.app_error", nil, "Id="+schemeId, http.StatusInternalServerError) + return result + } + } + } + + // Delete the roles belonging to the scheme. + roleIds := []string{scheme.DefaultChannelUserRole, scheme.DefaultChannelAdminRole} + if scheme.Scope == model.SCHEME_SCOPE_TEAM { + roleIds = append(roleIds, scheme.DefaultTeamUserRole, scheme.DefaultTeamAdminRole) + } + + var inQueryList []string + queryArgs := make(map[string]interface{}) + for i, roleId := range roleIds { + inQueryList = append(inQueryList, fmt.Sprintf(":RoleId%v", i)) + queryArgs[fmt.Sprintf("RoleId%v", i)] = roleId + } + inQuery := strings.Join(inQueryList, ", ") + + time := model.GetMillis() + queryArgs["UpdateAt"] = time + queryArgs["DeleteAt"] = time + + if _, err := s.GetMaster().Exec("UPDATE Roles SET UpdateAt = :UpdateAt, DeleteAt = :DeleteAt WHERE Id IN ("+inQuery+")", queryArgs); err != nil { + result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.delete.role_update.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError) + return result + } + + // Delete the scheme itself. + scheme.UpdateAt = time + scheme.DeleteAt = time + + if rowsChanged, err := s.GetMaster().Update(&scheme); err != nil { + result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.delete.update.app_error", nil, "Id="+schemeId+", "+err.Error(), http.StatusInternalServerError) + } else if rowsChanged != 1 { + result.Err = model.NewAppError("SqlSchemeStore.Delete", "store.sql_scheme.delete.update.app_error", nil, "no record to update", http.StatusInternalServerError) + } else { + result.Data = &scheme + } + + return result +} -- cgit v1.2.3-1-g7c22