From a825002b3650b90c0992c022cbec4b1471b7a0f6 Mon Sep 17 00:00:00 2001 From: Pierre Gerbelot Date: Wed, 27 Nov 2024 16:49:59 +0100 Subject: [PATCH] feat(COR-981): add admin command to manage JWT for internal usage --- cmd/admin.go | 28 ++++--- cmd/admin_jw_qovery_usage_create.go | 114 ++++++++++++++++++++++++++++ cmd/admin_jw_qovery_usage_delete.go | 61 +++++++++++++++ cmd/admin_jw_qovery_usage_list.go | 112 +++++++++++++++++++++++++++ cmd/admin_jwt.go | 2 +- cmd/admin_jwt_qovery_usage.go | 28 +++++++ go.mod | 1 + go.sum | 2 + 8 files changed, 335 insertions(+), 13 deletions(-) create mode 100644 cmd/admin_jw_qovery_usage_create.go create mode 100644 cmd/admin_jw_qovery_usage_delete.go create mode 100644 cmd/admin_jw_qovery_usage_list.go create mode 100644 cmd/admin_jwt_qovery_usage.go diff --git a/cmd/admin.go b/cmd/admin.go index 867838b..c7c3f0e 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -5,18 +5,22 @@ import ( ) var ( - jwtKid string - clusterId string - projectId string - lockReason string - orgaErr error - dryRun bool - version string - versionErr error - ageInDay int - execId string - directory string - adminCmd = &cobra.Command{Use: "admin", Hidden: true} + jwtKid string + clusterId string + organizationId string + projectId string + lockReason string + orgaErr error + dryRun bool + version string + versionErr error + ageInDay int + execId string + directory string + rootDns string + additionalClaims string + description string + adminCmd = &cobra.Command{Use: "admin", Hidden: true} ) func init() { diff --git a/cmd/admin_jw_qovery_usage_create.go b/cmd/admin_jw_qovery_usage_create.go new file mode 100644 index 0000000..4dd2869 --- /dev/null +++ b/cmd/admin_jw_qovery_usage_create.go @@ -0,0 +1,114 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/go-jose/go-jose/v4/json" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "io" + "net/http" + "os" + "text/tabwriter" + + "github.com/qovery/qovery-cli/utils" +) + +var ( + adminJwtForQoveryUsageCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a Jwt for Qovery usage", + Run: func(cmd *cobra.Command, args []string) { + createJwtForQoveryUsage() + }, + } +) + +func init() { + adminJwtForQoveryUsageCreateCmd.Flags().StringVarP(&clusterId, "cluster-id", "c", "", "Cluster's id") + adminJwtForQoveryUsageCreateCmd.Flags().StringVarP(&organizationId, "organization-id", "", "", "Organization's id") + adminJwtForQoveryUsageCreateCmd.Flags().StringVarP(&rootDns, "root-dns", "", "", "root dns") + adminJwtForQoveryUsageCreateCmd.Flags().StringVarP(&additionalClaims, "additional-claims", "", "{}", "Additional claims in JSON format (e.g., '{\"key1\":\"value1\",\"key2\":\"value2\"}')") + adminJwtForQoveryUsageCreateCmd.Flags().StringVarP(&description, "description", "d", "", "Description of the JWT") + + adminJwtForQoveryUsageCmd.AddCommand(adminJwtForQoveryUsageCreateCmd) +} + +func createJwtForQoveryUsage() { + utils.CheckAdminUrl() + + tokenType, token, err := utils.GetAccessToken() + if err != nil { + utils.PrintlnError(err) + os.Exit(0) + } + + var claimsMap map[string]string + err = json.Unmarshal([]byte(additionalClaims), &claimsMap) + if err != nil { + fmt.Printf("Error when parsing additional-claims : %v\n", err) + return + } + + type Payload struct { + OrganizationId string `json:"organization_id"` + ClusterId string `json:"cluster_id"` + RootDns string `json:"root_dns"` + AdditionalClaims map[string]string `json:"additional_claims"` + Description string `json:"description"` + } + + var payload, _ = json.Marshal(Payload{ + ClusterId: clusterId, + OrganizationId: organizationId, + RootDns: rootDns, + AdditionalClaims: claimsMap, + Description: description, + }) + + url := fmt.Sprintf("%s/jwts", utils.AdminUrl) + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload)) + if err != nil { + log.Fatal(err) + } + req.Header.Set("Authorization", utils.GetAuthorizationHeaderValue(tokenType, token)) + req.Header.Set("Content-Type", "application/json") + + res, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatal(err) + } + + body, _ := io.ReadAll(res.Body) + if res.StatusCode != http.StatusOK { + utils.PrintlnError(fmt.Errorf("error uploading debug logs: %s %s", res.Status, body)) + return + } + + jwtForQoveryUsage := struct { + KeyId string `json:"key_id"` + Description string `json:"description"` + Jwt string `json:"decrypted_jwt"` + CreatedAt string `json:"created_at"` + }{} + + if err := json.Unmarshal(body, &jwtForQoveryUsage); err != nil { + log.Fatal(err) + } + _, jwtPayload, err := DecodeJWT(jwtForQoveryUsage.Jwt) + if err != nil { + log.Fatal(err) + } + + w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) + + _, _ = fmt.Fprintln(w, "Field\t | Value") + _, _ = fmt.Fprintln(w, "------\t | ------") + + _, _ = fmt.Fprintf(w, "key_id\t | %s\n", jwtForQoveryUsage.KeyId) + _, _ = fmt.Fprintf(w, "description\t | %s\n", jwtForQoveryUsage.Description) + _, _ = fmt.Fprintf(w, "jwt payload\t | %s\n", jwtPayload) + _, _ = fmt.Fprintf(w, "jwt\t | %s\n", jwtForQoveryUsage.Jwt) + _, _ = fmt.Fprintf(w, "created_at\t | %s\n", jwtForQoveryUsage.CreatedAt) + _ = w.Flush() +} diff --git a/cmd/admin_jw_qovery_usage_delete.go b/cmd/admin_jw_qovery_usage_delete.go new file mode 100644 index 0000000..0b913e6 --- /dev/null +++ b/cmd/admin_jw_qovery_usage_delete.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "fmt" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "net/http" + "os" + + "github.com/qovery/qovery-cli/utils" +) + +var ( + adminJwtForQoveryUsageDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a Jwt for Qovery Usage", + Run: func(cmd *cobra.Command, args []string) { + deleteJwtForQoveryUsage() + }, + } +) + +func init() { + adminJwtForQoveryUsageDeleteCmd.Flags().StringVarP(&jwtKid, "kid", "", "", "Cluster's id") + + adminJwtForQoveryUsageCmd.AddCommand(adminJwtForQoveryUsageDeleteCmd) + +} + +func deleteJwtForQoveryUsage() { + utils.CheckAdminUrl() + + tokenType, token, err := utils.GetAccessToken() + if err != nil { + utils.PrintlnError(err) + os.Exit(0) + } + + url := fmt.Sprintf("%s/jwts/%s", utils.AdminUrl, jwtKid) + req, err := http.NewRequest(http.MethodDelete, url, nil) + if err != nil { + log.Fatal(err) + } + req.Header.Set("Authorization", utils.GetAuthorizationHeaderValue(tokenType, token)) + req.Header.Set("Content-Type", "application/json") + + res, err := http.DefaultClient.Do(req) + if res == nil { + utils.PrintlnError(fmt.Errorf("error sending delete HTTP request")) + return + } + + if res.StatusCode != http.StatusNoContent { + utils.PrintlnError(fmt.Errorf("error: %s", res.Status)) + return + } + + if err != nil { + log.Fatal(err) + } +} diff --git a/cmd/admin_jw_qovery_usage_list.go b/cmd/admin_jw_qovery_usage_list.go new file mode 100644 index 0000000..880c8e3 --- /dev/null +++ b/cmd/admin_jw_qovery_usage_list.go @@ -0,0 +1,112 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "github.com/golang-jwt/jwt/v5" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "io" + "net/http" + "os" + "text/tabwriter" + + "github.com/qovery/qovery-cli/utils" +) + +var ( + adminJwtForQoveryUsageListCmd = &cobra.Command{ + Use: "list", + Short: "List Jwt for Qovery usage", + Run: func(cmd *cobra.Command, args []string) { + listJwtsForQoveryUsage() + }, + } +) + +func init() { + adminJwtForQoveryUsageListCmd.Flags() + + adminJwtForQoveryUsageCmd.AddCommand(adminJwtForQoveryUsageListCmd) + +} + +func listJwtsForQoveryUsage() { + utils.CheckAdminUrl() + + tokenType, token, err := utils.GetAccessToken() + if err != nil { + utils.PrintlnError(err) + os.Exit(0) + } + + url := fmt.Sprintf("%s/jwts", utils.AdminUrl) + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + log.Fatal(err) + } + req.Header.Set("Authorization", utils.GetAuthorizationHeaderValue(tokenType, token)) + req.Header.Set("Content-Type", "application/json") + + res, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatal(err) + } + + body, _ := io.ReadAll(res.Body) + if res.StatusCode != http.StatusOK { + utils.PrintlnError(fmt.Errorf("error uploading debug logs: %s %s", res.Status, body)) + return + } + + resp := struct { + Results []struct { + KeyId string `json:"key_id"` + Description string `json:"description"` + Jwt string `json:"decrypted_jwt"` + CreatedAt string `json:"created_at"` + } `json:"results"` + }{} + if err := json.Unmarshal(body, &resp); err != nil { + log.Fatal(err) + } + + w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) + format := "%s\t | %s\t | %s\t | %s\t | %s\n" + _, _ = fmt.Fprintf(w, format, "", "key_id", "descripton", "jwt payload", "created_at") + for idx, jwtForQoveryUsage := range resp.Results { + _, jwtPayload, err := DecodeJWT(jwtForQoveryUsage.Jwt) + if err != nil { + log.Fatal(err) + } + _, _ = fmt.Fprintln(w, "Field\t | Value") + _, _ = fmt.Fprintln(w, "------\t | ------") + + _, _ = fmt.Fprintf(w, "index\t | %s\n", fmt.Sprintf("%d", idx+1)) + _, _ = fmt.Fprintf(w, "key_id\t | %s\n", jwtForQoveryUsage.KeyId) + _, _ = fmt.Fprintf(w, "description\t | %s\n", jwtForQoveryUsage.Description) + _, _ = fmt.Fprintf(w, "jwt payload\t | %s\n", jwtPayload) + _, _ = fmt.Fprintf(w, "jwt\t | %s\n", jwtForQoveryUsage.Jwt) + _, _ = fmt.Fprintf(w, "created_at\t | %s\n", jwtForQoveryUsage.CreatedAt) + } + _ = w.Flush() +} + +func DecodeJWT(tokenString string) (string, string, error) { + token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{}) + if err != nil { + return "", "", fmt.Errorf("failed to parse token: %w", err) + } + + headerJSON, err := json.Marshal(token.Header) + if err != nil { + return "", "", fmt.Errorf("failed to marshal header: %w", err) + } + + claimsJSON, err := json.Marshal(token.Claims) + if err != nil { + return "", "", fmt.Errorf("failed to marshal claims: %w", err) + } + + return string(headerJSON), string(claimsJSON), nil +} diff --git a/cmd/admin_jwt.go b/cmd/admin_jwt.go index ef92aba..5757e16 100644 --- a/cmd/admin_jwt.go +++ b/cmd/admin_jwt.go @@ -11,7 +11,7 @@ import ( var ( adminJwtCmd = &cobra.Command{ Use: "jwt", - Short: "Manage clusters", + Short: "Manage JWT associated to clusters", Run: func(cmd *cobra.Command, args []string) { utils.Capture(cmd) diff --git a/cmd/admin_jwt_qovery_usage.go b/cmd/admin_jwt_qovery_usage.go new file mode 100644 index 0000000..85199cb --- /dev/null +++ b/cmd/admin_jwt_qovery_usage.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" + + "github.com/qovery/qovery-cli/utils" +) + +var ( + adminJwtForQoveryUsageCmd = &cobra.Command{ + Use: "jwt-qovery-usage", + Short: "Manage JWT for qovery usage ", + Run: func(cmd *cobra.Command, args []string) { + utils.Capture(cmd) + + if len(args) == 0 { + _ = cmd.Help() + os.Exit(0) + } + }, + } +) + +func init() { + adminCmd.AddCommand(adminJwtForQoveryUsageCmd) +} diff --git a/go.mod b/go.mod index 22140b1..29ab549 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gookit/color v1.5.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 3f0ecbb..09649ef 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,8 @@ github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=