package keymgr import ( "fmt" "os" "os/exec" "path/filepath" "ssh-access-tui/internal/model" ) // ConfigOnlyResult describes a lightweight config-refresh archive: no // keypair is generated or touched. type ConfigOnlyResult struct { Username string ArchivePath string } // BuildConfigOnlyArchive packages just a fresh client config + instructions // (no key files at all) reflecting the user's CURRENT allowed forwards. // // This exists for the common case where only a user's group membership // changed: server-side access is already updated immediately (see // sysadm.WriteAuthorizedKeys / resyncGroupUsers, called the moment groups // change), and their existing private key is still perfectly valid — only // the local ssh config listing which services are reachable is now stale. // Reissuing a whole new keypair for that would be needless churn (and // needless key material handed around). The config's IdentityFile line // references the same deterministic filename GeneratePair always used // (username + "_jump_key"), so it matches the key the user already has. func BuildConfigOnlyArchive(scratchDir, outDir, jumpHost string, jumpPort int, username, password string, forwards []model.Forward, proxyJump bool) (*ConfigOnlyResult, error) { userScratch := filepath.Join(scratchDir, username+"_configonly") if err := os.MkdirAll(userScratch, 0o700); err != nil { return nil, fmt.Errorf("create scratch dir: %w", err) } defer Wipe(userScratch) keyFileName := username + "_jump_key" var cfg, instr string if proxyJump { cfg = RenderClientConfigProxyJump(jumpHost, jumpPort, username, keyFileName, forwards) instr = RenderConfigUpdateInstructionsProxyJump(username, forwards) } else { allocated := AllocateLocalPorts(forwards) cfg = RenderClientConfig(jumpHost, jumpPort, username, keyFileName, allocated) instr = RenderConfigUpdateInstructions(username, allocated) } if err := os.WriteFile(filepath.Join(userScratch, "config"), []byte(cfg), 0o600); err != nil { return nil, fmt.Errorf("write config: %w", err) } if err := os.WriteFile(filepath.Join(userScratch, "INSTRUCTIONS.txt"), []byte(instr), 0o600); err != nil { return nil, fmt.Errorf("write instructions: %w", err) } if err := os.MkdirAll(outDir, 0o750); err != nil { return nil, fmt.Errorf("create output dir: %w", err) } archivePath := filepath.Join(outDir, username+"_config_update.7z") _ = os.Remove(archivePath) cmd := exec.Command("7z", "a", "-p"+password, "-mhe=on", "-mx=9", archivePath, filepath.Join(userScratch, "config"), filepath.Join(userScratch, "INSTRUCTIONS.txt"), ) if out, err := cmd.CombinedOutput(); err != nil { return nil, fmt.Errorf("7z: %w: %s", err, out) } return &ConfigOnlyResult{Username: username, ArchivePath: archivePath}, nil }