fixes #945 adds ext jwt signer support

This commit is contained in:
Andrew Martinez
2023-03-27 14:56:34 -04:00
parent 56039d121a
commit 79ff705763
7 changed files with 479 additions and 32 deletions
+6
View File
@@ -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
}
+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)
+161
View File
@@ -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 <name> (-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) != 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
}
+93 -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,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
}
+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(""),
},
}
+200
View File
@@ -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 <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")
}
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
}