mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 19:28:09 +00:00
add create service token to cli + docs for it
This commit is contained in:
+21
-1
@@ -25,7 +25,7 @@ func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncrypted
|
|||||||
}
|
}
|
||||||
|
|
||||||
if response.IsError() {
|
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
|
return result, nil
|
||||||
@@ -339,3 +339,23 @@ func CallGetSingleSecretByNameV3(httpClient *resty.Client, request CreateSecretV
|
|||||||
|
|
||||||
return nil
|
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"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
} `json:"secrets"`
|
} `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{
|
var userCmd = &cobra.Command{
|
||||||
Use: "user",
|
Use: "user",
|
||||||
Short: "Used to manage user credentials",
|
Short: "Used to manage local user credentials",
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
Example: "infisical user",
|
Example: "infisical user",
|
||||||
Args: cobra.ExactArgs(0),
|
Args: cobra.ExactArgs(0),
|
||||||
|
|||||||
@@ -684,3 +684,44 @@ func GetEnvelopmentBasedOnGitBranch(workspaceFile models.WorkspaceConfigFile) st
|
|||||||
return ""
|
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/run",
|
||||||
"cli/commands/secrets",
|
"cli/commands/secrets",
|
||||||
"cli/commands/export",
|
"cli/commands/export",
|
||||||
|
"cli/commands/service-token",
|
||||||
"cli/commands/vault",
|
"cli/commands/vault",
|
||||||
"cli/commands/user",
|
"cli/commands/user",
|
||||||
"cli/commands/reset",
|
"cli/commands/reset",
|
||||||
|
|||||||
Reference in New Issue
Block a user