github.com/masterhung0112/hk_server/v5@v5.0.0-20220302090640-ec71aef15e1c/api4/config.go (about)

     1  package api4
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"reflect"
     7  	"strings"
     8  
     9  	"github.com/masterhung0112/hk_server/v5/audit"
    10  	"github.com/masterhung0112/hk_server/v5/config"
    11  	"github.com/masterhung0112/hk_server/v5/model"
    12  	"github.com/masterhung0112/hk_server/v5/shared/mlog"
    13  	"github.com/masterhung0112/hk_server/v5/utils"
    14  )
    15  
    16  var writeFilter func(c *Context, structField reflect.StructField) bool
    17  var readFilter func(c *Context, structField reflect.StructField) bool
    18  var permissionMap map[string]*model.Permission
    19  
    20  type filterType string
    21  
    22  const (
    23  	FilterTypeWrite filterType = "write"
    24  	FilterTypeRead  filterType = "read"
    25  )
    26  
    27  func (api *API) InitConfig() {
    28  	api.BaseRoutes.ApiRoot.Handle("/config", api.ApiSessionRequired(getConfig)).Methods("GET")
    29  	api.BaseRoutes.ApiRoot.Handle("/config", api.ApiSessionRequired(updateConfig)).Methods("PUT")
    30  	api.BaseRoutes.ApiRoot.Handle("/config/patch", api.ApiSessionRequired(patchConfig)).Methods("PUT")
    31  	api.BaseRoutes.ApiRoot.Handle("/config/reload", api.ApiSessionRequired(configReload)).Methods("POST")
    32  	api.BaseRoutes.ApiRoot.Handle("/config/client", api.ApiHandler(getClientConfig)).Methods("GET")
    33  	api.BaseRoutes.ApiRoot.Handle("/config/environment", api.ApiSessionRequired(getEnvironmentConfig)).Methods("GET")
    34  	api.BaseRoutes.ApiRoot.Handle("/config/migrate", api.ApiSessionRequired(migrateConfig)).Methods("POST")
    35  }
    36  
    37  func init() {
    38  	writeFilter = makeFilterConfigByPermission(FilterTypeWrite)
    39  	readFilter = makeFilterConfigByPermission(FilterTypeRead)
    40  	permissionMap = map[string]*model.Permission{}
    41  	for _, p := range model.AllPermissions {
    42  		permissionMap[p.Id] = p
    43  	}
    44  }
    45  
    46  func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
    47  	if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
    48  		c.SetPermissionError(model.SysconsoleReadPermissions...)
    49  		return
    50  	}
    51  
    52  	auditRec := c.MakeAuditRecord("getConfig", audit.Fail)
    53  	defer c.LogAuditRec(auditRec)
    54  
    55  	cfg, err := config.Merge(&model.Config{}, c.App.GetSanitizedConfig(), &utils.MergeConfig{
    56  		StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
    57  			return readFilter(c, structField)
    58  		},
    59  	})
    60  	if err != nil {
    61  		c.Err = model.NewAppError("getConfig", "api.config.get_config.restricted_merge.app_error", nil, err.Error(), http.StatusInternalServerError)
    62  	}
    63  
    64  	auditRec.Success()
    65  
    66  	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
    67  	if c.App.Srv().License() != nil && *c.App.Srv().License().Features.Cloud {
    68  		w.Write([]byte(cfg.ToJsonFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)))
    69  	} else {
    70  		w.Write([]byte(cfg.ToJson()))
    71  	}
    72  }
    73  
    74  func configReload(c *Context, w http.ResponseWriter, r *http.Request) {
    75  	auditRec := c.MakeAuditRecord("configReload", audit.Fail)
    76  	defer c.LogAuditRec(auditRec)
    77  
    78  	if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PERMISSION_RELOAD_CONFIG) {
    79  		c.SetPermissionError(model.PERMISSION_RELOAD_CONFIG)
    80  		return
    81  	}
    82  
    83  	if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
    84  		c.Err = model.NewAppError("configReload", "api.restricted_system_admin", nil, "", http.StatusBadRequest)
    85  		return
    86  	}
    87  
    88  	c.App.ReloadConfig()
    89  
    90  	auditRec.Success()
    91  
    92  	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
    93  	ReturnStatusOK(w)
    94  }
    95  
    96  func updateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
    97  	cfg := model.ConfigFromJson(r.Body)
    98  	if cfg == nil {
    99  		c.SetInvalidParam("config")
   100  		return
   101  	}
   102  
   103  	auditRec := c.MakeAuditRecord("updateConfig", audit.Fail)
   104  	defer c.LogAuditRec(auditRec)
   105  
   106  	cfg.SetDefaults()
   107  
   108  	if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleWritePermissions) {
   109  		c.SetPermissionError(model.SysconsoleWritePermissions...)
   110  		return
   111  	}
   112  
   113  	appCfg := c.App.Config()
   114  	if *appCfg.ServiceSettings.SiteURL != "" && *cfg.ServiceSettings.SiteURL == "" {
   115  		c.Err = model.NewAppError("updateConfig", "api.config.update_config.clear_siteurl.app_error", nil, "", http.StatusBadRequest)
   116  		return
   117  	}
   118  
   119  	var err1 error
   120  	cfg, err1 = config.Merge(appCfg, cfg, &utils.MergeConfig{
   121  		StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
   122  			return writeFilter(c, structField)
   123  		},
   124  	})
   125  	if err1 != nil {
   126  		c.Err = model.NewAppError("updateConfig", "api.config.update_config.restricted_merge.app_error", nil, err1.Error(), http.StatusInternalServerError)
   127  	}
   128  
   129  	// Do not allow plugin uploads to be toggled through the API
   130  	cfg.PluginSettings.EnableUploads = appCfg.PluginSettings.EnableUploads
   131  
   132  	// Do not allow certificates to be changed through the API
   133  	cfg.PluginSettings.SignaturePublicKeyFiles = appCfg.PluginSettings.SignaturePublicKeyFiles
   134  
   135  	c.App.HandleMessageExportConfig(cfg, appCfg)
   136  
   137  	err := cfg.IsValid()
   138  	if err != nil {
   139  		c.Err = err
   140  		return
   141  	}
   142  
   143  	oldCfg, newCfg, err := c.App.SaveConfig(cfg, true)
   144  	if err != nil {
   145  		c.Err = err
   146  		return
   147  	}
   148  
   149  	diffs, diffErr := config.Diff(oldCfg, newCfg)
   150  	if diffErr != nil {
   151  		c.Err = model.NewAppError("updateConfig", "api.config.update_config.diff.app_error", nil, diffErr.Error(), http.StatusInternalServerError)
   152  		return
   153  	}
   154  	auditRec.AddMeta("diff", diffs)
   155  
   156  	newCfg.Sanitize()
   157  
   158  	cfg, mergeErr := config.Merge(&model.Config{}, newCfg, &utils.MergeConfig{
   159  		StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
   160  			return readFilter(c, structField)
   161  		},
   162  	})
   163  	if mergeErr != nil {
   164  		c.Err = model.NewAppError("getConfig", "api.config.update_config.restricted_merge.app_error", nil, err.Error(), http.StatusInternalServerError)
   165  	}
   166  
   167  	auditRec.Success()
   168  	c.LogAudit("updateConfig")
   169  
   170  	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
   171  	if c.App.Srv().License() != nil && *c.App.Srv().License().Features.Cloud {
   172  		w.Write([]byte(cfg.ToJsonFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)))
   173  	} else {
   174  		w.Write([]byte(cfg.ToJson()))
   175  	}
   176  }
   177  
   178  func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
   179  	format := r.URL.Query().Get("format")
   180  
   181  	if format == "" {
   182  		c.Err = model.NewAppError("getClientConfig", "api.config.client.old_format.app_error", nil, "", http.StatusNotImplemented)
   183  		return
   184  	}
   185  
   186  	if format != "old" {
   187  		c.SetInvalidParam("format")
   188  		return
   189  	}
   190  
   191  	var config map[string]string
   192  	if c.AppContext.Session().UserId == "" {
   193  		config = c.App.LimitedClientConfigWithComputed()
   194  	} else {
   195  		config = c.App.ClientConfigWithComputed()
   196  	}
   197  
   198  	w.Write([]byte(model.MapToJson(config)))
   199  }
   200  
   201  func getEnvironmentConfig(c *Context, w http.ResponseWriter, r *http.Request) {
   202  	// Only return the environment variables for the subsections which the client is
   203  	// allowed to see
   204  	envConfig := c.App.GetEnvironmentConfig(func(structField reflect.StructField) bool {
   205  		return readFilter(c, structField)
   206  	})
   207  
   208  	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
   209  	w.Write([]byte(model.StringInterfaceToJson(envConfig)))
   210  }
   211  
   212  func patchConfig(c *Context, w http.ResponseWriter, r *http.Request) {
   213  	cfg := model.ConfigFromJson(r.Body)
   214  	if cfg == nil {
   215  		c.SetInvalidParam("config")
   216  		return
   217  	}
   218  
   219  	auditRec := c.MakeAuditRecord("patchConfig", audit.Fail)
   220  	defer c.LogAuditRec(auditRec)
   221  
   222  	if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleWritePermissions) {
   223  		c.SetPermissionError(model.SysconsoleWritePermissions...)
   224  		return
   225  	}
   226  
   227  	appCfg := c.App.Config()
   228  	if *appCfg.ServiceSettings.SiteURL != "" && cfg.ServiceSettings.SiteURL != nil && *cfg.ServiceSettings.SiteURL == "" {
   229  		c.Err = model.NewAppError("patchConfig", "api.config.update_config.clear_siteurl.app_error", nil, "", http.StatusBadRequest)
   230  		return
   231  	}
   232  
   233  	filterFn := func(structField reflect.StructField, base, patch reflect.Value) bool {
   234  		return writeFilter(c, structField)
   235  	}
   236  
   237  	// Do not allow plugin uploads to be toggled through the API
   238  	if cfg.PluginSettings.EnableUploads != nil && *cfg.PluginSettings.EnableUploads != *appCfg.PluginSettings.EnableUploads {
   239  		c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]interface{}{"Name": "PluginSettings.EnableUploads"}, "", http.StatusForbidden)
   240  		return
   241  	}
   242  
   243  	if cfg.MessageExportSettings.EnableExport != nil {
   244  		c.App.HandleMessageExportConfig(cfg, appCfg)
   245  	}
   246  
   247  	updatedCfg, mergeErr := config.Merge(appCfg, cfg, &utils.MergeConfig{
   248  		StructFieldFilter: filterFn,
   249  	})
   250  
   251  	if mergeErr != nil {
   252  		c.Err = model.NewAppError("patchConfig", "api.config.update_config.restricted_merge.app_error", nil, mergeErr.Error(), http.StatusInternalServerError)
   253  		return
   254  	}
   255  
   256  	err := updatedCfg.IsValid()
   257  	if err != nil {
   258  		c.Err = err
   259  		return
   260  	}
   261  
   262  	oldCfg, newCfg, err := c.App.SaveConfig(updatedCfg, true)
   263  	if err != nil {
   264  		c.Err = err
   265  		return
   266  	}
   267  	diffs, diffErr := config.Diff(oldCfg, newCfg)
   268  	if diffErr != nil {
   269  		c.Err = model.NewAppError("patchConfig", "api.config.patch_config.diff.app_error", nil, diffErr.Error(), http.StatusInternalServerError)
   270  		return
   271  	}
   272  	auditRec.AddMeta("diff", diffs)
   273  
   274  	newCfg.Sanitize()
   275  
   276  	auditRec.Success()
   277  
   278  	cfg, mergeErr = config.Merge(&model.Config{}, newCfg, &utils.MergeConfig{
   279  		StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
   280  			return readFilter(c, structField)
   281  		},
   282  	})
   283  	if mergeErr != nil {
   284  		c.Err = model.NewAppError("getConfig", "api.config.patch_config.restricted_merge.app_error", nil, err.Error(), http.StatusInternalServerError)
   285  	}
   286  
   287  	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
   288  	if c.App.Srv().License() != nil && *c.App.Srv().License().Features.Cloud {
   289  		w.Write([]byte(cfg.ToJsonFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)))
   290  	} else {
   291  		w.Write([]byte(cfg.ToJson()))
   292  	}
   293  }
   294  
   295  func makeFilterConfigByPermission(accessType filterType) func(c *Context, structField reflect.StructField) bool {
   296  	return func(c *Context, structField reflect.StructField) bool {
   297  		if structField.Type.Kind() == reflect.Struct {
   298  			return true
   299  		}
   300  
   301  		tagPermissions := strings.Split(structField.Tag.Get("access"), ",")
   302  
   303  		// If there are no access tag values and the role has manage_system, no need to continue
   304  		// checking permissions.
   305  		if len(tagPermissions) == 0 {
   306  			if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PERMISSION_MANAGE_SYSTEM) {
   307  				return true
   308  			}
   309  		}
   310  
   311  		// one iteration for write_restrictable value, it could be anywhere in the order of values
   312  		for _, val := range tagPermissions {
   313  			tagValue := strings.TrimSpace(val)
   314  			if tagValue == "" {
   315  				continue
   316  			}
   317  			// ConfigAccessTagWriteRestrictable trumps all other permissions
   318  			if tagValue == model.ConfigAccessTagWriteRestrictable || tagValue == model.ConfigAccessTagCloudRestrictable {
   319  				if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin && accessType == FilterTypeWrite {
   320  					return false
   321  				}
   322  				continue
   323  			}
   324  		}
   325  
   326  		// another iteration for permissions checks of other tag values
   327  		for _, val := range tagPermissions {
   328  			tagValue := strings.TrimSpace(val)
   329  			if tagValue == "" {
   330  				continue
   331  			}
   332  			if tagValue == model.ConfigAccessTagWriteRestrictable {
   333  				continue
   334  			}
   335  			if tagValue == model.ConfigAccessTagCloudRestrictable {
   336  				continue
   337  			}
   338  			if tagValue == model.ConfigAccessTagAnySysConsoleRead && accessType == FilterTypeRead &&
   339  				c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
   340  				return true
   341  			}
   342  
   343  			permissionID := fmt.Sprintf("sysconsole_%s_%s", accessType, tagValue)
   344  			if permission, ok := permissionMap[permissionID]; ok {
   345  				if c.App.SessionHasPermissionTo(*c.AppContext.Session(), permission) {
   346  					return true
   347  				}
   348  			} else {
   349  				mlog.Warn("Unrecognized config permissions tag value.", mlog.String("tag_value", permissionID))
   350  			}
   351  		}
   352  
   353  		// with manage_system, default to allow, otherwise default not-allow
   354  		return c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PERMISSION_MANAGE_SYSTEM)
   355  	}
   356  }
   357  
   358  func migrateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
   359  	props := model.StringInterfaceFromJson(r.Body)
   360  	from, ok := props["from"].(string)
   361  	if !ok {
   362  		c.SetInvalidParam("from")
   363  		return
   364  	}
   365  	to, ok := props["to"].(string)
   366  	if !ok {
   367  		c.SetInvalidParam("to")
   368  		return
   369  	}
   370  
   371  	auditRec := c.MakeAuditRecord("migrateConfig", audit.Fail)
   372  	auditRec.AddMeta("from", from)
   373  	auditRec.AddMeta("to", to)
   374  	defer c.LogAuditRec(auditRec)
   375  
   376  	if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PERMISSION_MANAGE_SYSTEM) {
   377  		c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
   378  		return
   379  	}
   380  
   381  	err := config.Migrate(from, to)
   382  	if err != nil {
   383  		c.Err = model.NewAppError("migrateConfig", "api.config.migrate_config.app_error", nil, err.Error(), http.StatusInternalServerError)
   384  		return
   385  	}
   386  
   387  	auditRec.Success()
   388  	ReturnStatusOK(w)
   389  }