From 2cece40f1efa421880460a672b483d4fa78af69e Mon Sep 17 00:00:00 2001 From: Neeladri Das Date: Thu, 12 Feb 2026 10:38:07 +0000 Subject: [PATCH 1/2] list admin tokens --- CLAUDE.md | 24 ++++++++---- README.md | 30 ++++++++++++++- main.go | 50 ++++++++++++++++++++++-- tokens.go | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+), 11 deletions(-) create mode 100644 tokens.go diff --git a/CLAUDE.md b/CLAUDE.md index 6e6d4f9..b69d6e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a Go-based CLI tool for interacting with JuliaHub, a platform for Julia computing. The CLI provides commands for authentication, dataset management, registry management, project management, user information, Git integration, and Julia integration. +This is a Go-based CLI tool for interacting with JuliaHub, a platform for Julia computing. The CLI provides commands for authentication, dataset management, registry management, project management, user information, token management, Git integration, and Julia integration. ## Architecture @@ -16,6 +16,7 @@ The application follows a command-line interface pattern using the Cobra library - **registries.go**: Registry operations (list) with REST API integration - **projects.go**: Project management using GraphQL API with user filtering - **user.go**: User information retrieval using GraphQL API and REST API for listing users +- **tokens.go**: Token management operations (list) with REST API integration - **git.go**: Git integration (clone, push, fetch, pull) with JuliaHub authentication - **julia.go**: Julia installation and management - **run.go**: Julia execution with JuliaHub configuration @@ -30,11 +31,7 @@ The application follows a command-line interface pattern using the Cobra library - Stores tokens securely in `~/.juliahub` with 0600 permissions 2. **API Integration**: -<<<<<<< HEAD - - **REST API**: Used for dataset operations (`/api/v1/datasets`, `/datasets/{uuid}/url/{version}`) and user management (`/app/config/features/manage`) -======= - - **REST API**: Used for dataset operations (`/api/v1/datasets`, `/datasets/{uuid}/url/{version}`) and registry operations (`/api/v1/ui/registries/descriptions`) ->>>>>>> fadd2b0ea19a8b11eb903d5884ffa50371e337e4 + - **REST API**: Used for dataset operations (`/api/v1/datasets`, `/datasets/{uuid}/url/{version}`), registry operations (`/api/v1/ui/registries/descriptions`), user management (`/app/config/features/manage`), and token management (`/app/token/activelist`) - **GraphQL API**: Used for projects and user info (`/v1/graphql`) - **Headers**: All GraphQL requests require `X-Hasura-Role: jhuser` header - **Authentication**: Uses ID tokens (`token.IDToken`) for API calls @@ -45,8 +42,9 @@ The application follows a command-line interface pattern using the Cobra library - `jh registry`: Registry operations (list with REST API, supports verbose mode) - `jh project`: Project management (list with GraphQL, supports user filtering) - `jh user`: User information (info with GraphQL) - - `jh admin`: Administrative commands (user management) + - `jh admin`: Administrative commands (user management, token management) - `jh admin user`: User management (list all users with REST API, supports verbose mode) + - `jh admin token`: Token management (list all tokens with REST API, supports verbose mode) - `jh clone`: Git clone with JuliaHub authentication and project name resolution - `jh push/fetch/pull`: Git operations with JuliaHub authentication - `jh git-credential`: Git credential helper for seamless authentication @@ -108,6 +106,13 @@ go run . admin user list go run . admin user list --verbose ``` +### Test token operations +```bash +go run . admin token list +go run . admin token list --verbose +TZ=America/New_York go run . admin token list --verbose # With specific timezone +``` + ### Test Git operations ```bash go run . clone john/my-project # Clone from another user @@ -181,6 +186,7 @@ The application uses OAuth2 device flow: ### REST API Integration - **Dataset operations**: Use presigned URLs for upload/download - **User management**: `/app/config/features/manage` endpoint for listing all users +- **Token management**: `/app/token/activelist` endpoint for listing all API tokens - **Authentication**: Bearer token with ID token - **Upload workflow**: 3-step process (request presigned URL, upload to URL, close upload) @@ -298,6 +304,10 @@ jh run setup - Admin user list command (`jh admin user list`) uses REST API endpoint `/app/config/features/manage` which requires appropriate permissions - User list output is concise by default (Name and Email only); use `--verbose` flag for detailed information (UUID, groups, features) - Registry list output is concise by default (UUID and Name only); use `--verbose` flag for detailed information (owner, creation date, package count, description) +- Admin token list command (`jh admin token list`) uses REST API endpoint `/app/token/activelist` which requires appropriate permissions +- Token list output is concise by default (Subject, Created By, and Expired status only); use `--verbose` flag for detailed information (signature, creation date, expiration date with estimate indicator) +- Token dates are formatted in human-readable format and converted to local timezone (respects system timezone or TZ environment variable) +- Token expiration estimate indicator only shown when `expires_at_is_estimate` is true in API response ## Implementation Details diff --git a/README.md b/README.md index 1fe27b7..f36e321 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A command-line interface for interacting with JuliaHub, a platform for Julia com - **Git Integration**: Clone, push, fetch, and pull with automatic JuliaHub authentication - **Julia Integration**: Install Julia and run with JuliaHub package server configuration - **User Management**: Display user information and view profile details -- **Administrative Commands**: Manage users and system resources (requires admin permissions) +- **Administrative Commands**: Manage users, tokens, and system resources (requires admin permissions) ## Installation @@ -186,10 +186,16 @@ go build -o jh . ### Administrative Commands (`jh admin`) +#### User Management - `jh admin user list` - List all users (requires appropriate permissions) - Default: Shows only Name and Email - `jh admin user list --verbose` - Show detailed user information including UUID, groups, and features +#### Token Management +- `jh admin token list` - List all tokens (requires appropriate permissions) + - Default: Shows only Subject, Created By, and Expired status + - `jh admin token list --verbose` - Show detailed token information including signature, creation date, expiration date (with estimate indicator) + ### Update (`jh update`) - `jh update` - Check for updates and automatically install the latest version @@ -254,6 +260,28 @@ jh project list --user jh project list --user alice ``` +### Administrative Operations + +```bash +# List all users (requires admin permissions) +jh admin user list + +# List users with detailed information +jh admin user list --verbose + +# List all tokens (requires admin permissions) +jh admin token list + +# List tokens with detailed information including signatures and dates +jh admin token list --verbose + +# List tokens on custom server +jh admin token list -s yourinstall + +# Use specific timezone for date display +TZ=America/New_York jh admin token list --verbose +``` + ### Git Workflow ```bash diff --git a/main.go b/main.go index b45856c..870b237 100644 --- a/main.go +++ b/main.go @@ -167,7 +167,7 @@ Available command categories: registry - Registry management (list registries) project - Project management (list, filter by user) user - User information and profile - admin - Administrative commands (user management) + admin - Administrative commands (user management, token management) clone - Clone projects with automatic authentication push - Push changes with authentication fetch - Fetch updates with authentication @@ -1036,7 +1036,7 @@ var adminCmd = &cobra.Command{ Long: `Administrative commands for JuliaHub. These commands provide administrative functionality for managing JuliaHub -resources such as users, groups, and system configuration. +resources such as users, tokens, groups, and system configuration. Note: Some commands may require administrative permissions.`, } @@ -1051,6 +1051,47 @@ Provides commands to list and manage users across the JuliaHub instance. Note: These commands require appropriate administrative permissions.`, } +var adminTokenCmd = &cobra.Command{ + Use: "token", + Short: "Token management commands", + Long: `Administrative commands for managing API tokens on JuliaHub. + +Provides commands to list and manage API tokens across the JuliaHub instance. + +Note: These commands require appropriate administrative permissions.`, +} + +var tokenListCmd = &cobra.Command{ + Use: "list", + Short: "List all tokens", + Long: `List all API tokens from JuliaHub. + +By default, displays only Subject, Created By, and Expired status for each token. +Use --verbose flag to display comprehensive information including: +- Subject and signature +- Created by and creation date +- Expiration date (with estimate indicator) +- Expiration status + +This command uses the /app/token/activelist endpoint which requires +appropriate permissions to view all tokens.`, + Example: " jh admin token list\n jh admin token list --verbose", + Run: func(cmd *cobra.Command, args []string) { + server, err := getServerFromFlagOrConfig(cmd) + if err != nil { + fmt.Printf("Failed to get server config: %v\n", err) + os.Exit(1) + } + + verbose, _ := cmd.Flags().GetBool("verbose") + + if err := listTokens(server, verbose); err != nil { + fmt.Printf("Failed to list tokens: %v\n", err) + os.Exit(1) + } + }, +} + var updateCmd = &cobra.Command{ Use: "update", Short: "Update jh to the latest version", @@ -1087,6 +1128,8 @@ func init() { userInfoCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") userListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") userListCmd.Flags().Bool("verbose", false, "Show detailed user information") + tokenListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") + tokenListCmd.Flags().Bool("verbose", false, "Show detailed token information") cloneCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") pushCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") fetchCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") @@ -1100,7 +1143,8 @@ func init() { projectCmd.AddCommand(projectListCmd) userCmd.AddCommand(userInfoCmd) adminUserCmd.AddCommand(userListCmd) - adminCmd.AddCommand(adminUserCmd) + adminTokenCmd.AddCommand(tokenListCmd) + adminCmd.AddCommand(adminUserCmd, adminTokenCmd) juliaCmd.AddCommand(juliaInstallCmd) runCmd.AddCommand(runSetupCmd) gitCredentialCmd.AddCommand(gitCredentialHelperCmd, gitCredentialGetCmd, gitCredentialStoreCmd, gitCredentialEraseCmd, gitCredentialSetupCmd) diff --git a/tokens.go b/tokens.go new file mode 100644 index 0000000..97dc0bf --- /dev/null +++ b/tokens.go @@ -0,0 +1,113 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// Token represents a JuliaHub API token +type Token struct { + CreatedBy string `json:"created_by"` + IsExpired bool `json:"is_expired"` + CreatedAt string `json:"created_at"` + ExpiresAt string `json:"expires_at"` + ExpiresAtIsEstimate bool `json:"expires_at_is_estimate,omitempty"` + Subject string `json:"subject"` + Signature string `json:"signature"` +} + +// TokensResponse represents the response from /app/token/activelist +type TokensResponse struct { + Tokens []Token `json:"tokens"` + Message string `json:"message"` + Success bool `json:"success"` +} + +// formatTokenDate parses and formats a token date string into a readable format +func formatTokenDate(dateStr string) string { + // Try parsing as RFC3339 with fractional seconds + t, err := time.Parse(time.RFC3339, dateStr) + if err != nil { + // If parsing fails, return the original string + return dateStr + } + // Convert to local timezone + localTime := t.Local() + // Format with timezone offset: "Jan 02, 2006 15:04:05 -0700" + return localTime.Format("Jan 02, 2006 15:04:05 -0700") +} + +func listTokens(server string, verbose bool) error { + token, err := ensureValidToken() + if err != nil { + return fmt.Errorf("authentication required: %w", err) + } + + url := fmt.Sprintf("https://%s/app/token/activelist", server) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.IDToken)) + req.Header.Set("Accept", "application/json") + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to make request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response: %w", err) + } + + var response TokensResponse + if err := json.Unmarshal(body, &response); err != nil { + return fmt.Errorf("failed to parse response: %w", err) + } + + if !response.Success { + return fmt.Errorf("API request failed: %s", response.Message) + } + + // Display tokens + fmt.Printf("Tokens (%d total):\n\n", len(response.Tokens)) + + if verbose { + // Verbose mode: show all details + for _, tok := range response.Tokens { + fmt.Printf("Subject: %s\n", tok.Subject) + fmt.Printf("Signature: %s\n", tok.Signature) + fmt.Printf("Created By: %s\n", tok.CreatedBy) + fmt.Printf("Created At: %s\n", formatTokenDate(tok.CreatedAt)) + fmt.Printf("Expires At: %s", formatTokenDate(tok.ExpiresAt)) + if tok.ExpiresAtIsEstimate { + fmt.Printf(" (estimate)") + } + fmt.Printf("\n") + fmt.Printf("Expired: %t\n", tok.IsExpired) + fmt.Println() + } + } else { + // Default mode: show only Subject, Created By, and Expired status + for _, tok := range response.Tokens { + fmt.Printf("Subject: %s\n", tok.Subject) + fmt.Printf("Created By: %s\n", tok.CreatedBy) + fmt.Printf("Expired: %t\n", tok.IsExpired) + fmt.Println() + } + } + + return nil +} From 41f6433752d086c6ba61a6b131e82ee316a69136 Mon Sep 17 00:00:00 2001 From: Neeladri Das Date: Thu, 12 Feb 2026 11:57:38 +0000 Subject: [PATCH 2/2] refactor --- tokens.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tokens.go b/tokens.go index 97dc0bf..d4e3f9c 100644 --- a/tokens.go +++ b/tokens.go @@ -8,7 +8,6 @@ import ( "time" ) -// Token represents a JuliaHub API token type Token struct { CreatedBy string `json:"created_by"` IsExpired bool `json:"is_expired"` @@ -19,24 +18,21 @@ type Token struct { Signature string `json:"signature"` } -// TokensResponse represents the response from /app/token/activelist type TokensResponse struct { Tokens []Token `json:"tokens"` Message string `json:"message"` Success bool `json:"success"` } -// formatTokenDate parses and formats a token date string into a readable format func formatTokenDate(dateStr string) string { // Try parsing as RFC3339 with fractional seconds t, err := time.Parse(time.RFC3339, dateStr) if err != nil { - // If parsing fails, return the original string return dateStr } + // Convert to local timezone localTime := t.Local() - // Format with timezone offset: "Jan 02, 2006 15:04:05 -0700" return localTime.Format("Jan 02, 2006 15:04:05 -0700") }