C · 28321 bytes Raw Blame History
1 /* Configuration file management with comprehensive security validation
2 * Implements secure TOML-based configuration for gitswitch-c
3 */
4
5 #include <stdio.h>
6 #include <stdlib.h>
7 #include <string.h>
8 #include <sys/stat.h>
9 #include <time.h>
10 #include <unistd.h>
11
12 #include "accounts.h"
13 #include "config.h"
14 #include "toml_parser.h"
15 #include "error.h"
16 #include "utils.h"
17
18 /* Default configuration template with security-focused defaults */
19 const char *default_config_template =
20 "# gitswitch-c Configuration File\n"
21 "# This file contains sensitive information - ensure proper permissions (600)\n"
22 "\n"
23 "[settings]\n"
24 "# Default scope for git configuration changes\n"
25 "# Options: \"local\" (repository-specific) or \"global\" (user-wide)\n"
26 "default_scope = \"local\"\n"
27 "\n"
28 "# Example account configuration\n"
29 "# Uncomment and modify for your accounts\n"
30 "\n"
31 "#[accounts.1]\n"
32 "#name = \"Your Name\"\n"
33 "#email = \"your.email@example.com\"\n"
34 "#description = \"Personal Account\"\n"
35 "#preferred_scope = \"local\"\n"
36 "#ssh_key = \"~/.ssh/id_ed25519_personal\"\n"
37 "#gpg_key = \"1234567890ABCDEF\"\n"
38 "#gpg_signing_enabled = true\n"
39 "\n"
40 "#[accounts.2]\n"
41 "#name = \"Your Name\"\n"
42 "#email = \"work@company.com\"\n"
43 "#description = \"Work Account\"\n"
44 "#preferred_scope = \"global\"\n"
45 "#ssh_key = \"~/.ssh/id_rsa_work\"\n"
46 "#gpg_key = \"ABCDEF1234567890\"\n"
47 "#gpg_signing_enabled = true\n"
48 "#ssh_host = \"github.com-work\"\n"
49 "\n"
50 "# Security Notes:\n"
51 "# - SSH keys should have 600 permissions\n"
52 "# - GPG keys should exist in your keyring\n"
53 "# - This config file should have 600 permissions\n"
54 "# - Use absolute paths or ~ expansion for key files\n";
55
56 /* Internal helper functions */
57 static int validate_config_file_security(const char *config_path);
58 static int create_config_directory_secure(const char *config_dir);
59 static int load_accounts_from_toml(gitswitch_ctx_t *ctx, const toml_document_t *doc);
60 static int save_accounts_to_toml(const gitswitch_ctx_t *ctx, toml_document_t *doc);
61 static int parse_account_id_from_section(const char *section_name, uint32_t *account_id);
62 static int validate_account_security(const account_t *account);
63
64 /* Initialize configuration system */
65 int config_init(gitswitch_ctx_t *ctx) {
66 char config_path[MAX_PATH_LEN];
67 char config_dir[MAX_PATH_LEN];
68
69 if (!ctx) {
70 set_error(ERR_INVALID_ARGS, "NULL context to config_init");
71 return -1;
72 }
73
74 /* Initialize context */
75 memset(ctx, 0, sizeof(gitswitch_ctx_t));
76 ctx->config.default_scope = GIT_SCOPE_LOCAL;
77 ctx->config.verbose = false;
78 ctx->config.dry_run = false;
79 ctx->config.color_output = true;
80
81 /* Get configuration directory path */
82 if (get_config_directory(config_dir, sizeof(config_dir)) != 0) {
83 return -1;
84 }
85
86 /* Ensure config directory exists with secure permissions */
87 if (create_config_directory_secure(config_dir) != 0) {
88 return -1;
89 }
90
91 /* Build config file path */
92 if (join_path(config_path, sizeof(config_path), config_dir, DEFAULT_CONFIG_FILE) != 0) {
93 return -1;
94 }
95
96 /* Store config path in context */
97 safe_strncpy(ctx->config.config_path, config_path, sizeof(ctx->config.config_path));
98
99 /* Load configuration if it exists */
100 if (path_exists(config_path)) {
101 log_info("Loading configuration from: %s", config_path);
102 return config_load(ctx, config_path);
103 } else {
104 log_info("Configuration file not found, will create default");
105 /* Don't automatically create - let user create when needed */
106 return 0;
107 }
108 }
109
110 /* Load configuration from TOML file */
111 int config_load(gitswitch_ctx_t *ctx, const char *config_path) {
112 toml_document_t toml_doc;
113 char scope_str[32];
114
115 if (!ctx || !config_path) {
116 set_error(ERR_INVALID_ARGS, "Invalid arguments to config_load");
117 return -1;
118 }
119
120 /* Validate file security before loading */
121 if (validate_config_file_security(config_path) != 0) {
122 return -1;
123 }
124
125 /* Parse TOML configuration */
126 toml_init_document(&toml_doc);
127 if (toml_parse_file(config_path, &toml_doc) != 0) {
128 toml_cleanup_document(&toml_doc);
129 return -1;
130 }
131
132 /* Load settings section */
133 if (toml_get_string(&toml_doc, "settings", "default_scope",
134 scope_str, sizeof(scope_str)) == 0) {
135 ctx->config.default_scope = config_parse_scope(scope_str);
136 } else {
137 log_warning("No default_scope found in settings, using local");
138 ctx->config.default_scope = GIT_SCOPE_LOCAL;
139 }
140
141 /* Load accounts */
142 if (load_accounts_from_toml(ctx, &toml_doc) != 0) {
143 toml_cleanup_document(&toml_doc);
144 return -1;
145 }
146
147 /* Store config path */
148 safe_strncpy(ctx->config.config_path, config_path, sizeof(ctx->config.config_path));
149
150 toml_cleanup_document(&toml_doc);
151
152 /* Detect current account from SSH socket symlink */
153 accounts_detect_current(ctx);
154
155 log_info("Configuration loaded successfully: %zu accounts", ctx->account_count);
156 return 0;
157 }
158
159 /* Save configuration to TOML file */
160 int config_save(const gitswitch_ctx_t *ctx, const char *config_path) {
161 toml_document_t toml_doc;
162 char temp_path[MAX_PATH_LEN];
163 int result = -1;
164
165 if (!ctx || !config_path) {
166 set_error(ERR_INVALID_ARGS, "Invalid arguments to config_save");
167 return -1;
168 }
169
170 /* Create backup if file exists */
171 if (path_exists(config_path)) {
172 if (config_backup(config_path) != 0) {
173 log_warning("Failed to create backup before saving config");
174 }
175 }
176
177 /* Create temporary file path for atomic write */
178 if ((size_t)snprintf(temp_path, sizeof(temp_path), "%s.tmp", config_path) >= sizeof(temp_path)) {
179 set_error(ERR_INVALID_ARGS, "Temporary file path too long");
180 return -1;
181 }
182
183 /* Initialize TOML document */
184 toml_init_document(&toml_doc);
185
186 /* Add/update settings section */
187 if (toml_set_string(&toml_doc, "settings", "default_scope",
188 config_scope_to_string(ctx->config.default_scope)) != 0) {
189 goto cleanup;
190 }
191
192 /* Add current accounts */
193 log_debug("About to save accounts to TOML doc with %zu sections", toml_doc.section_count);
194 if (save_accounts_to_toml(ctx, &toml_doc) != 0) {
195 goto cleanup;
196 }
197 log_debug("After saving accounts, TOML doc has %zu sections", toml_doc.section_count);
198
199 /* Write to temporary file first */
200 if (toml_write_file(&toml_doc, temp_path) != 0) {
201 goto cleanup;
202 }
203
204 /* Set secure permissions on temp file */
205 if (set_file_permissions(temp_path, PERM_USER_RW) != 0) {
206 unlink(temp_path);
207 goto cleanup;
208 }
209
210 /* Atomic move from temp to final location */
211 if (rename(temp_path, config_path) != 0) {
212 set_system_error(ERR_CONFIG_WRITE_FAILED,
213 "Failed to move temporary config file to final location");
214 unlink(temp_path);
215 goto cleanup;
216 }
217
218 log_info("Configuration saved successfully to: %s", config_path);
219 result = 0;
220
221 cleanup:
222 toml_cleanup_document(&toml_doc);
223 return result;
224 }
225
226 /* Create default configuration file */
227 int config_create_default(const char *config_path) {
228 FILE *file;
229 char config_dir[MAX_PATH_LEN];
230 char *last_slash;
231
232 if (!config_path) {
233 set_error(ERR_INVALID_ARGS, "NULL config path to config_create_default");
234 return -1;
235 }
236
237 /* Extract directory from config path */
238 safe_strncpy(config_dir, config_path, sizeof(config_dir));
239 last_slash = strrchr(config_dir, '/');
240 if (last_slash) {
241 *last_slash = '\0';
242 }
243
244 /* Ensure directory exists */
245 if (create_config_directory_secure(config_dir) != 0) {
246 return -1;
247 }
248
249 /* Create file with secure permissions */
250 file = fopen(config_path, "w");
251 if (!file) {
252 set_system_error(ERR_CONFIG_WRITE_FAILED, "Failed to create config file: %s", config_path);
253 return -1;
254 }
255
256 /* Write default template */
257 if (fwrite(default_config_template, 1, strlen(default_config_template), file) !=
258 strlen(default_config_template)) {
259 set_system_error(ERR_CONFIG_WRITE_FAILED, "Failed to write default config content");
260 fclose(file);
261 return -1;
262 }
263
264 fclose(file);
265
266 /* Set secure permissions */
267 if (set_file_permissions(config_path, PERM_USER_RW) != 0) {
268 return -1;
269 }
270
271 log_info("Created default configuration file: %s", config_path);
272 return 0;
273 }
274
275 /* Validate configuration structure */
276 int config_validate(const gitswitch_ctx_t *ctx) {
277 if (!ctx) {
278 set_error(ERR_INVALID_ARGS, "NULL context to config_validate");
279 return -1;
280 }
281
282 /* Validate configuration file security */
283 if (path_exists(ctx->config.config_path)) {
284 if (validate_config_file_security(ctx->config.config_path) != 0) {
285 return -1;
286 }
287 }
288
289 /* Validate each account */
290 for (size_t i = 0; i < ctx->account_count; i++) {
291 if (validate_account_security(&ctx->accounts[i]) != 0) {
292 set_error(ERR_ACCOUNT_INVALID, "Account %u failed security validation",
293 ctx->accounts[i].id);
294 return -1;
295 }
296 }
297
298 log_debug("Configuration validation passed for %zu accounts", ctx->account_count);
299 return 0;
300 }
301
302 /* Get configuration file path */
303 int config_get_path(char *path_buffer, size_t buffer_size) {
304 char config_dir[MAX_PATH_LEN];
305
306 if (!path_buffer || buffer_size == 0) {
307 set_error(ERR_INVALID_ARGS, "Invalid arguments to config_get_path");
308 return -1;
309 }
310
311 /* Get config directory */
312 if (get_config_directory(config_dir, sizeof(config_dir)) != 0) {
313 return -1;
314 }
315
316 /* Build full path */
317 return join_path(path_buffer, buffer_size, config_dir, DEFAULT_CONFIG_FILE);
318 }
319
320 /* Add new account to configuration */
321 int config_add_account(gitswitch_ctx_t *ctx, const account_t *account) {
322 if (!ctx || !account) {
323 set_error(ERR_INVALID_ARGS, "Invalid arguments to config_add_account");
324 return -1;
325 }
326
327 if (ctx->account_count >= MAX_ACCOUNTS) {
328 set_error(ERR_ACCOUNT_EXISTS, "Maximum number of accounts reached: %d", MAX_ACCOUNTS);
329 return -1;
330 }
331
332 /* Validate account security */
333 if (validate_account_security(account) != 0) {
334 return -1;
335 }
336
337 /* Check for duplicate IDs */
338 for (size_t i = 0; i < ctx->account_count; i++) {
339 if (ctx->accounts[i].id == account->id) {
340 set_error(ERR_ACCOUNT_EXISTS, "Account with ID %u already exists", account->id);
341 return -1;
342 }
343 }
344
345 /* Add account */
346 ctx->accounts[ctx->account_count] = *account;
347 ctx->account_count++;
348
349 log_info("Added account: %s (%s)", account->name, account->description);
350 return 0;
351 }
352
353 /* Remove account from configuration */
354 int config_remove_account(gitswitch_ctx_t *ctx, uint32_t account_id) {
355 size_t found_index = SIZE_MAX;
356
357 if (!ctx) {
358 set_error(ERR_INVALID_ARGS, "NULL context to config_remove_account");
359 return -1;
360 }
361
362 /* Find account */
363 for (size_t i = 0; i < ctx->account_count; i++) {
364 if (ctx->accounts[i].id == account_id) {
365 found_index = i;
366 break;
367 }
368 }
369
370 if (found_index == SIZE_MAX) {
371 set_error(ERR_ACCOUNT_NOT_FOUND, "Account with ID %u not found", account_id);
372 return -1;
373 }
374
375 /* Clear sensitive data before removing */
376 secure_zero_memory(&ctx->accounts[found_index], sizeof(account_t));
377
378 /* Shift remaining accounts */
379 for (size_t i = found_index; i < ctx->account_count - 1; i++) {
380 ctx->accounts[i] = ctx->accounts[i + 1];
381 }
382
383 ctx->account_count--;
384
385 /* Clear the last slot */
386 memset(&ctx->accounts[ctx->account_count], 0, sizeof(account_t));
387
388 log_info("Removed account with ID: %u", account_id);
389 return 0;
390 }
391
392 /* Update existing account */
393 int config_update_account(gitswitch_ctx_t *ctx, const account_t *account) {
394 account_t *existing_account = NULL;
395
396 if (!ctx || !account) {
397 set_error(ERR_INVALID_ARGS, "Invalid arguments to config_update_account");
398 return -1;
399 }
400
401 /* Find existing account */
402 for (size_t i = 0; i < ctx->account_count; i++) {
403 if (ctx->accounts[i].id == account->id) {
404 existing_account = &ctx->accounts[i];
405 break;
406 }
407 }
408
409 if (!existing_account) {
410 set_error(ERR_ACCOUNT_NOT_FOUND, "Account with ID %u not found", account->id);
411 return -1;
412 }
413
414 /* Validate new account data */
415 if (validate_account_security(account) != 0) {
416 return -1;
417 }
418
419 /* Clear old sensitive data */
420 secure_zero_memory(existing_account, sizeof(account_t));
421
422 /* Update with new data */
423 *existing_account = *account;
424
425 log_info("Updated account: %s (%s)", account->name, account->description);
426 return 0;
427 }
428
429 /* Find account by ID or name/description */
430 account_t *config_find_account(gitswitch_ctx_t *ctx, const char *identifier) {
431 char *endptr;
432 unsigned long account_id;
433
434 if (!ctx || !identifier) {
435 set_error(ERR_INVALID_ARGS, "Invalid arguments to config_find_account");
436 return NULL;
437 }
438
439 /* Try to parse as numeric ID */
440 account_id = strtoul(identifier, &endptr, 10);
441 if (*endptr == '\0') {
442 /* It's a number - search by ID */
443 for (size_t i = 0; i < ctx->account_count; i++) {
444 if (ctx->accounts[i].id == (uint32_t)account_id) {
445 return &ctx->accounts[i];
446 }
447 }
448 } else {
449 /* Search by name, email, or description */
450 for (size_t i = 0; i < ctx->account_count; i++) {
451 if (strstr(ctx->accounts[i].name, identifier) ||
452 strstr(ctx->accounts[i].description, identifier) ||
453 strcmp(ctx->accounts[i].email, identifier) == 0) {
454 return &ctx->accounts[i];
455 }
456 }
457 }
458
459 return NULL;
460 }
461
462 /* Parse git scope from string */
463 git_scope_t config_parse_scope(const char *scope_str) {
464 if (!scope_str) return GIT_SCOPE_LOCAL;
465
466 if (strcmp(scope_str, "global") == 0) {
467 return GIT_SCOPE_GLOBAL;
468 } else if (strcmp(scope_str, "system") == 0) {
469 return GIT_SCOPE_SYSTEM;
470 } else {
471 return GIT_SCOPE_LOCAL;
472 }
473 }
474
475 /* Convert git scope to string */
476 const char *config_scope_to_string(git_scope_t scope) {
477 switch (scope) {
478 case GIT_SCOPE_GLOBAL: return "global";
479 case GIT_SCOPE_SYSTEM: return "system";
480 case GIT_SCOPE_LOCAL:
481 default:
482 return "local";
483 }
484 }
485
486 /* Backup configuration file with timestamp */
487 int config_backup(const char *config_path) {
488 char backup_path[MAX_PATH_LEN];
489 char timestamp[32];
490 time_t now;
491 struct tm *tm_info;
492
493 if (!config_path) {
494 set_error(ERR_INVALID_ARGS, "NULL config path to config_backup");
495 return -1;
496 }
497
498 if (!path_exists(config_path)) {
499 log_debug("Config file does not exist, no backup needed");
500 return 0;
501 }
502
503 /* Generate timestamp */
504 time(&now);
505 tm_info = localtime(&now);
506 if (tm_info) {
507 strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm_info);
508 } else {
509 snprintf(timestamp, sizeof(timestamp), "%ld", (long)now);
510 }
511
512 /* Create backup path */
513 if ((size_t)snprintf(backup_path, sizeof(backup_path), "%s.backup.%s",
514 config_path, timestamp) >= sizeof(backup_path)) {
515 set_error(ERR_INVALID_ARGS, "Backup path too long");
516 return -1;
517 }
518
519 /* Copy file */
520 if (copy_file(config_path, backup_path) != 0) {
521 return -1;
522 }
523
524 /* Set secure permissions on backup */
525 if (set_file_permissions(backup_path, PERM_USER_RW) != 0) {
526 return -1;
527 }
528
529 log_info("Created configuration backup: %s", backup_path);
530 return 0;
531 }
532
533 /* Internal helper functions implementation */
534
535 /* Validate configuration file security */
536 static int validate_config_file_security(const char *config_path) {
537 struct stat file_stat;
538
539 if (stat(config_path, &file_stat) != 0) {
540 set_system_error(ERR_CONFIG_NOT_FOUND, "Cannot access config file: %s", config_path);
541 return -1;
542 }
543
544 /* Check file permissions - must not be readable by group/others */
545 if (file_stat.st_mode & (S_IRGRP | S_IROTH | S_IWGRP | S_IWOTH)) {
546 set_error(ERR_PERMISSION_DENIED,
547 "Configuration file has unsafe permissions: %o (should be 600)",
548 file_stat.st_mode & 0777);
549 return -1;
550 }
551
552 /* Check ownership - must be owned by current user */
553 if (file_stat.st_uid != getuid()) {
554 set_error(ERR_PERMISSION_DENIED, "Configuration file not owned by current user");
555 return -1;
556 }
557
558 /* Check file size is reasonable */
559 if (file_stat.st_size > TOML_MAX_FILE_SIZE) {
560 set_error(ERR_CONFIG_INVALID, "Configuration file too large: %ld bytes", file_stat.st_size);
561 return -1;
562 }
563
564 return 0;
565 }
566
567 /* Create config directory with secure permissions */
568 static int create_config_directory_secure(const char *config_dir) {
569 if (!path_exists(config_dir)) {
570 if (create_directory_recursive(config_dir, PERM_USER_RWX) != 0) {
571 return -1;
572 }
573 log_info("Created configuration directory: %s", config_dir);
574 }
575
576 /* Verify directory permissions */
577 mode_t dir_mode;
578 if (get_file_permissions(config_dir, &dir_mode) == 0) {
579 if ((dir_mode & 077) != 0) {
580 /* Directory has group/other permissions - fix it */
581 if (set_file_permissions(config_dir, PERM_USER_RWX) != 0) {
582 return -1;
583 }
584 log_warning("Fixed configuration directory permissions");
585 }
586 }
587
588 return 0;
589 }
590
591 /* Load accounts from TOML document */
592 static int load_accounts_from_toml(gitswitch_ctx_t *ctx, const toml_document_t *doc) {
593 char sections[TOML_MAX_SECTIONS][TOML_MAX_SECTION_LEN];
594 size_t section_count;
595
596 if (toml_get_sections(doc, sections, TOML_MAX_SECTIONS, &section_count) != 0) {
597 set_error(ERR_CONFIG_INVALID, "Failed to get sections from TOML document");
598 return -1;
599 }
600
601 ctx->account_count = 0;
602
603 for (size_t i = 0; i < section_count; i++) {
604 if (string_starts_with(sections[i], "accounts.")) {
605 account_t account;
606 uint32_t account_id;
607 char temp_str[256];
608 bool temp_bool;
609
610 /* Parse account ID from section name */
611 if (parse_account_id_from_section(sections[i], &account_id) != 0) {
612 log_warning("Invalid account section name: %s", sections[i]);
613 continue;
614 }
615
616 /* Initialize account */
617 memset(&account, 0, sizeof(account));
618 account.id = account_id;
619 account.preferred_scope = GIT_SCOPE_LOCAL; /* Default */
620
621 /* Load required fields */
622 if (toml_get_string(doc, sections[i], "name", account.name, sizeof(account.name)) != 0) {
623 log_error("Account %u missing required 'name' field", account_id);
624 continue;
625 }
626
627 if (toml_get_string(doc, sections[i], "email", account.email, sizeof(account.email)) != 0) {
628 log_error("Account %u missing required 'email' field", account_id);
629 continue;
630 }
631
632 /* Load optional fields - clear errors after each since missing optional fields are not errors */
633 if (toml_get_string(doc, sections[i], "description",
634 account.description, sizeof(account.description)) != 0) {
635 /* Use name as description if not provided */
636 safe_strncpy(account.description, account.name, sizeof(account.description));
637 clear_error();
638 }
639
640 if (toml_get_string(doc, sections[i], "preferred_scope", temp_str, sizeof(temp_str)) == 0) {
641 account.preferred_scope = config_parse_scope(temp_str);
642 } else {
643 clear_error();
644 }
645
646 /* SSH configuration */
647 if (toml_get_string(doc, sections[i], "ssh_key",
648 account.ssh_key_path, sizeof(account.ssh_key_path)) == 0 &&
649 strlen(account.ssh_key_path) > 0) {
650 account.ssh_enabled = true;
651
652 /* Expand path if needed */
653 char expanded_path[MAX_PATH_LEN];
654 if (expand_path(account.ssh_key_path, expanded_path, sizeof(expanded_path)) == 0) {
655 safe_strncpy(account.ssh_key_path, expanded_path, sizeof(account.ssh_key_path));
656 }
657
658 /* Optional SSH host alias */
659 if (toml_get_string(doc, sections[i], "ssh_host",
660 account.ssh_host_alias, sizeof(account.ssh_host_alias)) != 0) {
661 clear_error();
662 }
663 } else {
664 clear_error();
665 }
666
667 /* GPG configuration */
668 if (toml_get_string(doc, sections[i], "gpg_key",
669 account.gpg_key_id, sizeof(account.gpg_key_id)) == 0 &&
670 strlen(account.gpg_key_id) > 0) {
671 account.gpg_enabled = true;
672
673 /* GPG signing preference */
674 if (toml_get_boolean(doc, sections[i], "gpg_signing_enabled", &temp_bool) != 0) {
675 clear_error();
676 } else {
677 account.gpg_signing_enabled = temp_bool;
678 }
679 } else {
680 clear_error();
681 }
682
683 /* Validate and add account */
684 if (validate_account_security(&account) == 0) {
685 if (ctx->account_count < MAX_ACCOUNTS) {
686 ctx->accounts[ctx->account_count] = account;
687 ctx->account_count++;
688 log_debug("Loaded account: %s (%s)", account.name, account.description);
689 } else {
690 log_error("Too many accounts, skipping account %u", account_id);
691 }
692 } else {
693 log_error("Account %u failed security validation", account_id);
694 }
695 }
696 }
697
698 log_info("Loaded %zu accounts from configuration", ctx->account_count);
699 return 0;
700 }
701
702 /* Parse account ID from section name like "accounts.1" */
703 static int parse_account_id_from_section(const char *section_name, uint32_t *account_id) {
704 const char *dot_pos;
705 char *endptr;
706 unsigned long parsed_id;
707
708 if (!section_name || !account_id) return -1;
709
710 dot_pos = strchr(section_name, '.');
711 if (!dot_pos || dot_pos == section_name + strlen(section_name) - 1) {
712 return -1;
713 }
714
715 parsed_id = strtoul(dot_pos + 1, &endptr, 10);
716 if (*endptr != '\0' || parsed_id == 0 || parsed_id > UINT32_MAX) {
717 return -1;
718 }
719
720 *account_id = (uint32_t)parsed_id;
721 return 0;
722 }
723
724 /* Validate account security */
725 static int validate_account_security(const account_t *account) {
726 char expanded_path[MAX_PATH_LEN];
727 mode_t file_mode;
728
729 if (!account) {
730 set_error(ERR_INVALID_ARGS, "NULL account to validate");
731 return -1;
732 }
733
734 /* Validate required fields */
735 if (!validate_name(account->name)) {
736 set_error(ERR_ACCOUNT_INVALID, "Invalid account name: %s", account->name);
737 return -1;
738 }
739
740 if (!validate_email(account->email)) {
741 set_error(ERR_ACCOUNT_INVALID, "Invalid email address: %s", account->email);
742 return -1;
743 }
744
745 /* Validate SSH key if configured */
746 if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
747 if (expand_path(account->ssh_key_path, expanded_path, sizeof(expanded_path)) != 0) {
748 set_error(ERR_ACCOUNT_INVALID, "Invalid SSH key path: %s", account->ssh_key_path);
749 return -1;
750 }
751
752 if (!path_exists(expanded_path)) {
753 set_error(ERR_ACCOUNT_INVALID, "SSH key file not found: %s", expanded_path);
754 return -1;
755 }
756
757 /* Check SSH key file permissions - must be 600 */
758 if (get_file_permissions(expanded_path, &file_mode) == 0) {
759 if ((file_mode & 077) != 0) {
760 set_error(ERR_ACCOUNT_INVALID,
761 "SSH key file has unsafe permissions: %o (should be 600)",
762 file_mode & 0777);
763 return -1;
764 }
765 }
766 }
767
768 /* Validate GPG key if configured */
769 if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
770 if (!validate_key_id(account->gpg_key_id)) {
771 set_error(ERR_ACCOUNT_INVALID, "Invalid GPG key ID: %s", account->gpg_key_id);
772 return -1;
773 }
774 }
775
776 return 0;
777 }
778
779 /* Save accounts to TOML document */
780 static int save_accounts_to_toml(const gitswitch_ctx_t *ctx, toml_document_t *doc) {
781 char section_name[64];
782
783 if (!ctx || !doc) {
784 set_error(ERR_INVALID_ARGS, "Invalid arguments to save_accounts_to_toml");
785 return -1;
786 }
787
788 log_debug("Saving %zu accounts to TOML (doc has %zu sections before save)",
789 ctx->account_count, doc->section_count);
790
791 /* Save each account */
792 for (size_t i = 0; i < ctx->account_count; i++) {
793 const account_t *account = &ctx->accounts[i];
794
795 log_debug("Saving account %zu: ID=%u, name='%s', email='%s'",
796 i, account->id, account->name, account->email);
797
798 /* Create section name */
799 if ((size_t)snprintf(section_name, sizeof(section_name), "accounts.%u", account->id) >= sizeof(section_name)) {
800 set_error(ERR_ACCOUNT_INVALID, "Account ID too large: %u", account->id);
801 return -1;
802 }
803
804 log_debug("Creating/updating section: %s", section_name);
805
806 /* Save required fields */
807 if (toml_set_string(doc, section_name, "name", account->name) != 0) {
808 set_error(ERR_CONFIG_INVALID, "Failed to save account name");
809 return -1;
810 }
811
812 if (toml_set_string(doc, section_name, "email", account->email) != 0) {
813 set_error(ERR_CONFIG_INVALID, "Failed to save account email");
814 return -1;
815 }
816
817 /* Save optional fields */
818 if (strlen(account->description) > 0) {
819 toml_set_string(doc, section_name, "description", account->description);
820 }
821
822 toml_set_string(doc, section_name, "preferred_scope",
823 config_scope_to_string(account->preferred_scope));
824
825 /* Save SSH configuration */
826 if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
827 toml_set_string(doc, section_name, "ssh_key", account->ssh_key_path);
828
829 if (strlen(account->ssh_host_alias) > 0) {
830 toml_set_string(doc, section_name, "ssh_host", account->ssh_host_alias);
831 }
832 }
833
834 /* Save GPG configuration */
835 if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
836 toml_set_string(doc, section_name, "gpg_key", account->gpg_key_id);
837 toml_set_boolean(doc, section_name, "gpg_signing_enabled", account->gpg_signing_enabled);
838 }
839 }
840
841 log_debug("Completed saving %zu accounts. TOML doc now has %zu sections",
842 ctx->account_count, doc->section_count);
843
844 /* Debug: List all sections in the document */
845 for (size_t i = 0; i < doc->section_count; i++) {
846 log_debug("TOML section %zu: '%s' (is_set=%s)",
847 i, doc->sections[i].name, doc->sections[i].is_set ? "true" : "false");
848 }
849
850 return 0;
851 }