From 79ff70576339655d49fb99aea7fd1830fa3e0acd Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Mon, 27 Mar 2023 14:56:34 -0400 Subject: [PATCH] fixes #945 adds ext jwt signer support --- ziti/cmd/edge/create.go | 6 + ziti/cmd/edge/create_ca.go | 36 ++--- ziti/cmd/edge/create_extjwtsigner.go | 161 +++++++++++++++++++++ ziti/cmd/edge/list.go | 95 ++++++++++++- ziti/cmd/edge/update.go | 1 + ziti/cmd/edge/update_ca.go | 12 +- ziti/cmd/edge/update_extjwtsigner.go | 200 +++++++++++++++++++++++++++ 7 files changed, 479 insertions(+), 32 deletions(-) create mode 100644 ziti/cmd/edge/create_extjwtsigner.go create mode 100644 ziti/cmd/edge/update_extjwtsigner.go diff --git a/ziti/cmd/edge/create.go b/ziti/cmd/edge/create.go index 4bbc33b43..b86ab5848 100644 --- a/ziti/cmd/edge/create.go +++ b/ziti/cmd/edge/create.go @@ -26,6 +26,11 @@ import ( "github.com/spf13/cobra" ) +func Ptr[T any](sub T) *T { + return &sub +} + +// newCreateCmd creates a command object for the "list" command // newCreateCmd creates a command object for the "create" command func newCreateCmd(out io.Writer, errOut io.Writer) *cobra.Command { cmd := &cobra.Command{ @@ -51,6 +56,7 @@ func newCreateCmd(out io.Writer, errOut io.Writer) *cobra.Command { cmd.AddCommand(newCreateServicePolicyCmd(out, errOut)) cmd.AddCommand(newCreateTerminatorCmd(out, errOut)) cmd.AddCommand(newCreateTransitRouterCmd(out, errOut)) + cmd.AddCommand(newCreateExtJwtSignerCmd(out, errOut)) return cmd } diff --git a/ziti/cmd/edge/create_ca.go b/ziti/cmd/edge/create_ca.go index 0d84e53d8..d4fbc738a 100644 --- a/ziti/cmd/edge/create_ca.go +++ b/ziti/cmd/edge/create_ca.go @@ -39,21 +39,21 @@ func newCreateCaCmd(out io.Writer, errOut io.Writer) *cobra.Command { options := &createCaOptions{ EntityOptions: api.NewEntityOptions(out, errOut), Ca: rest_model.CaCreate{ - CertPem: S(""), + CertPem: Ptr(""), ExternalIDClaim: &rest_model.ExternalIDClaim{ - Index: I64(0), - Location: S(""), - Matcher: S(""), - MatcherCriteria: S(""), - Parser: S(""), - ParserCriteria: S(""), + Index: Ptr(int64(0)), + Location: Ptr(""), + Matcher: Ptr(""), + MatcherCriteria: Ptr(""), + Parser: Ptr(""), + ParserCriteria: Ptr(""), }, IdentityNameFormat: "", IdentityRoles: []string{}, - IsAuthEnabled: B(false), - IsAutoCaEnrollmentEnabled: B(false), - IsOttCaEnrollmentEnabled: B(false), - Name: S(""), + IsAuthEnabled: Ptr(false), + IsAutoCaEnrollmentEnabled: Ptr(false), + IsOttCaEnrollmentEnabled: Ptr(false), + Name: Ptr(""), Tags: &rest_model.Tags{ SubTags: map[string]interface{}{}, }, @@ -75,7 +75,7 @@ func newCreateCaCmd(out io.Writer, errOut io.Writer) *cobra.Command { pemBytes, err := os.ReadFile(caFile) - options.Ca.CertPem = S(string(pemBytes)) + options.Ca.CertPem = Ptr(string(pemBytes)) if err != nil { return fmt.Errorf("could not read CA certificate file: %s", err) @@ -114,18 +114,6 @@ func newCreateCaCmd(out io.Writer, errOut io.Writer) *cobra.Command { return cmd } -func S(s string) *string { - return &s -} - -func B(b bool) *bool { - return &b -} - -func I64(i int64) *int64 { - return &i -} - func runCreateCa(options *createCaOptions) (err error) { managementClient, err := util.NewEdgeManagementClient(options) diff --git a/ziti/cmd/edge/create_extjwtsigner.go b/ziti/cmd/edge/create_extjwtsigner.go new file mode 100644 index 000000000..feedc3dc6 --- /dev/null +++ b/ziti/cmd/edge/create_extjwtsigner.go @@ -0,0 +1,161 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package edge + +import ( + "fmt" + "github.com/go-openapi/strfmt" + "github.com/openziti/edge-api/rest_management_api_client/external_jwt_signer" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/ziti/ziti/cmd/api" + cmdhelper "github.com/openziti/ziti/ziti/cmd/helpers" + "github.com/openziti/ziti/ziti/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "io" + "os" +) + +type createExtJwtSignerOptions struct { + api.EntityOptions + ExtJwtSigner rest_model.ExternalJWTSignerCreate + CertFilePath string + JwksEndpoint string +} + +// newCreateExtJwtSignerCmd creates the 'edge controller create ca local' command for the given entity type +func newCreateExtJwtSignerCmd(out io.Writer, errOut io.Writer) *cobra.Command { + jwksEndpoint := strfmt.URI("") + options := &createExtJwtSignerOptions{ + EntityOptions: api.NewEntityOptions(out, errOut), + ExtJwtSigner: rest_model.ExternalJWTSignerCreate{ + Audience: Ptr(""), + CertPem: Ptr(""), + ClaimsProperty: Ptr(""), + Enabled: Ptr(true), + ExternalAuthURL: Ptr(""), + Issuer: Ptr(""), + JwksEndpoint: &jwksEndpoint, + Kid: Ptr(""), + Name: Ptr(""), + Tags: &rest_model.Tags{SubTags: map[string]interface{}{}}, + UseExternalID: Ptr(false), + }, + } + + cmd := &cobra.Command{ + Use: "ext-jwt-signer (-u |-p |-f ) [-a -c -xe]", + Short: "creates an external JWT signer managed by the Ziti Edge Controller", + Long: "creates an external JWT signer managed by the Ziti Edge Controller", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("requires 1 arg, received %d", len(args)) + } + + options.ExtJwtSigner.Name = &args[0] + + return nil + + }, + Run: func(cmd *cobra.Command, args []string) { + options.Cmd = cmd + options.Args = args + err := runCreateExtJwtSigner(options) + + cmdhelper.CheckErr(err) + }, + SuggestFor: []string{}, + } + + // allow interspersing positional args and flags + cmd.Flags().SetInterspersed(true) + cmd.Flags().BoolVarP(options.ExtJwtSigner.UseExternalID, "external-id", "x", true, "Matches identity external ids rather than internal ids") + cmd.Flags().BoolVarP(options.ExtJwtSigner.Enabled, "enabled", "e", true, "Enable this entity") + cmd.Flags().StringVarP(&options.JwksEndpoint, "jwks-endpoint", "u", "", "A valid URI for a target JWKS endpoint, not usable with -p or -f") + cmd.Flags().StringVarP(options.ExtJwtSigner.ClaimsProperty, "claims-property", "c", "sub", "The JWT property matched to identity internal/external ids") + cmd.Flags().StringVarP(options.ExtJwtSigner.Audience, "audience", "a", "", "The expected audience of the incoming JWTs") + cmd.Flags().StringVarP(options.ExtJwtSigner.CertPem, "cert-pem", "p", "", "A public certificate PEM for the signer, not usable with -u, -f") + cmd.Flags().StringVarP(&options.CertFilePath, "cert-file", "f", "", "A public certificate PEM file, not usable with -u, -p") + cmd.Flags().StringVarP(options.ExtJwtSigner.ExternalAuthURL, "external-auth-url", "y", "", "The URL that users are directed to obtain a JWT") + cmd.Flags().StringVarP(options.ExtJwtSigner.Kid, "kid", "k", "", "The KID for the signer, required if using -p or -f") + options.AddCommonFlags(cmd) + + return cmd +} + +func runCreateExtJwtSigner(options *createExtJwtSignerOptions) (err error) { + managementClient, err := util.NewEdgeManagementClient(options) + + if err != nil { + return err + } + + hasJwks := options.JwksEndpoint != "" + hasCertPem := options.ExtJwtSigner.CertPem != nil || *options.ExtJwtSigner.CertPem == "" + hasCertPath := options.CertFilePath != "" + + if (hasJwks && (hasCertPem || hasCertPath)) || (hasCertPem && (hasJwks || hasCertPath)) || (hasCertPath && (hasJwks || hasCertPem)) { + return errors.New("must specify only one certificate source (JWKS, cert file, or inline cert PEM") + } + + if hasCertPem || hasCertPath && (options.ExtJwtSigner.Kid == nil || *options.ExtJwtSigner.Kid == "") { + return errors.New("must specify a KID if using a certificate file or inline PEM") + } + + if options.ExtJwtSigner.Audience == nil || *options.ExtJwtSigner.Audience == "" { + return errors.New("audience must be specified") + } + + if options.ExtJwtSigner.ClaimsProperty != nil && *options.ExtJwtSigner.ClaimsProperty == "" { + return errors.New("claims property must not be an empty string") + } + + if options.JwksEndpoint != "" { + jwks := strfmt.URI(options.JwksEndpoint) + options.ExtJwtSigner.JwksEndpoint = &jwks + } + + if hasCertPath { + pem, err := os.ReadFile(options.CertFilePath) + if err != nil { + return fmt.Errorf("could not read certificate file path %s: %w", options.CertFilePath, err) + } + pemStr := string(pem) + options.ExtJwtSigner.CertPem = &pemStr + } + + for k, v := range options.GetTags() { + options.ExtJwtSigner.Tags.SubTags[k] = v + } + + params := external_jwt_signer.NewCreateExternalJWTSignerParams() + params.ExternalJWTSigner = &options.ExtJwtSigner + + resp, err := managementClient.ExternalJWTSigner.CreateExternalJWTSigner(params, nil) + + if err != nil { + return util.WrapIfApiError(err) + } + + checkId := resp.GetPayload().Data.ID + + if _, err = fmt.Fprintf(options.Out, "%v\n", checkId); err != nil { + panic(err) + } + + return err +} diff --git a/ziti/cmd/edge/list.go b/ziti/cmd/edge/list.go index 1561393c9..2909fd85b 100644 --- a/ziti/cmd/edge/list.go +++ b/ziti/cmd/edge/list.go @@ -22,6 +22,7 @@ import ( "github.com/go-openapi/runtime" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" + "github.com/openziti/edge-api/rest_management_api_client/external_jwt_signer" "github.com/openziti/foundation/v2/stringz" "github.com/openziti/ziti/ziti/cmd/api" "github.com/openziti/ziti/ziti/cmd/common" @@ -67,6 +68,7 @@ func newListCmd(out io.Writer, errOut io.Writer) *cobra.Command { cmd.AddCommand(newListEdgeRoutersCmd(newOptions())) cmd.AddCommand(newListCmdForEntityType("edge-router-policies", runListEdgeRouterPolicies, newOptions(), "erps")) cmd.AddCommand(newListCmdForEntityType("enrollments", runListEnrollments, newOptions())) + cmd.AddCommand(newListCmdForEntityType("ext-jwt-signers", runListExtJwtSigners, newOptions())) cmd.AddCommand(newListCmdForEntityType("terminators", runListTerminators, newOptions())) cmd.AddCommand(newListIdentitiesCmd(newOptions())) cmd.AddCommand(newListServicesCmd(newOptions())) @@ -478,15 +480,104 @@ func outputAuthenticators(o *api.Options, children []*gabs.Container, pagingInfo return nil } +func runListExtJwtSigners(options *api.Options) error { + client, err := util.NewEdgeManagementClient(options) + + if err != nil { + return err + } + + var filter *string = nil + + if len(options.Args) > 0 { + filter = &options.Args[0] + } + + params := external_jwt_signer.NewListExternalJWTSignersParams() + params.Filter = filter + + result, err := client.ExternalJWTSigner.ListExternalJWTSigners(params, nil) + + if err != nil { + return util.WrapIfApiError(err) + } + + if options.OutputJSONResponse { + return nil + } + + payload := result.GetPayload() + + if payload == nil { + return errors.New("unexpected empty response payload") + } + + outTable := table.NewWriter() + outTable.SetStyle(table.StyleRounded) + + rowConfigAutoMerge := table.RowConfig{AutoMerge: true, AutoMergeAlign: text.AlignLeft} + + outTable.AppendHeader(table.Row{"ID", "Name", "Enabled", "Audience", "Type", "ClaimsProp", "UseExternalId", "Config", "Config"}, rowConfigAutoMerge) + + for _, entity := range result.GetPayload().Data { + id := *entity.ID + name := *entity.Name + audience := *entity.Audience + isEnabled := *entity.Enabled + claimsProperty := *entity.ClaimsProperty + useExternalId := *entity.UseExternalID + + if entity.JwksEndpoint != nil { + confType := "JWKS" + outTable.AppendRow(table.Row{id, name, isEnabled, audience, confType, claimsProperty, useExternalId, "JWKS", entity.JwksEndpoint.String()}) + } else { + confType := "CERT" + outTable.AppendRow(table.Row{id, name, isEnabled, audience, confType, claimsProperty, useExternalId, "Fingerprint", *entity.Fingerprint}) + } + + } + + pagingInfo := newPagingInfo(payload.Meta) + api.RenderTable(options, outTable, pagingInfo) + + _, _ = fmt.Fprint(options.Out, "\nFlags: (V) Verified, (A) AutoCa Enrollment, (O) OttCA Enrollment, (E) Authentication Enabled\n\n") + + return nil +} + +func outputEnrollments(o *api.Options, children []*gabs.Container, pagingInfo *api.Paging) error { + if o.OutputJSONResponse { + return nil + } + + t := table.NewWriter() + t.SetStyle(table.StyleRounded) + t.AppendHeader(table.Row{"ID", "Method", "Identity Id", "Identity Name", "Expires At", "Token", "JWT"}) + + for _, entity := range children { + id, _ := entity.Path("id").Data().(string) + method := entity.Path("method").Data().(string) + identityId := entity.Path("identityId").Data().(string) + identityName := entity.Path("identity.name").Data().(string) + expiresAt := entity.Path("expiresAt").Data().(string) + token := entity.Path("token").Data().(string) + jwt := "See json" + + t.AppendRow(table.Row{id, method, identityId, identityName, expiresAt, token, jwt}) + } + api.RenderTable(o, t, pagingInfo) + return nil +} + func runListEnrollments(o *api.Options) error { - children, pagingInfo, err := listEntitiesWithOptions("enrollments", o) + children, pagingInfo, err := listEntitiesWithOptions("ext-jwt-signers", o) if err != nil { return err } return outputEnrollments(o, children, pagingInfo) } -func outputEnrollments(o *api.Options, children []*gabs.Container, pagingInfo *api.Paging) error { +func outputExtJwtSigners(o *api.Options, children []*gabs.Container, pagingInfo *api.Paging) error { if o.OutputJSONResponse { return nil } diff --git a/ziti/cmd/edge/update.go b/ziti/cmd/edge/update.go index 903377156..883ade283 100644 --- a/ziti/cmd/edge/update.go +++ b/ziti/cmd/edge/update.go @@ -51,6 +51,7 @@ func newUpdateCmd(out io.Writer, errOut io.Writer) *cobra.Command { cmd.AddCommand(newUpdateServiceEdgeRouterPolicyCmd(out, errOut)) cmd.AddCommand(newUpdateTerminatorCmd(out, errOut)) cmd.AddCommand(newUpdatePostureCheckCmd(out, errOut)) + cmd.AddCommand(newUpdateExtJwtSignerCmd(out, errOut)) return cmd } diff --git a/ziti/cmd/edge/update_ca.go b/ziti/cmd/edge/update_ca.go index fbb2f93b0..fd2ee0fc0 100644 --- a/ziti/cmd/edge/update_ca.go +++ b/ziti/cmd/edge/update_ca.go @@ -52,12 +52,12 @@ func newUpdateCaCmd(out io.Writer, errOut io.Writer) *cobra.Command { EntityOptions: api.NewEntityOptions(out, errOut), verify: false, externalIDClaim: rest_model.ExternalIDClaimPatch{ - Index: I64(0), - Location: S(""), - Matcher: S(""), - MatcherCriteria: S(""), - Parser: S(""), - ParserCriteria: S(""), + Index: Ptr(int64(0)), + Location: Ptr(""), + Matcher: Ptr(""), + MatcherCriteria: Ptr(""), + Parser: Ptr(""), + ParserCriteria: Ptr(""), }, } diff --git a/ziti/cmd/edge/update_extjwtsigner.go b/ziti/cmd/edge/update_extjwtsigner.go new file mode 100644 index 000000000..529617279 --- /dev/null +++ b/ziti/cmd/edge/update_extjwtsigner.go @@ -0,0 +1,200 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package edge + +import ( + "fmt" + "github.com/go-openapi/strfmt" + "github.com/openziti/edge-api/rest_management_api_client/external_jwt_signer" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/ziti/ziti/cmd/api" + "github.com/openziti/ziti/ziti/cmd/helpers" + "github.com/openziti/ziti/ziti/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "io" +) + +type updateExtJwtOptions struct { + api.EntityOptions + ExtJwtSigner rest_model.ExternalJWTSignerPatch + nameOrId string + newName string + JwksEndpoint string +} + +// newUpdateExtJwtSignerCmd creates the 'edge controller update authenticator' command +func newUpdateExtJwtSignerCmd(out io.Writer, errOut io.Writer) *cobra.Command { + jwksEndpoint := strfmt.URI("") + + options := updateExtJwtOptions{ + EntityOptions: api.NewEntityOptions(out, errOut), + ExtJwtSigner: rest_model.ExternalJWTSignerPatch{ + Audience: Ptr(""), + CertPem: Ptr(""), + ClaimsProperty: Ptr(""), + Enabled: Ptr(true), + ExternalAuthURL: Ptr(""), + Issuer: Ptr(""), + JwksEndpoint: &jwksEndpoint, + Kid: Ptr(""), + Name: Ptr(""), + Tags: &rest_model.Tags{SubTags: map[string]interface{}{}}, + UseExternalID: Ptr(false), + }, + nameOrId: "", + newName: "", + JwksEndpoint: "", + } + + cmd := &cobra.Command{ + Use: "ext-jwt-signer [-u |-p |-f ] [-n -a -c -xe]", + Short: "updates an external jwt signer managed by the Ziti Edge Controller", + Long: "updates an external jwt signer managed by the Ziti Edge Controller", + Args: func(cmd *cobra.Command, args []string) error { + switch { + case len(args) == 0: + return fmt.Errorf("id or name is required") + case len(args) > 1: + return fmt.Errorf("too many positional arguments") + } + + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + options.Cmd = cmd + options.Args = args + err := runUpdateExtJwtSigner(options) + + helpers.CheckErr(err) + }, + SuggestFor: []string{}, + } + + cmd.Flags().SetInterspersed(true) + + cmd.Flags().StringVarP(&options.newName, "name", "n", "", "The name to give the CA") + + options.AddCommonFlags(cmd) + cmd.Flags().BoolVarP(options.ExtJwtSigner.UseExternalID, "use-external-id", "x", true, "Matches identity external ids rather than internal ids") + cmd.Flags().BoolVarP(options.ExtJwtSigner.Enabled, "enabled", "e", true, "Enable this entity") + cmd.Flags().StringVarP(&options.JwksEndpoint, "jwks-endpoint", "u", "", "A valid URI for a target JWKS endpoint, not usable with -p or -f") + cmd.Flags().StringVarP(options.ExtJwtSigner.ClaimsProperty, "claims-property", "c", "sub", "The JWT property matched to identity internal/external ids") + cmd.Flags().StringVarP(options.ExtJwtSigner.Audience, "audience", "a", "", "The expected audience of the incoming JWTs") + cmd.Flags().StringVarP(options.ExtJwtSigner.CertPem, "cert-pem", "p", "", "A public certificate PEM for the signer, not usable with -u, -f") + cmd.Flags().StringVarP(options.ExtJwtSigner.CertPem, "cert-file", "f", "", "A public certificate PEM file, not usable with -u, -p") + cmd.Flags().StringVarP(options.ExtJwtSigner.ExternalAuthURL, "external-auth-url", "y", "", "The URL that users are directed to obtain a JWT") + cmd.Flags().StringVarP(options.ExtJwtSigner.Kid, "kid", "k", "", "The KID for the signer, required if using -p or -f") + return cmd +} + +func runUpdateExtJwtSigner(options updateExtJwtOptions) error { + id, err := mapNameToID("external-jwt-signers", options.nameOrId, options.Options) + + if err != nil { + return err + } + + client, err := util.NewEdgeManagementClient(&options) + + if err != nil { + return err + } + + changed := false + + if options.Cmd.Flag("name").Changed { + options.ExtJwtSigner.Name = &options.newName + changed = true + } else { + options.ExtJwtSigner.Name = nil + } + + if options.Cmd.Flag("use-external-id").Changed { + changed = true + } else { + options.ExtJwtSigner.UseExternalID = nil + } + + if options.Cmd.Flag("enabled").Changed { + changed = true + } else { + options.ExtJwtSigner.Enabled = nil + } + + if options.Cmd.Flag("jwks-endpoint").Changed { + changed = true + } else { + options.ExtJwtSigner.JwksEndpoint = nil + } + + if options.Cmd.Flag("claims-property").Changed { + changed = true + } else { + options.ExtJwtSigner.ClaimsProperty = nil + } + + if options.Cmd.Flag("audience").Changed { + changed = true + } else { + options.ExtJwtSigner.Audience = nil + } + + if options.Cmd.Flag("cert-pem").Changed || options.Cmd.Flag("cert-file").Changed { + changed = true + } else { + options.ExtJwtSigner.CertPem = nil + } + + if options.Cmd.Flag("external-auth-url").Changed { + changed = true + } else { + options.ExtJwtSigner.ExternalAuthURL = nil + } + + if options.Cmd.Flag("kid").Changed { + changed = true + } else { + options.ExtJwtSigner.Kid = nil + } + + if options.TagsProvided() { + options.ExtJwtSigner.Tags = &rest_model.Tags{ + SubTags: rest_model.SubTags{}, + } + for k, v := range options.GetTags() { + options.ExtJwtSigner.Tags.SubTags[k] = v + } + changed = true + } + + if !changed { + return errors.New("no values changed") + } + + params := external_jwt_signer.NewPatchExternalJWTSignerParams() + params.ExternalJWTSigner = &options.ExtJwtSigner + params.ID = id + + _, err = client.ExternalJWTSigner.PatchExternalJWTSigner(params, nil) + + if err != nil { + return util.WrapIfApiError(err) + } + + return nil +}