Files
zeekay e1f15ca18b cli: workspace config .kms.json -> kms.json (straightforward, not a hidden dotfile)
De-Infisical the project-link file: rename the per-directory workspace config from
the hidden .kms.json to a plain kms.json, DRY the one hardcoded copy (config.go now
uses KMS_WORKSPACE_CONFIG_FILE_NAME), update flag help, and rip the stale
.infisical.json from .gitignore (-> kms.json). Forward-only; no .infisical.json or
.kms.json files exist to migrate.
2026-06-25 17:48:47 -07:00

181 lines
5.9 KiB
Go

/*
Copyright (c) 2024 Hanzo AI Inc.
*/
package cmd
import (
"encoding/hex"
"fmt"
"strings"
"github.com/hanzokms/cli/packages/api"
"github.com/hanzokms/cli/packages/crypto"
"github.com/hanzokms/cli/packages/util"
"github.com/spf13/cobra"
)
var tokensCmd = &cobra.Command{
Use: "service-token",
Short: "Manage service tokens",
DisableFlagsInUseLine: true,
Example: "kms service-token",
Args: cobra.ExactArgs(0),
PreRun: func(cmd *cobra.Command, args []string) {
util.RequireLogin()
},
Run: func(cmd *cobra.Command, args []string) {
},
}
var tokensCreateCmd = &cobra.Command{
Use: "create",
Short: "Used to create service tokens",
DisableFlagsInUseLine: true,
Example: "kms service-token create",
Args: cobra.ExactArgs(0),
PreRun: func(cmd *cobra.Command, args []string) {
util.RequireLogin()
},
Run: func(cmd *cobra.Command, args []string) {
// get plain text workspace key
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "Unable to retrieve your logged in your details. Please login in then try again")
}
if loggedInUserDetails.LoginExpired {
loggedInUserDetails = util.EstablishUserLoginSession()
}
tokenOnly, err := cmd.Flags().GetBool("token-only")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
workspaceId, err := cmd.Flags().GetString("projectId")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if workspaceId == "" {
configFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.PrintErrorMessageAndExit("Please either run kms init to connect to a project or pass in project id with --projectId flag")
}
workspaceId = configFile.WorkspaceId
}
serviceTokenName, err := cmd.Flags().GetString("name")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
expireSeconds, err := cmd.Flags().GetInt("expiry-seconds")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
scopes, err := cmd.Flags().GetStringSlice("scope")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if len(scopes) == 0 {
util.PrintErrorMessageAndExit("You must define the environments and paths your service token should have access to via the --scope flag")
}
permissions := []api.ScopePermission{}
for _, scope := range scopes {
parts := strings.Split(scope, ":")
if len(parts) != 2 {
util.PrintlnStderr("--scope flag is malformed. Each scope flag should be in the following format: <env-slug>:<folder-path>")
return
}
permissions = append(permissions, api.ScopePermission{Environment: parts[0], SecretPath: parts[1]})
}
accessLevels, err := cmd.Flags().GetStringSlice("access-level")
if err != nil {
util.HandleError(err, "Unable to parse flag accessLevels")
}
if len(accessLevels) == 0 {
util.PrintErrorMessageAndExit("You must define whether your service token can be used to read and or write via the --access-level flag")
}
for _, accessLevel := range accessLevels {
if accessLevel != "read" && accessLevel != "write" {
util.PrintErrorMessageAndExit("--access-level can only be of values read and write")
}
}
randomBytes, err := crypto.GenerateRandomBytes(16)
if err != nil {
util.HandleError(err)
}
hexEncodedRandomBytes := hex.EncodeToString(randomBytes)
// make a call to the api to save the encrypted symmetric key details
httpClient, err := util.GetRestyClientWithCustomHeaders()
if err != nil {
util.HandleError(err, "Unable to get resty client with custom headers")
}
httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json")
createServiceTokenResponse, err := api.CallCreateServiceToken(httpClient, api.CreateServiceTokenRequest{
Name: serviceTokenName,
WorkspaceId: workspaceId,
Scopes: permissions,
ExpiresIn: expireSeconds,
Permissions: accessLevels,
RandomBytes: hexEncodedRandomBytes,
// No longer required for creating service tokens:
EncryptedKey: "",
Iv: "",
Tag: "",
})
if err != nil {
util.HandleError(err, "Unable to create service token")
}
serviceToken := createServiceTokenResponse.ServiceToken + "." + hexEncodedRandomBytes
if tokenOnly {
util.PrintlnStdout(serviceToken)
} else {
printablePermission := []string{}
for _, permission := range permissions {
printablePermission = append(printablePermission, fmt.Sprintf("([environment: %v] [path: %v])", permission.Environment, permission.SecretPath))
}
util.PrintfStderr("New service token created\n")
util.PrintfStderr("Name: %v\n", serviceTokenName)
util.PrintfStderr("Project ID: %v\n", workspaceId)
util.PrintfStderr("Access type: [%v]\n", strings.Join(accessLevels, ", "))
util.PrintfStderr("Permission(s): %v\n", strings.Join(printablePermission, ", "))
util.PrintfStderr("Service Token: %v\n", serviceToken)
}
},
}
func init() {
tokensCreateCmd.Flags().String("projectId", "", "The project ID you'd like to create the service token for. Default: will use linked project in kms.json")
tokensCreateCmd.Flags().StringSliceP("scope", "s", []string{}, "Environment and secret path. Example format: <env-slug>:<folder-path>")
tokensCreateCmd.Flags().StringP("name", "n", "Service token generated via CLI", "Service token name")
tokensCreateCmd.Flags().StringSliceP("access-level", "a", []string{}, "The type of access the service token should have. Can be 'read' and or 'write'")
tokensCreateCmd.Flags().Bool("token-only", false, "When true, only the service token will be printed")
tokensCreateCmd.Flags().IntP("expiry-seconds", "e", 86400, "Set the service token's expiration time in seconds from now. To never expire set to zero. Default: 1 day ")
tokensCmd.AddCommand(tokensCreateCmd)
RootCmd.AddCommand(tokensCmd)
}