Merge pull request #3441 from gravitl/nmctl-access-token

feat: add support for user access tokens in nmctl
This commit is contained in:
Aceix 2025-05-20 03:46:24 +00:00 committed by GitHub
parent f9bc3a5386
commit 506f73ebb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 185 additions and 7 deletions

View file

@ -0,0 +1,43 @@
package access_token
import (
"time"
"github.com/gravitl/netmaker/cli/functions"
"github.com/gravitl/netmaker/schema"
"github.com/spf13/cobra"
)
var accessTokenCreateCmd = &cobra.Command{
Use: "create [token-name]",
Short: "Create an access token",
Long: `Create an access token for a user`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
userName, _ := cmd.Flags().GetString("user")
expiresAt, _ := cmd.Flags().GetString("expires")
accessToken := &schema.UserAccessToken{}
accessToken.Name = args[0]
accessToken.UserName = userName
expTime := time.Now().Add(time.Hour * 24 * 365) // default to 1 year
if expiresAt != "" {
var err error
expTime, err = time.Parse(time.RFC3339, expiresAt)
if err != nil {
cmd.PrintErrf("Invalid expiration time format. Please use RFC3339 format (e.g. 2024-01-01T00:00:00Z). Using default 1 year.\n")
}
}
accessToken.ExpiresAt = expTime
functions.PrettyPrint(functions.CreateAccessToken(accessToken))
},
}
func init() {
accessTokenCreateCmd.Flags().String("user", "", "Username to create token for")
accessTokenCreateCmd.Flags().String("expires", "", "Expiration time for the token in RFC3339 format (e.g. 2024-01-01T00:00:00Z). Defaults to 1 year from now.")
accessTokenCreateCmd.MarkFlagRequired("user")
rootCmd.AddCommand(accessTokenCreateCmd)
}

View file

@ -0,0 +1,23 @@
package access_token
import (
"fmt"
"github.com/gravitl/netmaker/cli/functions"
"github.com/spf13/cobra"
)
var accessTokenDeleteCmd = &cobra.Command{
Use: "delete [ACCESS TOKEN ID]",
Short: "Delete an access token",
Long: `Delete an access token by ID`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
functions.DeleteAccessToken(args[0])
fmt.Println("Access token deleted successfully")
},
}
func init() {
rootCmd.AddCommand(accessTokenDeleteCmd)
}

View file

@ -0,0 +1,20 @@
package access_token
import (
"github.com/gravitl/netmaker/cli/functions"
"github.com/spf13/cobra"
)
var accessTokenGetCmd = &cobra.Command{
Use: "get [USERNAME]",
Short: "Get a user's access token",
Long: `Get a user's access token`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
functions.PrettyPrint(functions.GetAccessToken(args[0]))
},
}
func init() {
rootCmd.AddCommand(accessTokenGetCmd)
}

View file

@ -0,0 +1,28 @@
package access_token
import (
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "access_token",
Short: "Manage Netmaker user access tokens",
Long: `Manage a Netmaker user's access tokens. This command allows you to create, delete, and list access tokens for a user.`,
}
// GetRoot returns the root subcommand
func GetRoot() *cobra.Command {
return rootCmd
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}

View file

@ -17,6 +17,7 @@ var (
sso bool
tenantId string
saas bool
authToken string
)
var contextSetCmd = &cobra.Command{
@ -30,13 +31,14 @@ var contextSetCmd = &cobra.Command{
Username: username,
Password: password,
MasterKey: masterKey,
AuthToken: authToken,
SSO: sso,
TenantId: tenantId,
Saas: saas,
}
if !ctx.Saas {
if ctx.Username == "" && ctx.MasterKey == "" && !ctx.SSO {
log.Fatal("Either username/password or master key is required")
if ctx.Username == "" && ctx.MasterKey == "" && !ctx.SSO && ctx.AuthToken == "" {
log.Fatal("Either username/password or master key or auth token is required")
cmd.Usage()
}
if ctx.Endpoint == "" {
@ -49,8 +51,8 @@ var contextSetCmd = &cobra.Command{
cmd.Usage()
}
ctx.Endpoint = fmt.Sprintf(functions.TenantUrlTemplate, tenantId)
if ctx.Username == "" && ctx.Password == "" && !ctx.SSO {
log.Fatal("Username/password is required for non-SSO SaaS contexts")
if ctx.Username == "" && ctx.Password == "" && ctx.AuthToken == "" && !ctx.SSO {
log.Fatal("Username/password or authtoken is required for non-SSO SaaS contexts")
cmd.Usage()
}
}
@ -62,6 +64,7 @@ func init() {
contextSetCmd.Flags().StringVar(&endpoint, "endpoint", "", "Endpoint of the API Server")
contextSetCmd.Flags().StringVar(&username, "username", "", "Username")
contextSetCmd.Flags().StringVar(&password, "password", "", "Password")
contextSetCmd.Flags().StringVar(&authToken, "auth_token", "", "Auth Token")
contextSetCmd.MarkFlagsRequiredTogether("username", "password")
contextSetCmd.Flags().BoolVar(&sso, "sso", false, "Login via Single Sign On (SSO)?")
contextSetCmd.Flags().StringVar(&masterKey, "master_key", "", "Master Key")

View file

@ -1,9 +1,9 @@
package cmd
import (
"github.com/gravitl/netmaker/cli/cmd/gateway"
"os"
"github.com/gravitl/netmaker/cli/cmd/access_token"
"github.com/gravitl/netmaker/cli/cmd/acl"
"github.com/gravitl/netmaker/cli/cmd/commons"
"github.com/gravitl/netmaker/cli/cmd/context"
@ -11,12 +11,14 @@ import (
"github.com/gravitl/netmaker/cli/cmd/enrollment_key"
"github.com/gravitl/netmaker/cli/cmd/ext_client"
"github.com/gravitl/netmaker/cli/cmd/failover"
"github.com/gravitl/netmaker/cli/cmd/gateway"
"github.com/gravitl/netmaker/cli/cmd/host"
"github.com/gravitl/netmaker/cli/cmd/metrics"
"github.com/gravitl/netmaker/cli/cmd/network"
"github.com/gravitl/netmaker/cli/cmd/node"
"github.com/gravitl/netmaker/cli/cmd/server"
"github.com/gravitl/netmaker/cli/cmd/user"
"github.com/spf13/cobra"
)
@ -57,4 +59,5 @@ func init() {
rootCmd.AddCommand(enrollment_key.GetRoot())
rootCmd.AddCommand(failover.GetRoot())
rootCmd.AddCommand(gateway.GetRoot())
rootCmd.AddCommand(access_token.GetRoot())
}

View file

@ -0,0 +1,58 @@
package functions
import (
"encoding/json"
"log"
"net/http"
"github.com/gravitl/netmaker/models"
"github.com/gravitl/netmaker/schema"
)
// CreateAccessToken - creates an access token for a user
func CreateAccessToken(payload *schema.UserAccessToken) *models.SuccessfulUserLoginResponse {
res := request[models.SuccessResponse](http.MethodPost, "/api/v1/users/access_token", payload)
if res.Code != http.StatusOK {
log.Fatalf("Error creating access token: %s", res.Message)
}
var token models.SuccessfulUserLoginResponse
responseBytes, err := json.Marshal(res.Response)
if err != nil {
log.Fatalf("Error marshaling response: %v", err)
}
if err := json.Unmarshal(responseBytes, &token); err != nil {
log.Fatalf("Error unmarshaling token: %v", err)
}
return &token
}
// GetAccessToken - fetch all access tokens per user
func GetAccessToken(userName string) []schema.UserAccessToken {
res := request[models.SuccessResponse](http.MethodGet, "/api/v1/users/access_token?username="+userName, nil)
if res.Code != http.StatusOK {
log.Fatalf("Error getting access token: %s", res.Message)
}
var tokens []schema.UserAccessToken
responseBytes, err := json.Marshal(res.Response)
if err != nil {
log.Fatalf("Error marshaling response: %v", err)
}
if err := json.Unmarshal(responseBytes, &tokens); err != nil {
log.Fatalf("Error unmarshaling tokens: %v", err)
}
return tokens
}
// DeleteAccessToken - delete an access token
func DeleteAccessToken(id string) {
res := request[models.SuccessResponse](http.MethodDelete, "/api/v1/users/access_token?id="+id, nil)
if res.Code != http.StatusOK {
log.Fatalf("Error deleting access token: %s", res.Message)
}
}

View file

@ -192,7 +192,7 @@ retry:
body := new(T)
if len(resBodyBytes) > 0 {
if err := json.Unmarshal(resBodyBytes, body); err != nil {
log.Fatalf("Error unmarshalling JSON: %s", err)
log.Fatalf("Error unmarshalling JSON: %s %s", err, string(resBodyBytes))
}
}
return body

View file

@ -163,7 +163,7 @@ func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) {
}
err := a.Get(r.Context())
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("id is required"), "badrequest"))
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("token does not exist"), "badrequest"))
return
}
caller, err := logic.GetUser(r.Header.Get("user"))