github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/service/config.go (about) 1 // Copyright 2019 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package service 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io" 10 "os" 11 "path/filepath" 12 "strings" 13 "time" 14 15 "golang.org/x/net/context" 16 17 "github.com/keybase/client/go/engine" 18 "github.com/keybase/client/go/install" 19 "github.com/keybase/client/go/libkb" 20 keybase1 "github.com/keybase/client/go/protocol/keybase1" 21 "github.com/keybase/client/go/status" 22 "github.com/keybase/go-framed-msgpack-rpc/rpc" 23 jsonw "github.com/keybase/go-jsonw" 24 ) 25 26 type ConfigHandler struct { 27 libkb.Contextified 28 xp rpc.Transporter 29 svc *Service 30 connID libkb.ConnectionID 31 } 32 33 var _ keybase1.ConfigInterface = (*ConfigHandler)(nil) 34 35 func NewConfigHandler(xp rpc.Transporter, i libkb.ConnectionID, g *libkb.GlobalContext, svc *Service) *ConfigHandler { 36 return &ConfigHandler{ 37 Contextified: libkb.NewContextified(g), 38 xp: xp, 39 svc: svc, 40 connID: i, 41 } 42 } 43 44 func (h ConfigHandler) GetCurrentStatus(ctx context.Context, sessionID int) (res keybase1.CurrentStatus, err error) { 45 mctx := libkb.NewMetaContext(ctx, h.G()).WithLogTag("CFG") 46 return status.GetCurrentStatus(mctx) 47 } 48 49 func (h ConfigHandler) GuiGetValue(ctx context.Context, path string) (ret keybase1.ConfigValue, err error) { 50 return h.getValue(ctx, path, h.G().Env.GetGUIConfig()) 51 } 52 53 func (h ConfigHandler) GetValue(ctx context.Context, path string) (ret keybase1.ConfigValue, err error) { 54 return h.getValue(ctx, path, h.G().Env.GetConfig()) 55 } 56 57 func (h ConfigHandler) getValue(_ context.Context, path string, reader libkb.JSONReader) (ret keybase1.ConfigValue, err error) { 58 var i interface{} 59 i, err = reader.GetInterfaceAtPath(path) 60 if err != nil { 61 return ret, err 62 } 63 if i == nil { 64 ret.IsNull = true 65 } else { 66 switch v := i.(type) { 67 case int: 68 ret.I = &v 69 case string: 70 ret.S = &v 71 case bool: 72 ret.B = &v 73 case float64: 74 tmp := int(v) 75 ret.I = &tmp 76 default: 77 var b []byte 78 b, err = json.Marshal(v) 79 if err == nil { 80 tmp := string(b) 81 ret.O = &tmp 82 } 83 } 84 } 85 return ret, err 86 } 87 88 func (h ConfigHandler) GuiSetValue(ctx context.Context, arg keybase1.GuiSetValueArg) (err error) { 89 return h.setValue(ctx, keybase1.SetValueArg(arg), h.G().Env.GetGUIConfig()) 90 } 91 92 func (h ConfigHandler) SetValue(ctx context.Context, arg keybase1.SetValueArg) (err error) { 93 return h.setValue(ctx, arg, h.G().Env.GetConfigWriter()) 94 } 95 96 func (h ConfigHandler) setValue(_ context.Context, arg keybase1.SetValueArg, w libkb.JSONWriter) (err error) { 97 if arg.Path == "users" { 98 err = fmt.Errorf("The field 'users' cannot be edited for fear of config corruption") 99 return err 100 } 101 switch { 102 case arg.Value.IsNull: 103 err = w.SetNullAtPath(arg.Path) 104 case arg.Value.S != nil: 105 err = w.SetStringAtPath(arg.Path, *arg.Value.S) 106 case arg.Value.I != nil: 107 err = w.SetIntAtPath(arg.Path, *arg.Value.I) 108 case arg.Value.F != nil: 109 err = w.SetFloatAtPath(arg.Path, *arg.Value.F) 110 case arg.Value.B != nil: 111 err = w.SetBoolAtPath(arg.Path, *arg.Value.B) 112 case arg.Value.O != nil: 113 var jw *jsonw.Wrapper 114 jw, err = jsonw.Unmarshal([]byte(*arg.Value.O)) 115 if err == nil { 116 err = w.SetWrapperAtPath(arg.Path, jw) 117 } 118 default: 119 err = fmt.Errorf("Bad type for setting a value") 120 } 121 if err == nil { 122 reloadErr := h.G().ConfigReload() 123 if reloadErr != nil { 124 h.G().Log.Debug("setValue: error reloading: %+v", reloadErr) 125 } 126 } 127 return err 128 } 129 130 func (h ConfigHandler) GuiClearValue(ctx context.Context, path string) error { 131 return h.clearValue(ctx, path, h.G().Env.GetGUIConfig()) 132 } 133 134 func (h ConfigHandler) ClearValue(ctx context.Context, path string) error { 135 return h.clearValue(ctx, path, h.G().Env.GetConfigWriter()) 136 } 137 138 func (h ConfigHandler) clearValue(_ context.Context, path string, w libkb.JSONWriter) error { 139 w.DeleteAtPath(path) 140 return h.G().ConfigReload() 141 } 142 143 func (h ConfigHandler) GetClientStatus(ctx context.Context, sessionID int) (res []keybase1.ClientStatus, err error) { 144 mctx := libkb.NewMetaContext(ctx, h.G()).WithLogTag("CFG") 145 defer mctx.Trace("GetClientStatus", &err)() 146 return libkb.GetClientStatus(mctx), nil 147 } 148 149 func (h ConfigHandler) GetConfig(ctx context.Context, sessionID int) (res keybase1.Config, err error) { 150 mctx := libkb.NewMetaContext(ctx, h.G()).WithLogTag("CFG") 151 defer mctx.Trace("GetConfig", &err)() 152 forkType := keybase1.ForkType_NONE 153 if h.svc != nil { 154 forkType = h.svc.ForkType 155 } 156 return status.GetConfig(mctx, forkType) 157 } 158 159 func (h ConfigHandler) GetFullStatus(ctx context.Context, sessionID int) (res *keybase1.FullStatus, err error) { 160 mctx := libkb.NewMetaContext(ctx, h.G()).WithLogTag("CFG") 161 defer mctx.Trace("GetFullStatus", &err)() 162 return status.GetFullStatus(mctx) 163 } 164 165 func (h ConfigHandler) IsServiceRunning(ctx context.Context, sessionID int) (res bool, err error) { 166 mctx := libkb.NewMetaContext(ctx, h.G()).WithLogTag("CFG") 167 defer mctx.Trace("IsServiceRunning", &err)() 168 169 // set service status 170 if mctx.G().Env.GetStandalone() { 171 res = false 172 } else { 173 res = true 174 } 175 return 176 } 177 178 func (h ConfigHandler) IsKBFSRunning(ctx context.Context, sessionID int) (res bool, err error) { 179 mctx := libkb.NewMetaContext(ctx, h.G()).WithLogTag("CFG") 180 defer mctx.Trace("IsKBFSRunning", &err)() 181 182 clients := libkb.GetClientStatus(mctx) 183 184 kbfs := status.GetFirstClient(clients, keybase1.ClientType_KBFS) 185 186 return kbfs != nil, nil 187 } 188 189 func (h ConfigHandler) GetNetworkStats(ctx context.Context, arg keybase1.GetNetworkStatsArg) (res []keybase1.InstrumentationStat, err error) { 190 mctx := libkb.NewMetaContext(ctx, h.G()).WithLogTag("CFG") 191 defer mctx.Trace("GetNetworkStats", &err)() 192 switch arg.NetworkSrc { 193 case keybase1.NetworkSource_LOCAL: 194 return mctx.G().LocalNetworkInstrumenterStorage.Stats(ctx) 195 case keybase1.NetworkSource_REMOTE: 196 return mctx.G().RemoteNetworkInstrumenterStorage.Stats(ctx) 197 default: 198 return nil, fmt.Errorf("Unknown network source %d", arg.NetworkSrc) 199 } 200 } 201 202 func (h ConfigHandler) LogSend(ctx context.Context, arg keybase1.LogSendArg) (res keybase1.LogSendID, err error) { 203 mctx := libkb.NewMetaContext(ctx, h.G()).WithLogTag("CFG") 204 defer mctx.Trace("LogSend", &err)() 205 206 fstatus, err := status.GetFullStatus(mctx) 207 if err != nil { 208 return "", err 209 } 210 statusJSON := status.MergeStatusJSON(fstatus, "fstatus", arg.StatusJSON) 211 212 numBytes := status.LogSendDefaultBytesDesktop 213 if arg.SendMaxBytes { 214 numBytes = status.LogSendMaxBytes 215 } 216 217 // pass empty networkStatsJSON here since we call LogSend with addNetworkStats=true below 218 logSendContext := status.NewLogSendContext(h.G(), fstatus, statusJSON, "", arg.Feedback) 219 return logSendContext.LogSend(arg.SendLogs, numBytes, 220 false /* mergeExtendedStatus */, true /* addNetworkStats */) 221 } 222 223 func (h ConfigHandler) GetAllProvisionedUsernames(ctx context.Context, sessionID int) (res keybase1.AllProvisionedUsernames, err error) { 224 defaultUsername, all, err := libkb.GetAllProvisionedUsernames(libkb.NewMetaContext(ctx, h.G()).WithLogTag("CFG")) 225 if err != nil { 226 return res, err 227 } 228 229 // If the default is missing, fill it in from the first provisioned. 230 if defaultUsername.IsNil() && len(all) > 0 { 231 defaultUsername = all[0] 232 } 233 hasProvisionedUser := !defaultUsername.IsNil() 234 235 // Callers expect ProvisionedUsernames to contain the DefaultUsername, so 236 // we ensure it is here as a final sanity check before returning. 237 hasDefaultUsername := false 238 provisionedUsernames := []string{} 239 for _, username := range all { 240 provisionedUsernames = append(provisionedUsernames, username.String()) 241 hasDefaultUsername = hasDefaultUsername || username.Eq(defaultUsername) 242 } 243 244 if !hasDefaultUsername && hasProvisionedUser { 245 provisionedUsernames = append(provisionedUsernames, defaultUsername.String()) 246 } 247 248 return keybase1.AllProvisionedUsernames{ 249 DefaultUsername: defaultUsername.String(), 250 ProvisionedUsernames: provisionedUsernames, 251 HasProvisionedUser: hasProvisionedUser, 252 }, nil 253 } 254 255 func (h ConfigHandler) SetUserConfig(ctx context.Context, arg keybase1.SetUserConfigArg) (err error) { 256 eng := engine.NewUserConfigEngine(h.G(), &engine.UserConfigEngineArg{ 257 Key: arg.Key, 258 Value: arg.Value, 259 }) 260 m := libkb.NewMetaContext(ctx, h.G()) 261 err = engine.RunEngine2(m, eng) 262 if err != nil { 263 return err 264 } 265 return nil 266 } 267 268 func (h ConfigHandler) SetPath(_ context.Context, arg keybase1.SetPathArg) error { 269 h.G().Log.Debug("SetPath calling mergeIntoPath(%s)", arg.Path) 270 return mergeIntoPath(h.G(), arg.Path) 271 } 272 273 func mergeIntoPath(g *libkb.GlobalContext, p2 string) error { 274 275 svcPath := os.Getenv("PATH") 276 g.Log.Debug("mergeIntoPath: service path = %s", svcPath) 277 g.Log.Debug("mergeIntoPath: merge path = %s", p2) 278 279 pathenv := filepath.SplitList(svcPath) 280 pathset := make(map[string]bool) 281 for _, p := range pathenv { 282 pathset[p] = true 283 } 284 285 var clientAdditions []string 286 for _, dir := range filepath.SplitList(p2) { 287 if _, ok := pathset[dir]; ok { 288 continue 289 } 290 clientAdditions = append(clientAdditions, dir) 291 } 292 293 pathenv = append(pathenv, clientAdditions...) 294 combined := strings.Join(pathenv, string(os.PathListSeparator)) 295 296 if combined == svcPath { 297 g.Log.Debug("No path changes needed") 298 return nil 299 } 300 301 g.Log.Debug("mergeIntoPath: merged path = %s", combined) 302 os.Setenv("PATH", combined) 303 return nil 304 } 305 306 func (h ConfigHandler) HelloIAm(_ context.Context, arg keybase1.ClientDetails) error { 307 arg.Redact() 308 h.G().Log.Debug("HelloIAm: %d - %v", h.connID, arg) 309 return h.G().ConnectionManager.Label(h.connID, arg) 310 } 311 312 func (h ConfigHandler) CheckAPIServerOutOfDateWarning(_ context.Context) (keybase1.OutOfDateInfo, error) { 313 return h.G().GetOutOfDateInfo(), nil 314 } 315 316 func (h ConfigHandler) GetUpdateInfo(ctx context.Context) (res keybase1.UpdateInfo, err error) { 317 mctx := libkb.NewMetaContext(ctx, h.G()) 318 defer mctx.Trace("GetUpdateInfo", &err)() 319 outOfDateInfo := h.G().GetOutOfDateInfo() 320 if len(outOfDateInfo.UpgradeTo) != 0 { 321 // This is from the API server. Consider client critically out of date 322 // if we are asked to upgrade by the API server. 323 return keybase1.UpdateInfo{ 324 Status: keybase1.UpdateInfoStatus_CRITICALLY_OUT_OF_DATE, 325 Message: outOfDateInfo.CustomMessage, 326 }, nil 327 } 328 needUpdate, err := install.GetNeedUpdate() // This is from the updater. 329 if err != nil { 330 return keybase1.UpdateInfo{ 331 Status: keybase1.UpdateInfoStatus_UP_TO_DATE, 332 }, err 333 } 334 if needUpdate { 335 return keybase1.UpdateInfo{ 336 Status: keybase1.UpdateInfoStatus_NEED_UPDATE, 337 }, nil 338 } 339 return keybase1.UpdateInfo{ 340 Status: keybase1.UpdateInfoStatus_UP_TO_DATE, 341 }, nil 342 } 343 344 func (h ConfigHandler) StartUpdateIfNeeded(ctx context.Context) error { 345 return install.StartUpdateIfNeeded(ctx, h.G().Log) 346 } 347 348 func (h ConfigHandler) WaitForClient(_ context.Context, arg keybase1.WaitForClientArg) (bool, error) { 349 return h.G().ConnectionManager.WaitForClientType(arg.ClientType, arg.Timeout.Duration()), nil 350 } 351 352 func (h ConfigHandler) GetBootstrapStatus(ctx context.Context, sessionID int) (keybase1.BootstrapStatus, error) { 353 eng := engine.NewBootstrap(h.G()) 354 m := libkb.NewMetaContext(ctx, h.G()) 355 if err := engine.RunEngine2(m, eng); err != nil { 356 return keybase1.BootstrapStatus{}, err 357 } 358 status := eng.Status() 359 h.G().Log.CDebugf(ctx, "GetBootstrapStatus: attempting to get HTTP server address") 360 for i := 0; i < 40; i++ { // wait at most 2 seconds 361 addr, err := h.svc.httpSrv.Addr() 362 if err != nil { 363 h.G().Log.CDebugf(ctx, "GetBootstrapStatus: failed to get HTTP server address: %s", err) 364 } else { 365 h.G().Log.CDebugf(ctx, "GetBootstrapStatus: http server: addr: %s token: %s", addr, 366 h.svc.httpSrv.Token()) 367 status.HttpSrvInfo = &keybase1.HttpSrvInfo{ 368 Address: addr, 369 Token: h.svc.httpSrv.Token(), 370 } 371 break 372 } 373 time.Sleep(50 * time.Millisecond) 374 } 375 if status.HttpSrvInfo == nil { 376 h.G().Log.CDebugf(ctx, "GetBootstrapStatus: failed to get HTTP srv info after max attempts") 377 } 378 return status, nil 379 } 380 381 func (h ConfigHandler) RequestFollowingAndUnverifiedFollowers(ctx context.Context, sessionID int) error { 382 if err := assertLoggedIn(ctx, h.G()); err != nil { 383 return err 384 } 385 // Queue up a load for follower info 386 return h.svc.trackerLoader.Queue(ctx, h.G().ActiveDevice.UID()) 387 } 388 389 func (h ConfigHandler) GetRememberPassphrase(ctx context.Context, sessionID int) (bool, error) { 390 username := h.G().Env.GetUsername() 391 if username.IsNil() { 392 h.G().Log.CDebugf(ctx, "GetRememberPassphrase: got nil username; using legacy remember_passphrase setting") 393 } 394 return h.G().Env.GetRememberPassphrase(username), nil 395 } 396 397 func (h ConfigHandler) SetRememberPassphrase(ctx context.Context, arg keybase1.SetRememberPassphraseArg) error { 398 m := libkb.NewMetaContext(ctx, h.G()) 399 400 username := m.G().Env.GetUsername() 401 if username.IsNil() { 402 m.Debug("SetRememberPassphrase: got nil username; using legacy remember_passphrase setting") 403 } 404 remember, err := h.GetRememberPassphrase(ctx, arg.SessionID) 405 if err != nil { 406 return err 407 } 408 if remember == arg.Remember { 409 m.Debug("SetRememberPassphrase: no change necessary (remember = %v)", remember) 410 return nil 411 } 412 413 // set the config variable 414 w := h.G().Env.GetConfigWriter() 415 if err := w.SetRememberPassphrase(username, arg.Remember); err != nil { 416 return err 417 } 418 err = h.G().ConfigReload() 419 if err != nil { 420 return err 421 } 422 423 if err := h.G().ReplaceSecretStore(ctx); err != nil { 424 m.Debug("error replacing secret store for SetRememberPassphrase(%v): %s", arg.Remember, err) 425 return err 426 } 427 428 m.Debug("SetRememberPassphrase(%s, %v) success", username.String(), arg.Remember) 429 430 return nil 431 } 432 433 type rawGetPkgCheck struct { 434 Status libkb.AppStatus `json:"status"` 435 Res keybase1.UpdateInfo2 `json:"res"` 436 } 437 438 func (r *rawGetPkgCheck) GetAppStatus() *libkb.AppStatus { 439 return &r.Status 440 } 441 442 func (h ConfigHandler) GetUpdateInfo2(ctx context.Context, arg keybase1.GetUpdateInfo2Arg) (res keybase1.UpdateInfo2, err error) { 443 m := libkb.NewMetaContext(ctx, h.G()) 444 445 var version string 446 var platform string 447 448 if arg.Platform != nil { 449 platform = *arg.Platform 450 } else { 451 platform = libkb.GetPlatformString() 452 } 453 if arg.Version != nil { 454 version = *arg.Version 455 } else { 456 version = libkb.VersionString() 457 } 458 459 apiArg := libkb.APIArg{ 460 Endpoint: "pkg/check", 461 Args: libkb.HTTPArgs{ 462 "version": libkb.S{Val: version}, 463 "platform": libkb.S{Val: platform}, 464 }, 465 RetryCount: 3, 466 } 467 var raw rawGetPkgCheck 468 if err = m.G().API.GetDecode(m, apiArg, &raw); err != nil { 469 return res, err 470 } 471 return raw.Res, nil 472 } 473 474 func (h ConfigHandler) GetProxyData(ctx context.Context) (keybase1.ProxyData, error) { 475 config := h.G().Env.GetConfig() 476 proxyAddress := config.GetProxy() 477 proxyType := libkb.ProxyTypeStrToEnumFunc(config.GetProxyType()) 478 certPinning := config.IsCertPinningEnabled() 479 480 var convertedProxyType keybase1.ProxyType 481 if proxyType == libkb.NoProxy { 482 convertedProxyType = keybase1.ProxyType_No_Proxy 483 } else if proxyType == libkb.HTTPConnect { 484 convertedProxyType = keybase1.ProxyType_HTTP_Connect 485 } else if proxyType == libkb.Socks { 486 convertedProxyType = keybase1.ProxyType_Socks 487 } else { 488 return keybase1.ProxyData{AddressWithPort: "", ProxyType: keybase1.ProxyType_No_Proxy, CertPinning: true}, 489 fmt.Errorf("Failed to convert proxy type into a protocol compatible proxy type!") 490 } 491 492 return keybase1.ProxyData{AddressWithPort: proxyAddress, ProxyType: convertedProxyType, CertPinning: certPinning}, nil 493 } 494 495 func (h ConfigHandler) SetProxyData(ctx context.Context, arg keybase1.ProxyData) error { 496 configWriter := h.G().Env.GetConfigWriter() 497 498 rpcProxyType := arg.ProxyType 499 500 var convertedProxyType libkb.ProxyType 501 if rpcProxyType == keybase1.ProxyType_No_Proxy { 502 convertedProxyType = libkb.NoProxy 503 } else if rpcProxyType == keybase1.ProxyType_HTTP_Connect { 504 convertedProxyType = libkb.HTTPConnect 505 } else if rpcProxyType == keybase1.ProxyType_Socks { 506 convertedProxyType = libkb.Socks 507 } else { 508 // Got a bogus proxy type that we couldn't convert to a libkb enum so return an error 509 return fmt.Errorf("failed to convert given proxy type to a native libkb proxy type") 510 } 511 512 proxyTypeStr, ok := libkb.ProxyTypeEnumToStr[convertedProxyType] 513 514 if !ok { 515 // Got a bogus proxy type that we couldn't convert to a string 516 return fmt.Errorf("failed to convert proxy type into a string") 517 } 518 519 err := configWriter.SetStringAtPath("proxy", arg.AddressWithPort) 520 if err != nil { 521 return err 522 } 523 err = configWriter.SetBoolAtPath("disable-cert-pinning", !arg.CertPinning) 524 if err != nil { 525 return err 526 } 527 err = configWriter.SetStringAtPath("proxy-type", proxyTypeStr) 528 if err != nil { 529 return err 530 } 531 532 // Reload the config file in order to actually start using the proxy 533 err = h.G().ConfigReload() 534 if err != nil { 535 return err 536 } 537 538 return nil 539 } 540 541 func (h ConfigHandler) ToggleRuntimeStats(ctx context.Context) error { 542 configWriter := h.G().Env.GetConfigWriter() 543 curValue := h.G().Env.GetRuntimeStatsEnabled() 544 err := configWriter.SetBoolAtPath("runtime_stats_enabled", !curValue) 545 if err != nil { 546 return err 547 } 548 if err := h.G().ConfigReload(); err != nil { 549 return err 550 } 551 if curValue { 552 <-h.G().RuntimeStats.Stop(ctx) 553 } else { 554 h.G().RuntimeStats.Start(ctx) 555 } 556 return nil 557 } 558 559 func (h ConfigHandler) AppendGUILogs(ctx context.Context, content string) error { 560 wr := h.G().GetGUILogWriter() 561 _, err := io.WriteString(wr, content) 562 return err 563 } 564 565 func (h ConfigHandler) GenerateWebAuthToken(ctx context.Context) (ret string, err error) { 566 if err := assertLoggedIn(ctx, h.G()); err != nil { 567 return ret, err 568 } 569 570 nist, err := h.G().ActiveDevice.NISTWebAuthToken(ctx) 571 if err != nil { 572 return ret, err 573 } 574 if nist == nil { 575 return ret, fmt.Errorf("cannot generate a token when you are logged off") 576 } 577 uri := libkb.SiteURILookup[h.G().Env.GetRunMode()] + "/_/login/nist?tok=" + nist.Token().String() 578 return uri, nil 579 } 580 581 func (h ConfigHandler) UpdateLastLoggedInAndServerConfig( 582 ctx context.Context, serverConfigPath string) error { 583 arg := libkb.APIArg{ 584 Endpoint: "user/features", 585 SessionType: libkb.APISessionTypeREQUIRED, 586 } 587 mctx := libkb.NewMetaContext(ctx, h.G()) 588 resp, err := h.G().API.Get(mctx, arg) 589 if err != nil { 590 return err 591 } 592 jw := resp.Body 593 isAdmin, err := jw.AtPath("features.admin.value").GetBool() 594 if err != nil { 595 return err 596 } 597 598 // Try to read from the old config file. But ignore any error and just 599 // create a new one. 600 oldBytes, err := os.ReadFile(serverConfigPath) 601 if err != nil { 602 jw = jsonw.NewDictionary() 603 } else if jw, err = jsonw.Unmarshal(oldBytes); err != nil { 604 jw = jsonw.NewDictionary() 605 } 606 username := h.G().GetEnv().GetUsername().String() 607 if err = jw.SetValueAtPath(fmt.Sprintf("%s.chatIndexProfilingEnabled", username), jsonw.NewBool(isAdmin)); err != nil { 608 return err 609 } 610 if err = jw.SetValueAtPath(fmt.Sprintf("%s.dbCleanEnabled", username), jsonw.NewBool(isAdmin)); err != nil { 611 return err 612 } 613 if err = jw.SetValueAtPath(fmt.Sprintf("%s.printRPCStaus", username), jsonw.NewBool(isAdmin)); err != nil { 614 return err 615 } 616 if err = jw.SetKey("lastLoggedInUser", jsonw.NewString(username)); err != nil { 617 return err 618 } 619 newBytes, err := jw.Marshal() 620 if err != nil { 621 return err 622 } 623 return libkb.NewFile(serverConfigPath, newBytes, 0644).Save(h.G().Log) 624 }