add create service token to cli + docs for it

service-token-v2-create-cli
Maidul Islam 8 months ago
parent 48f7bd146f
commit 896a34eb65

@ -25,7 +25,7 @@ func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncrypted
}
if response.IsError() {
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response)
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response [%v %v] [status-code=%v]", response.Request.Method, response.Request.URL, response.StatusCode())
}
return result, nil
@ -339,3 +339,23 @@ func CallGetSingleSecretByNameV3(httpClient *resty.Client, request CreateSecretV
return nil
}
func CallCreateServiceToken(httpClient *resty.Client, request CreateServiceTokenRequest) (CreateServiceTokenResponse, error) {
var createServiceTokenResponse CreateServiceTokenResponse
response, err := httpClient.
R().
SetResult(&createServiceTokenResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v2/service-token/", config.INFISICAL_URL))
if err != nil {
return CreateServiceTokenResponse{}, fmt.Errorf("CallCreateServiceToken: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return CreateServiceTokenResponse{}, fmt.Errorf("CallCreateServiceToken: Unsuccessful response [%v %v] [status-code=%v]", response.Request.Method, response.Request.URL, response.StatusCode())
}
return createServiceTokenResponse, nil
}

@ -387,3 +387,37 @@ type GetSingleSecretByNameSecretResponse struct {
UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"`
}
type ScopePermission struct {
Environment string `json:"environment"`
SecretPath string `json:"secretPath"`
}
type CreateServiceTokenRequest struct {
Name string `json:"name"`
WorkspaceId string `json:"workspaceId"`
Scopes []ScopePermission `json:"scopes"`
ExpiresIn int `json:"expiresIn"`
EncryptedKey string `json:"encryptedKey"`
Iv string `json:"iv"`
Tag string `json:"tag"`
RandomBytes string `json:"randomBytes"`
Permissions []string `json:"permissions"`
}
type ServiceTokenData struct {
ID string `json:"_id"`
Name string `json:"name"`
Workspace string `json:"workspace"`
Scopes []interface{} `json:"scopes"`
User string `json:"user"`
LastUsed time.Time `json:"lastUsed"`
Permissions []string `json:"permissions"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type CreateServiceTokenResponse struct {
ServiceToken string `json:"serviceToken"`
ServiceTokenData ServiceTokenData `json:"serviceTokenData"`
}

@ -0,0 +1,181 @@
/*
Copyright (c) 2023 Infisical Inc.
*/
package cmd
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/crypto"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/go-resty/resty/v2"
"github.com/spf13/cobra"
)
var tokensCmd = &cobra.Command{
Use: "service-token",
Short: "Manage service tokens",
DisableFlagsInUseLine: true,
Example: "infisical 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: "infisical 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, _ := util.GetCurrentLoggedInUserDetails()
if loggedInUserDetails.LoginExpired {
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
}
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 infisical 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")
}
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 {
fmt.Println("--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")
}
}
workspaceKey, err := util.GetPlainTextWorkspaceKey(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceId)
if err != nil {
fmt.Println(err)
}
newWorkspaceEncryptionKey := make([]byte, 16)
_, err = rand.Read(newWorkspaceEncryptionKey)
if err != nil {
fmt.Println("Error generating random bytes:", err)
return
}
newWorkspaceEncryptionKeyHexFormat := hex.EncodeToString(newWorkspaceEncryptionKey)
// encrypt the workspace key symmetrically
encryptedDetails, err := crypto.EncryptSymmetric(workspaceKey, newWorkspaceEncryptionKey)
if err != nil {
fmt.Println(err)
}
// make a call to the api to save the encrypted symmetric key details
httpClient := resty.New()
httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json")
createServiceTokenResponse, err := api.CallCreateServiceToken(httpClient, api.CreateServiceTokenRequest{
Name: serviceTokenName,
WorkspaceId: workspaceId,
Scopes: permissions,
ExpiresIn: 0,
EncryptedKey: string(workspaceKey),
Iv: base64.StdEncoding.EncodeToString(encryptedDetails.Nonce),
Tag: base64.StdEncoding.EncodeToString(encryptedDetails.AuthTag),
RandomBytes: newWorkspaceEncryptionKeyHexFormat,
Permissions: accessLevels,
})
if err != nil {
fmt.Println(err)
}
serviceToken := createServiceTokenResponse.ServiceToken + "." + newWorkspaceEncryptionKeyHexFormat
if tokenOnly {
fmt.Println(serviceToken)
} else {
printablePermission := []string{}
for _, permission := range permissions {
printablePermission = append(printablePermission, fmt.Sprintf("([environment: %v] [path: %v])", permission.Environment, permission.SecretPath))
}
fmt.Printf("New service token created\n")
fmt.Printf("Name: %v\n", serviceTokenName)
fmt.Printf("Project ID: %v\n", workspaceId)
fmt.Printf("Access type: [%v]\n", strings.Join(accessLevels, ", "))
fmt.Printf("Permission(s): %v\n", strings.Join(printablePermission, ", "))
fmt.Printf("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 Infisical project in .infisical.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")
tokensCmd.AddCommand(tokensCreateCmd)
rootCmd.AddCommand(tokensCmd)
}

@ -14,7 +14,7 @@ import (
var userCmd = &cobra.Command{
Use: "user",
Short: "Used to manage user credentials",
Short: "Used to manage local user credentials",
DisableFlagsInUseLine: true,
Example: "infisical user",
Args: cobra.ExactArgs(0),

@ -684,3 +684,44 @@ func GetEnvelopmentBasedOnGitBranch(workspaceFile models.WorkspaceConfigFile) st
return ""
}
}
func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey string, workspaceId string) ([]byte, error) {
httpClient := resty.New()
httpClient.SetAuthToken(authenticationToken).
SetHeader("Accept", "application/json")
request := api.GetEncryptedWorkspaceKeyRequest{
WorkspaceId: workspaceId,
}
workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request)
if err != nil {
return nil, fmt.Errorf("GetPlainTextWorkspaceKey: unable to retrieve your encrypted workspace key. [err=%v]", err)
}
encryptedWorkspaceKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey)
if err != nil {
return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for encryptedWorkspaceKey [err=%v]", err)
}
encryptedWorkspaceKeySenderPublicKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey)
if err != nil {
return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for encryptedWorkspaceKeySenderPublicKey [err=%v]", err)
}
encryptedWorkspaceKeyNonce, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce)
if err != nil {
return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for encryptedWorkspaceKeyNonce [err=%v]", err)
}
currentUsersPrivateKey, err := base64.StdEncoding.DecodeString(receiverPrivateKey)
if err != nil {
return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for currentUsersPrivateKey [err=%v]", err)
}
if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 {
return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Missing credentials for generating plainTextEncryptionKey")
}
return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), nil
}

@ -0,0 +1,73 @@
---
title: "infisical service-token"
description: "Manage Infisical service tokens"
---
```bash
infisical service-token create --scope=dev:/global --scope=dev:/backend --access-level=read --access-level=write
```
## Description
The Infisical `service-token` command allows you to manage service tokens for a given Infisical project.
With this command can create, view and delete service tokens.
<Accordion title="service-token create" defaultOpen="true">
Use this command to create a service token
```bash
$ infisical service-token create --scope=dev:/backend/** --access-level=read --access-level=write
```
### Flags
<Accordion title="--scope">
```bash
infisical service-token create --scope=dev:/global --scope=dev:/backend/** --access-level=read
```
Use the scope flag to define which environments and paths your service token should be authorized to access.
The value of your scope flag should be in the following `<environment slug>:<path>`.
Here, `environment slug` refers to the slug name of the environment, and `path` indicates the folder path where your secrets are stored.
For specifying multiple scopes, you can use multiple --scope flags.
<Info>
The `path` can be a Glob pattern
</Info>
</Accordion>
<Accordion title="--projectId">
```bash
infisical service-token create --scope=dev:/global --access-level=read --projectId=63cefb15c8d3175601cfa989
```
The project ID you'd like to create the service token for.
By default, the CLI will attempt to use the linked Infisical project in `.infisical.json` generated by `infisical init` command.
</Accordion>
<Accordion title="--name">
```bash
infisical service-token create --scope=dev:/global --access-level=read --name service-token-name
```
Service token name
Default: `Service token generated via CLI`
</Accordion>
<Accordion title="--access-level">
```bash
infisical service-token create --scope=dev:/global --access-level=read --access-level=write
```
The type of access the service token should have. Can be `read` and or `write`
</Accordion>
<Accordion title="--token-only">
```bash
infisical service-token create --scope=dev:/global --access-level=read --access-level=write --token-only
```
When true, only the service token will be printed
Default: `false`
</Accordion>
</Accordion>

@ -169,6 +169,7 @@
"cli/commands/run",
"cli/commands/secrets",
"cli/commands/export",
"cli/commands/service-token",
"cli/commands/vault",
"cli/commands/user",
"cli/commands/reset",

Loading…
Cancel
Save