4
0

main.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. // webapi_keygen generates and manages Web API keys for the RAS Web AIM API.
  2. // Usage: go run ./cmd/webapi_keygen [command] [options]
  3. package main
  4. import (
  5. "context"
  6. "crypto/rand"
  7. "encoding/hex"
  8. "encoding/json"
  9. "flag"
  10. "fmt"
  11. "os"
  12. "strings"
  13. "text/tabwriter"
  14. "time"
  15. "github.com/google/uuid"
  16. "github.com/joho/godotenv"
  17. "github.com/mk6i/open-oscar-server/state"
  18. )
  19. const (
  20. keyLength = 32 // 256 bits of entropy
  21. )
  22. func main() {
  23. // Load environment configuration
  24. if err := godotenv.Load("config/settings.env"); err != nil {
  25. fmt.Printf("Config file not found, using environment variables\n")
  26. }
  27. if len(os.Args) < 2 {
  28. printUsage()
  29. os.Exit(1)
  30. }
  31. command := os.Args[1]
  32. args := os.Args[2:]
  33. switch command {
  34. case "generate", "gen":
  35. handleGenerate(args)
  36. case "list", "ls":
  37. handleList(args)
  38. case "revoke", "delete", "rm":
  39. handleRevoke(args)
  40. case "activate":
  41. handleActivate(args)
  42. case "update":
  43. handleUpdate(args)
  44. case "show":
  45. handleShow(args)
  46. case "help", "-h", "--help":
  47. printUsage()
  48. default:
  49. fmt.Fprintf(os.Stderr, "Unknown command: %s\n\n", command)
  50. printUsage()
  51. os.Exit(1)
  52. }
  53. }
  54. func printUsage() {
  55. fmt.Println("Web API Key Generator for RAS")
  56. fmt.Println("\nUsage: webapi_keygen <command> [options]")
  57. fmt.Println("\nCommands:")
  58. fmt.Println(" generate, gen Generate a new API key")
  59. fmt.Println(" list, ls List all API keys")
  60. fmt.Println(" show Show details of a specific key")
  61. fmt.Println(" revoke, delete Deactivate an API key")
  62. fmt.Println(" activate Reactivate an API key")
  63. fmt.Println(" update Update API key settings")
  64. fmt.Println(" help Show this help message")
  65. fmt.Println("\nGenerate Options:")
  66. fmt.Println(" --app-name Application name (required)")
  67. fmt.Println(" --origins Comma-separated list of allowed origins")
  68. fmt.Println(" --rate-limit Requests per minute (default: 60)")
  69. fmt.Println(" --capabilities Comma-separated list of capabilities")
  70. fmt.Println("\nUpdate Options:")
  71. fmt.Println(" --dev-id Developer ID to update (required)")
  72. fmt.Println(" --app-name New application name")
  73. fmt.Println(" --origins New comma-separated list of allowed origins")
  74. fmt.Println(" --rate-limit New requests per minute limit")
  75. fmt.Println(" --capabilities New comma-separated list of capabilities")
  76. fmt.Println("\nExamples:")
  77. fmt.Println(" webapi_keygen generate --app-name \"My Web Client\" --origins \"https://example.com,https://app.example.com\"")
  78. fmt.Println(" webapi_keygen list")
  79. fmt.Println(" webapi_keygen show --dev-id dev_abc123")
  80. fmt.Println(" webapi_keygen revoke --dev-id dev_abc123")
  81. fmt.Println(" webapi_keygen update --dev-id dev_abc123 --rate-limit 120")
  82. }
  83. func handleGenerate(args []string) {
  84. fs := flag.NewFlagSet("generate", flag.ExitOnError)
  85. appName := fs.String("app-name", "", "Application name (required)")
  86. originsStr := fs.String("origins", "", "Comma-separated list of allowed origins")
  87. rateLimit := fs.Int("rate-limit", 60, "Requests per minute")
  88. capabilitiesStr := fs.String("capabilities", "", "Comma-separated list of capabilities")
  89. if err := fs.Parse(args); err != nil {
  90. fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
  91. os.Exit(1)
  92. }
  93. if *appName == "" {
  94. fmt.Fprintln(os.Stderr, "Error: --app-name is required")
  95. os.Exit(1)
  96. }
  97. // Parse origins and capabilities
  98. var origins []string
  99. if *originsStr != "" {
  100. origins = parseCSV(*originsStr)
  101. }
  102. var capabilities []string
  103. if *capabilitiesStr != "" {
  104. capabilities = parseCSV(*capabilitiesStr)
  105. }
  106. // Generate secure random key
  107. keyBytes := make([]byte, keyLength)
  108. if _, err := rand.Read(keyBytes); err != nil {
  109. fmt.Fprintf(os.Stderr, "Error generating key: %v\n", err)
  110. os.Exit(1)
  111. }
  112. devKey := hex.EncodeToString(keyBytes)
  113. // Generate dev_id
  114. devID := fmt.Sprintf("dev_%s", uuid.New().String())
  115. // Create the API key record
  116. apiKey := state.WebAPIKey{
  117. DevID: devID,
  118. DevKey: devKey,
  119. AppName: *appName,
  120. CreatedAt: time.Now(),
  121. IsActive: true,
  122. RateLimit: *rateLimit,
  123. AllowedOrigins: origins,
  124. Capabilities: capabilities,
  125. }
  126. // Connect to database and insert the key
  127. store, err := connectToStore()
  128. if err != nil {
  129. fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
  130. os.Exit(1)
  131. }
  132. ctx := context.Background()
  133. if err := store.CreateAPIKey(ctx, apiKey); err != nil {
  134. fmt.Fprintf(os.Stderr, "Error creating API key: %v\n", err)
  135. os.Exit(1)
  136. }
  137. // Output the generated key details
  138. fmt.Println("Successfully generated Web API key:")
  139. fmt.Println("=====================================")
  140. fmt.Printf("Developer ID: %s\n", devID)
  141. fmt.Printf("API Key: %s\n", devKey)
  142. fmt.Printf("App Name: %s\n", *appName)
  143. fmt.Printf("Rate Limit: %d requests/minute\n", *rateLimit)
  144. if len(origins) > 0 {
  145. fmt.Printf("Origins: %s\n", strings.Join(origins, ", "))
  146. }
  147. if len(capabilities) > 0 {
  148. fmt.Printf("Capabilities: %s\n", strings.Join(capabilities, ", "))
  149. }
  150. fmt.Println("=====================================")
  151. fmt.Println("\nIMPORTANT: Save the API key securely. It cannot be retrieved later.")
  152. }
  153. func handleList(args []string) {
  154. store, err := connectToStore()
  155. if err != nil {
  156. fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
  157. os.Exit(1)
  158. }
  159. ctx := context.Background()
  160. keys, err := store.ListAPIKeys(ctx)
  161. if err != nil {
  162. fmt.Fprintf(os.Stderr, "Error listing API keys: %v\n", err)
  163. os.Exit(1)
  164. }
  165. if len(keys) == 0 {
  166. fmt.Println("No API keys found.")
  167. return
  168. }
  169. // Create a tabwriter for formatted output
  170. w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
  171. _, _ = fmt.Fprintln(w, "DEV ID\tAPP NAME\tACTIVE\tRATE LIMIT\tCREATED\tLAST USED")
  172. _, _ = fmt.Fprintln(w, "------\t--------\t------\t----------\t-------\t---------")
  173. for _, key := range keys {
  174. lastUsed := "Never"
  175. if key.LastUsed != nil {
  176. lastUsed = key.LastUsed.Format("2006-01-02 15:04")
  177. }
  178. _, _ = fmt.Fprintf(w, "%s\t%s\t%v\t%d/min\t%s\t%s\n",
  179. truncateString(key.DevID, 20),
  180. truncateString(key.AppName, 20),
  181. key.IsActive,
  182. key.RateLimit,
  183. key.CreatedAt.Format("2006-01-02"),
  184. lastUsed,
  185. )
  186. }
  187. _ = w.Flush()
  188. }
  189. func handleShow(args []string) {
  190. fs := flag.NewFlagSet("show", flag.ExitOnError)
  191. devID := fs.String("dev-id", "", "Developer ID (required)")
  192. if err := fs.Parse(args); err != nil {
  193. fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
  194. os.Exit(1)
  195. }
  196. if *devID == "" {
  197. fmt.Fprintln(os.Stderr, "Error: --dev-id is required")
  198. os.Exit(1)
  199. }
  200. store, err := connectToStore()
  201. if err != nil {
  202. fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
  203. os.Exit(1)
  204. }
  205. ctx := context.Background()
  206. key, err := store.GetAPIKeyByDevID(ctx, *devID)
  207. if err != nil {
  208. if err == state.ErrNoAPIKey {
  209. fmt.Fprintf(os.Stderr, "Error: API key not found for dev_id: %s\n", *devID)
  210. } else {
  211. fmt.Fprintf(os.Stderr, "Error retrieving API key: %v\n", err)
  212. }
  213. os.Exit(1)
  214. }
  215. // Output detailed key information
  216. fmt.Println("Web API Key Details:")
  217. fmt.Println("=====================================")
  218. fmt.Printf("Developer ID: %s\n", key.DevID)
  219. fmt.Printf("App Name: %s\n", key.AppName)
  220. fmt.Printf("Active: %v\n", key.IsActive)
  221. fmt.Printf("Rate Limit: %d requests/minute\n", key.RateLimit)
  222. fmt.Printf("Created: %s\n", key.CreatedAt.Format("2006-01-02 15:04:05"))
  223. if key.LastUsed != nil {
  224. fmt.Printf("Last Used: %s\n", key.LastUsed.Format("2006-01-02 15:04:05"))
  225. } else {
  226. fmt.Println("Last Used: Never")
  227. }
  228. if len(key.AllowedOrigins) > 0 {
  229. fmt.Printf("Origins: %s\n", strings.Join(key.AllowedOrigins, ", "))
  230. } else {
  231. fmt.Println("Origins: All origins allowed")
  232. }
  233. if len(key.Capabilities) > 0 {
  234. fmt.Printf("Capabilities: %s\n", strings.Join(key.Capabilities, ", "))
  235. } else {
  236. fmt.Println("Capabilities: All capabilities enabled")
  237. }
  238. fmt.Println("=====================================")
  239. }
  240. func handleRevoke(args []string) {
  241. fs := flag.NewFlagSet("revoke", flag.ExitOnError)
  242. devID := fs.String("dev-id", "", "Developer ID to revoke (required)")
  243. if err := fs.Parse(args); err != nil {
  244. fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
  245. os.Exit(1)
  246. }
  247. if *devID == "" {
  248. fmt.Fprintln(os.Stderr, "Error: --dev-id is required")
  249. os.Exit(1)
  250. }
  251. store, err := connectToStore()
  252. if err != nil {
  253. fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
  254. os.Exit(1)
  255. }
  256. ctx := context.Background()
  257. isActive := false
  258. update := state.WebAPIKeyUpdate{
  259. IsActive: &isActive,
  260. }
  261. if err := store.UpdateAPIKey(ctx, *devID, update); err != nil {
  262. if err == state.ErrNoAPIKey {
  263. fmt.Fprintf(os.Stderr, "Error: API key not found for dev_id: %s\n", *devID)
  264. } else {
  265. fmt.Fprintf(os.Stderr, "Error revoking API key: %v\n", err)
  266. }
  267. os.Exit(1)
  268. }
  269. fmt.Printf("Successfully revoked API key: %s\n", *devID)
  270. }
  271. func handleActivate(args []string) {
  272. fs := flag.NewFlagSet("activate", flag.ExitOnError)
  273. devID := fs.String("dev-id", "", "Developer ID to activate (required)")
  274. if err := fs.Parse(args); err != nil {
  275. fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
  276. os.Exit(1)
  277. }
  278. if *devID == "" {
  279. fmt.Fprintln(os.Stderr, "Error: --dev-id is required")
  280. os.Exit(1)
  281. }
  282. store, err := connectToStore()
  283. if err != nil {
  284. fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
  285. os.Exit(1)
  286. }
  287. ctx := context.Background()
  288. isActive := true
  289. update := state.WebAPIKeyUpdate{
  290. IsActive: &isActive,
  291. }
  292. if err := store.UpdateAPIKey(ctx, *devID, update); err != nil {
  293. if err == state.ErrNoAPIKey {
  294. fmt.Fprintf(os.Stderr, "Error: API key not found for dev_id: %s\n", *devID)
  295. } else {
  296. fmt.Fprintf(os.Stderr, "Error activating API key: %v\n", err)
  297. }
  298. os.Exit(1)
  299. }
  300. fmt.Printf("Successfully activated API key: %s\n", *devID)
  301. }
  302. func handleUpdate(args []string) {
  303. fs := flag.NewFlagSet("update", flag.ExitOnError)
  304. devID := fs.String("dev-id", "", "Developer ID to update (required)")
  305. appName := fs.String("app-name", "", "New application name")
  306. originsStr := fs.String("origins", "", "New comma-separated list of allowed origins")
  307. rateLimit := fs.Int("rate-limit", -1, "New requests per minute limit")
  308. capabilitiesStr := fs.String("capabilities", "", "New comma-separated list of capabilities")
  309. if err := fs.Parse(args); err != nil {
  310. fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
  311. os.Exit(1)
  312. }
  313. if *devID == "" {
  314. fmt.Fprintln(os.Stderr, "Error: --dev-id is required")
  315. os.Exit(1)
  316. }
  317. update := state.WebAPIKeyUpdate{}
  318. if *appName != "" {
  319. update.AppName = appName
  320. }
  321. if *originsStr != "" {
  322. origins := parseCSV(*originsStr)
  323. update.AllowedOrigins = &origins
  324. }
  325. if *rateLimit > 0 {
  326. update.RateLimit = rateLimit
  327. }
  328. if *capabilitiesStr != "" {
  329. capabilities := parseCSV(*capabilitiesStr)
  330. update.Capabilities = &capabilities
  331. }
  332. // Check if any updates were provided
  333. updateJSON, _ := json.Marshal(update)
  334. if string(updateJSON) == "{}" {
  335. fmt.Fprintln(os.Stderr, "Error: No update fields provided")
  336. os.Exit(1)
  337. }
  338. store, err := connectToStore()
  339. if err != nil {
  340. fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
  341. os.Exit(1)
  342. }
  343. ctx := context.Background()
  344. if err := store.UpdateAPIKey(ctx, *devID, update); err != nil {
  345. if err == state.ErrNoAPIKey {
  346. fmt.Fprintf(os.Stderr, "Error: API key not found for dev_id: %s\n", *devID)
  347. } else {
  348. fmt.Fprintf(os.Stderr, "Error updating API key: %v\n", err)
  349. }
  350. os.Exit(1)
  351. }
  352. fmt.Printf("Successfully updated API key: %s\n", *devID)
  353. }
  354. func connectToStore() (*state.SQLiteUserStore, error) {
  355. dbPath := os.Getenv("DB_PATH")
  356. if dbPath == "" {
  357. dbPath = "oscar.sqlite"
  358. }
  359. return state.NewSQLiteUserStore(dbPath)
  360. }
  361. func parseCSV(input string) []string {
  362. if input == "" {
  363. return []string{}
  364. }
  365. parts := strings.Split(input, ",")
  366. result := make([]string, 0, len(parts))
  367. for _, part := range parts {
  368. trimmed := strings.TrimSpace(part)
  369. if trimmed != "" {
  370. result = append(result, trimmed)
  371. }
  372. }
  373. return result
  374. }
  375. func truncateString(s string, maxLen int) string {
  376. if len(s) <= maxLen {
  377. return s
  378. }
  379. return s[:maxLen-3] + "..."
  380. }