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 }