mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-10 07:58:15 -05:00
add create service token to cli + docs for it
This commit is contained in:
@@ -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"`
|
||||
}
|
||||
|
||||
181
cli/packages/cmd/tokens.go
Normal file
181
cli/packages/cmd/tokens.go
Normal file
@@ -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
|
||||
}
|
||||
|
||||
73
docs/cli/commands/service-token.mdx
Normal file
73
docs/cli/commands/service-token.mdx
Normal file
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user