diff --git a/cli/cmd/access_token/create.go b/cli/cmd/access_token/create.go new file mode 100644 index 00000000..6500782c --- /dev/null +++ b/cli/cmd/access_token/create.go @@ -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) +} diff --git a/cli/cmd/access_token/delete.go b/cli/cmd/access_token/delete.go new file mode 100644 index 00000000..d935e84d --- /dev/null +++ b/cli/cmd/access_token/delete.go @@ -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) +} diff --git a/cli/cmd/access_token/get.go b/cli/cmd/access_token/get.go new file mode 100644 index 00000000..d6f4a6a3 --- /dev/null +++ b/cli/cmd/access_token/get.go @@ -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) +} diff --git a/cli/cmd/access_token/root.go b/cli/cmd/access_token/root.go new file mode 100644 index 00000000..34239b77 --- /dev/null +++ b/cli/cmd/access_token/root.go @@ -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) + } +} diff --git a/cli/cmd/context/set.go b/cli/cmd/context/set.go index ba3f4de6..d206d57e 100644 --- a/cli/cmd/context/set.go +++ b/cli/cmd/context/set.go @@ -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") diff --git a/cli/cmd/root.go b/cli/cmd/root.go index ebe9c957..19f24b60 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -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()) } diff --git a/cli/functions/access_tokens.go b/cli/functions/access_tokens.go new file mode 100644 index 00000000..0e5b236d --- /dev/null +++ b/cli/functions/access_tokens.go @@ -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) + } +} diff --git a/cli/functions/http_client.go b/cli/functions/http_client.go index fdb4210a..d25cee4f 100644 --- a/cli/functions/http_client.go +++ b/cli/functions/http_client.go @@ -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 diff --git a/controllers/user.go b/controllers/user.go index 5c8e0594..762b066f 100644 --- a/controllers/user.go +++ b/controllers/user.go @@ -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"))