Merge pull request #1054 from openziti/add.ext.jwt.signers.cli

adds ext jwt signer support
This commit is contained in:
Andrew
2023-03-28 15:40:10 -04:00
committed by GitHub
7 changed files with 511 additions and 32 deletions
+5
View File
@@ -26,6 +26,10 @@ import (
"github.com/spf13/cobra"
)
func Ptr[T any](sub T) *T {
return &sub
}
// newCreateCmd creates a command object for the "create" command
func newCreateCmd(out io.Writer, errOut io.Writer) *cobra.Command {
cmd := &cobra.Command{
@@ -51,6 +55,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
}
+12 -24
View File
@@ -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)
+166
View File
@@ -0,0 +1,166 @@
/*
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 <name> <issuer> (-u <jwksEndpoint>|-p <cert pem>|-f <cert file>) [-a <audience> -c <claimProperty> -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) != 2 {
return fmt.Errorf("requires 2 arg, received %d", len(args))
}
options.ExtJwtSigner.Name = &args[0]
options.ExtJwtSigner.Issuer = &args[1]
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
options.ExtJwtSigner.CertPem = nil
options.ExtJwtSigner.Kid = nil
} else {
options.ExtJwtSigner.JwksEndpoint = nil
}
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
}
+112 -2
View File
@@ -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,123 @@ 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)
outTable.Style().Options.SeparateRows = true
rowConfigAutoMerge := table.RowConfig{AutoMerge: true, AutoMergeAlign: text.AlignLeft}
outTable.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, AutoMerge: true, Align: text.AlignLeft},
{Number: 2, AutoMerge: true, Align: text.AlignLeft},
})
outTable.AppendHeader(table.Row{"ID", "Name", "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
issuer := *entity.Issuer
if entity.JwksEndpoint != nil {
confType := "JWKS"
urlStr := entity.JwksEndpoint.String()
outTable.AppendRow(table.Row{id, name, "Audience", audience}, rowConfigAutoMerge)
outTable.AppendRow(table.Row{id, name, "Claim Property", claimsProperty}, rowConfigAutoMerge)
outTable.AppendRow(table.Row{id, name, "Enabled", isEnabled}, rowConfigAutoMerge)
outTable.AppendRow(table.Row{id, name, "Issuer", issuer}, rowConfigAutoMerge)
outTable.AppendRow(table.Row{id, name, "JWKS URL", urlStr}, rowConfigAutoMerge)
outTable.AppendRow(table.Row{id, name, "Type", confType}, rowConfigAutoMerge)
outTable.AppendRow(table.Row{id, name, "Use External Id", useExternalId}, rowConfigAutoMerge)
} else {
confType := "CERT"
fingerprint := *entity.Fingerprint
outTable.AppendRow(table.Row{id, name, "Audience", audience}, rowConfigAutoMerge)
outTable.AppendRow(table.Row{id, name, "Claim Property", claimsProperty}, rowConfigAutoMerge)
outTable.AppendRow(table.Row{id, name, "Enabled", isEnabled}, rowConfigAutoMerge)
outTable.AppendRow(table.Row{id, name, "Issuer", issuer}, rowConfigAutoMerge)
outTable.AppendRow(table.Row{id, name, "Fingerprint", fingerprint}, rowConfigAutoMerge)
outTable.AppendRow(table.Row{id, name, "Type", confType}, rowConfigAutoMerge)
outTable.AppendRow(table.Row{id, name, "Use External Id", useExternalId}, rowConfigAutoMerge)
}
}
pagingInfo := newPagingInfo(payload.Meta)
api.RenderTable(options, outTable, pagingInfo)
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
}
+1
View File
@@ -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
}
+6 -6
View File
@@ -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(""),
},
}
+209
View File
@@ -0,0 +1,209 @@
/*
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 <id|name> [-u <jwksEndpoint>|-p <cert pem>|-f <cert file>] [-n <nameName> -a <audience> -c <claimProperty> -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")
}
options.nameOrId = args[0]
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")
cmd.Flags().StringVarP(options.ExtJwtSigner.Issuer, "issuer", "l", "", "The issuer for the signer")
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("issuer").Changed {
changed = true
} else {
options.ExtJwtSigner.Issuer = 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
}