diff --git a/authority/provisioner/ssh_options.go b/authority/provisioner/ssh_options.go new file mode 100644 index 00000000..784b0958 --- /dev/null +++ b/authority/provisioner/ssh_options.go @@ -0,0 +1,107 @@ +package provisioner + +import ( + "encoding/json" + "strings" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/sshutil" +) + +// CertificateOptions is an interface that returns a list of options passed when +// creating a new certificate. +type SSHCertificateOptions interface { + Options(SignSSHOptions) []sshutil.Option +} + +type sshCertificateOptionsFunc func(SignSSHOptions) []sshutil.Option + +func (fn sshCertificateOptionsFunc) Options(so SignSSHOptions) []sshutil.Option { + return fn(so) +} + +// SSHOptions are a collection of custom options that can be added to each +// provisioner. +type SSHOptions struct { + // Template contains an SSH certificate template. It can be a JSON template + // escaped in a string or it can be also encoded in base64. + Template string `json:"template,omitempty"` + + // TemplateFile points to a file containing a SSH certificate template. + TemplateFile string `json:"templateFile,omitempty"` + + // TemplateData is a JSON object with variables that can be used in custom + // templates. + TemplateData json.RawMessage `json:"templateData,omitempty"` +} + +// HasTemplate returns true if a template is defined in the provisioner options. +func (o *SSHOptions) HasTemplate() bool { + return o != nil && (o.Template != "" || o.TemplateFile != "") +} + +// SSHTemplateOptions generates a CertificateOptions with the template and data +// defined in the ProvisionerOptions, the provisioner generated data, and the +// user data provided in the request. If no template has been provided, +// x509util.DefaultLeafTemplate will be used. +func TemplateSSHOptions(o *SSHOptions, data sshutil.TemplateData) (SSHCertificateOptions, error) { + return CustomSSHTemplateOptions(o, data, sshutil.DefaultCertificate) +} + +// CustomTemplateOptions generates a CertificateOptions with the template, data +// defined in the ProvisionerOptions, the provisioner generated data and the +// user data provided in the request. If no template has been provided in the +// ProvisionerOptions, the given template will be used. +func CustomSSHTemplateOptions(o *SSHOptions, data sshutil.TemplateData, defaultTemplate string) (SSHCertificateOptions, error) { + if o != nil { + if data == nil { + data = sshutil.NewTemplateData() + } + + // Add template data if any. + if len(o.TemplateData) > 0 { + if err := json.Unmarshal(o.TemplateData, &data); err != nil { + return nil, errors.Wrap(err, "error unmarshaling template data") + } + } + } + + return sshCertificateOptionsFunc(func(so SignSSHOptions) []sshutil.Option { + // We're not provided user data without custom templates. + if !o.HasTemplate() { + return []sshutil.Option{ + sshutil.WithTemplate(defaultTemplate, data), + } + } + + // Add user provided data. + if len(so.TemplateData) > 0 { + userObject := make(map[string]interface{}) + if err := json.Unmarshal(so.TemplateData, &userObject); err != nil { + data.SetUserData(map[string]interface{}{}) + } else { + data.SetUserData(userObject) + } + } + + // Load a template from a file if Template is not defined. + if o.Template == "" && o.TemplateFile != "" { + return []sshutil.Option{ + sshutil.WithTemplateFile(o.TemplateFile, data), + } + } + + // Load a template from the Template fields + // 1. As a JSON in a string. + template := strings.TrimSpace(o.Template) + if strings.HasPrefix(template, "{") { + return []sshutil.Option{ + sshutil.WithTemplate(template, data), + } + } + // 2. As a base64 encoded JSON. + return []sshutil.Option{ + sshutil.WithTemplateBase64(template, data), + } + }), nil +}