forked from TrueCloudLab/certificates
Merge pull request #291 from smallstep/template-variables
Add support for required template variables
This commit is contained in:
commit
e9a0ad4ec7
9 changed files with 287 additions and 92 deletions
|
@ -31,6 +31,7 @@ type Authority struct {
|
||||||
keyManager kms.KeyManager
|
keyManager kms.KeyManager
|
||||||
provisioners *provisioner.Collection
|
provisioners *provisioner.Collection
|
||||||
db db.AuthDB
|
db db.AuthDB
|
||||||
|
templates *templates.Templates
|
||||||
|
|
||||||
// X509 CA
|
// X509 CA
|
||||||
rootX509Certs []*x509.Certificate
|
rootX509Certs []*x509.Certificate
|
||||||
|
@ -202,6 +203,7 @@ func (a *Authority) init() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt and load SSH keys
|
// Decrypt and load SSH keys
|
||||||
|
var tmplVars templates.Step
|
||||||
if a.config.SSH != nil {
|
if a.config.SSH != nil {
|
||||||
if a.config.SSH.HostKey != "" {
|
if a.config.SSH.HostKey != "" {
|
||||||
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||||
|
@ -255,6 +257,14 @@ func (a *Authority) init() error {
|
||||||
return errors.Errorf("unsupported type %s", key.Type)
|
return errors.Errorf("unsupported type %s", key.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure template variables.
|
||||||
|
tmplVars.SSH.HostKey = a.sshCAHostCertSignKey.PublicKey()
|
||||||
|
tmplVars.SSH.UserKey = a.sshCAUserCertSignKey.PublicKey()
|
||||||
|
// On the templates we skip the first one because there's a distinction
|
||||||
|
// between the main key and federated keys.
|
||||||
|
tmplVars.SSH.HostFederatedKeys = append(tmplVars.SSH.HostFederatedKeys, a.sshCAHostFederatedCerts[1:]...)
|
||||||
|
tmplVars.SSH.UserFederatedKeys = append(tmplVars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts[1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge global and configuration claims
|
// Merge global and configuration claims
|
||||||
|
@ -292,23 +302,16 @@ func (a *Authority) init() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure protected template variables:
|
// Configure templates, currently only ssh templates are supported.
|
||||||
if t := a.config.Templates; t != nil {
|
if a.sshCAHostCertSignKey != nil || a.sshCAUserCertSignKey != nil {
|
||||||
if t.Data == nil {
|
a.templates = a.config.Templates
|
||||||
t.Data = make(map[string]interface{})
|
if a.templates == nil {
|
||||||
|
a.templates = templates.DefaultTemplates()
|
||||||
}
|
}
|
||||||
var vars templates.Step
|
if a.templates.Data == nil {
|
||||||
if a.config.SSH != nil {
|
a.templates.Data = make(map[string]interface{})
|
||||||
if a.sshCAHostCertSignKey != nil {
|
|
||||||
vars.SSH.HostKey = a.sshCAHostCertSignKey.PublicKey()
|
|
||||||
vars.SSH.HostFederatedKeys = append(vars.SSH.HostFederatedKeys, a.sshCAHostFederatedCerts[1:]...)
|
|
||||||
}
|
|
||||||
if a.sshCAUserCertSignKey != nil {
|
|
||||||
vars.SSH.UserKey = a.sshCAUserCertSignKey.PublicKey()
|
|
||||||
vars.SSH.UserFederatedKeys = append(vars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts[1:]...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
t.Data["Step"] = vars
|
a.templates.Data["Step"] = tmplVars
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWT numeric dates are seconds.
|
// JWT numeric dates are seconds.
|
||||||
|
|
|
@ -125,19 +125,19 @@ func (a *Authority) GetSSHConfig(ctx context.Context, typ string, data map[strin
|
||||||
return nil, errs.NotFound("getSSHConfig: ssh is not configured")
|
return nil, errs.NotFound("getSSHConfig: ssh is not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.config.Templates == nil {
|
if a.templates == nil {
|
||||||
return nil, errs.NotFound("getSSHConfig: ssh templates are not configured")
|
return nil, errs.NotFound("getSSHConfig: ssh templates are not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
var ts []templates.Template
|
var ts []templates.Template
|
||||||
switch typ {
|
switch typ {
|
||||||
case provisioner.SSHUserCert:
|
case provisioner.SSHUserCert:
|
||||||
if a.config.Templates != nil && a.config.Templates.SSH != nil {
|
if a.templates != nil && a.templates.SSH != nil {
|
||||||
ts = a.config.Templates.SSH.User
|
ts = a.templates.SSH.User
|
||||||
}
|
}
|
||||||
case provisioner.SSHHostCert:
|
case provisioner.SSHHostCert:
|
||||||
if a.config.Templates != nil && a.config.Templates.SSH != nil {
|
if a.templates != nil && a.templates.SSH != nil {
|
||||||
ts = a.config.Templates.SSH.Host
|
ts = a.templates.SSH.Host
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return nil, errs.BadRequest("getSSHConfig: type %s is not valid", typ)
|
return nil, errs.BadRequest("getSSHConfig: type %s is not valid", typ)
|
||||||
|
@ -147,11 +147,11 @@ func (a *Authority) GetSSHConfig(ctx context.Context, typ string, data map[strin
|
||||||
var mergedData map[string]interface{}
|
var mergedData map[string]interface{}
|
||||||
|
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
mergedData = a.config.Templates.Data
|
mergedData = a.templates.Data
|
||||||
} else {
|
} else {
|
||||||
mergedData = make(map[string]interface{}, len(a.config.Templates.Data)+1)
|
mergedData = make(map[string]interface{}, len(a.templates.Data)+1)
|
||||||
mergedData["User"] = data
|
mergedData["User"] = data
|
||||||
for k, v := range a.config.Templates.Data {
|
for k, v := range a.templates.Data {
|
||||||
mergedData[k] = v
|
mergedData[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,6 +159,15 @@ func (a *Authority) GetSSHConfig(ctx context.Context, typ string, data map[strin
|
||||||
// Render templates
|
// Render templates
|
||||||
output := []templates.Output{}
|
output := []templates.Output{}
|
||||||
for _, t := range ts {
|
for _, t := range ts {
|
||||||
|
if err := t.Load(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for required variables.
|
||||||
|
if err := t.ValidateRequiredData(data); err != nil {
|
||||||
|
return nil, errs.BadRequestErr(err, errs.WithMessage("%v, please use `--set <key=value>` flag", err))
|
||||||
|
}
|
||||||
|
|
||||||
o, err := t.Output(mergedData)
|
o, err := t.Output(mergedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -398,7 +398,14 @@ func TestAuthority_GetSSHConfig(t *testing.T) {
|
||||||
{Name: "config.tpl", Type: templates.File, TemplatePath: "./testdata/templates/config.tpl", Path: "ssh/config", Comment: "#"},
|
{Name: "config.tpl", Type: templates.File, TemplatePath: "./testdata/templates/config.tpl", Path: "ssh/config", Comment: "#"},
|
||||||
},
|
},
|
||||||
Host: []templates.Template{
|
Host: []templates.Template{
|
||||||
{Name: "sshd_config.tpl", Type: templates.File, TemplatePath: "./testdata/templates/sshd_config.tpl", Path: "/etc/ssh/sshd_config", Comment: "#"},
|
{
|
||||||
|
Name: "sshd_config.tpl",
|
||||||
|
Type: templates.File,
|
||||||
|
TemplatePath: "./testdata/templates/sshd_config.tpl",
|
||||||
|
Path: "/etc/ssh/sshd_config",
|
||||||
|
Comment: "#",
|
||||||
|
RequiredData: []string{"Certificate", "Key"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
|
@ -429,6 +436,14 @@ func TestAuthority_GetSSHConfig(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tmplConfigFail := &templates.Templates{
|
||||||
|
SSH: &templates.SSHTemplates{
|
||||||
|
User: []templates.Template{
|
||||||
|
{Name: "fail.tpl", Type: templates.File, TemplatePath: "./testdata/templates/fail.tpl", Path: "ssh/fail", Comment: "#"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
type fields struct {
|
type fields struct {
|
||||||
templates *templates.Templates
|
templates *templates.Templates
|
||||||
userSigner ssh.Signer
|
userSigner ssh.Signer
|
||||||
|
@ -456,11 +471,13 @@ func TestAuthority_GetSSHConfig(t *testing.T) {
|
||||||
{"userError", fields{tmplConfigErr, userSigner, hostSigner}, args{"user", nil}, nil, true},
|
{"userError", fields{tmplConfigErr, userSigner, hostSigner}, args{"user", nil}, nil, true},
|
||||||
{"hostError", fields{tmplConfigErr, userSigner, hostSigner}, args{"host", map[string]string{"Function": "foo"}}, nil, true},
|
{"hostError", fields{tmplConfigErr, userSigner, hostSigner}, args{"host", map[string]string{"Function": "foo"}}, nil, true},
|
||||||
{"noTemplates", fields{nil, userSigner, hostSigner}, args{"user", nil}, nil, true},
|
{"noTemplates", fields{nil, userSigner, hostSigner}, args{"user", nil}, nil, true},
|
||||||
|
{"missingData", fields{tmplConfigWithUserData, userSigner, hostSigner}, args{"host", map[string]string{"Certificate": "ssh_host_ecdsa_key-cert.pub"}}, nil, true},
|
||||||
|
{"failError", fields{tmplConfigFail, userSigner, hostSigner}, args{"user", nil}, nil, true},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
a := testAuthority(t)
|
a := testAuthority(t)
|
||||||
a.config.Templates = tt.fields.templates
|
a.templates = tt.fields.templates
|
||||||
a.sshCAUserCertSignKey = tt.fields.userSigner
|
a.sshCAUserCertSignKey = tt.fields.userSigner
|
||||||
a.sshCAHostCertSignKey = tt.fields.hostSigner
|
a.sshCAHostCertSignKey = tt.fields.hostSigner
|
||||||
|
|
||||||
|
|
1
authority/testdata/templates/fail.tpl
vendored
Normal file
1
authority/testdata/templates/fail.tpl
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{{ fail "This template will fail" }}
|
|
@ -11,77 +11,13 @@ import (
|
||||||
"github.com/smallstep/cli/utils"
|
"github.com/smallstep/cli/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SSHTemplates contains the configuration of default templates used on ssh.
|
|
||||||
// Relative paths are relative to the StepPath.
|
|
||||||
var SSHTemplates = &templates.SSHTemplates{
|
|
||||||
User: []templates.Template{
|
|
||||||
{Name: "include.tpl", Type: templates.Snippet, TemplatePath: "templates/ssh/include.tpl", Path: "~/.ssh/config", Comment: "#"},
|
|
||||||
{Name: "config.tpl", Type: templates.File, TemplatePath: "templates/ssh/config.tpl", Path: "ssh/config", Comment: "#"},
|
|
||||||
{Name: "known_hosts.tpl", Type: templates.File, TemplatePath: "templates/ssh/known_hosts.tpl", Path: "ssh/known_hosts", Comment: "#"},
|
|
||||||
},
|
|
||||||
Host: []templates.Template{
|
|
||||||
{Name: "sshd_config.tpl", Type: templates.Snippet, TemplatePath: "templates/ssh/sshd_config.tpl", Path: "/etc/ssh/sshd_config", Comment: "#"},
|
|
||||||
{Name: "ca.tpl", Type: templates.Snippet, TemplatePath: "templates/ssh/ca.tpl", Path: "/etc/ssh/ca.pub", Comment: "#"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// SSHTemplateData contains the data of the default templates used on ssh.
|
|
||||||
var SSHTemplateData = map[string]string{
|
|
||||||
// include.tpl adds the step ssh config file.
|
|
||||||
//
|
|
||||||
// Note: on windows `Include C:\...` is treated as a relative path.
|
|
||||||
"include.tpl": `Host *
|
|
||||||
{{- if or .User.GOOS "none" | eq "windows" }}
|
|
||||||
Include "{{ .User.StepPath | replace "\\" "/" | trimPrefix "C:" }}/ssh/config"
|
|
||||||
{{- else }}
|
|
||||||
Include "{{.User.StepPath}}/ssh/config"
|
|
||||||
{{- end }}`,
|
|
||||||
|
|
||||||
// config.tpl is the step ssh config file, it includes the Match rule and
|
|
||||||
// references the step known_hosts file.
|
|
||||||
//
|
|
||||||
// Note: on windows ProxyCommand requires the full path
|
|
||||||
"config.tpl": `Match exec "step ssh check-host %h"
|
|
||||||
{{- if .User.User }}
|
|
||||||
User {{.User.User}}
|
|
||||||
{{- end }}
|
|
||||||
{{- if or .User.GOOS "none" | eq "windows" }}
|
|
||||||
UserKnownHostsFile "{{.User.StepPath}}\ssh\known_hosts"
|
|
||||||
ProxyCommand C:\Windows\System32\cmd.exe /c step ssh proxycommand %r %h %p
|
|
||||||
{{- else }}
|
|
||||||
UserKnownHostsFile "{{.User.StepPath}}/ssh/known_hosts"
|
|
||||||
ProxyCommand step ssh proxycommand %r %h %p
|
|
||||||
{{- end }}
|
|
||||||
`,
|
|
||||||
|
|
||||||
// known_hosts.tpl authorizes the ssh hosts key
|
|
||||||
"known_hosts.tpl": `@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}}
|
|
||||||
{{- range .Step.SSH.HostFederatedKeys}}
|
|
||||||
@cert-authority * {{.Type}} {{.Marshal | toString | b64enc}}
|
|
||||||
{{- end }}
|
|
||||||
`,
|
|
||||||
|
|
||||||
// sshd_config.tpl adds the configuration to support certificates
|
|
||||||
"sshd_config.tpl": `TrustedUserCAKeys /etc/ssh/ca.pub
|
|
||||||
HostCertificate /etc/ssh/{{.User.Certificate}}
|
|
||||||
HostKey /etc/ssh/{{.User.Key}}`,
|
|
||||||
|
|
||||||
// ca.tpl contains the public key used to authorized clients
|
|
||||||
"ca.tpl": `{{.Step.SSH.UserKey.Type}} {{.Step.SSH.UserKey.Marshal | toString | b64enc}}
|
|
||||||
{{- range .Step.SSH.UserFederatedKeys}}
|
|
||||||
{{.Type}} {{.Marshal | toString | b64enc}}
|
|
||||||
{{- end }}
|
|
||||||
`,
|
|
||||||
}
|
|
||||||
|
|
||||||
// getTemplates returns all the templates enabled
|
// getTemplates returns all the templates enabled
|
||||||
func (p *PKI) getTemplates() *templates.Templates {
|
func (p *PKI) getTemplates() *templates.Templates {
|
||||||
if !p.enableSSH {
|
if !p.enableSSH {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &templates.Templates{
|
return &templates.Templates{
|
||||||
SSH: SSHTemplates,
|
SSH: &templates.DefaultSSHTemplates,
|
||||||
Data: map[string]interface{}{},
|
Data: map[string]interface{}{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,7 +40,7 @@ func generateTemplates(t *templates.Templates) error {
|
||||||
}
|
}
|
||||||
// Create all templates
|
// Create all templates
|
||||||
for _, t := range t.SSH.User {
|
for _, t := range t.SSH.User {
|
||||||
data, ok := SSHTemplateData[t.Name]
|
data, ok := templates.DefaultSSHTemplateData[t.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.Errorf("template %s does not exists", t.Name)
|
return errors.Errorf("template %s does not exists", t.Name)
|
||||||
}
|
}
|
||||||
|
@ -113,7 +49,7 @@ func generateTemplates(t *templates.Templates) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, t := range t.SSH.Host {
|
for _, t := range t.SSH.Host {
|
||||||
data, ok := SSHTemplateData[t.Name]
|
data, ok := templates.DefaultSSHTemplateData[t.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.Errorf("template %s does not exists", t.Name)
|
return errors.Errorf("template %s does not exists", t.Name)
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,6 +106,7 @@ type Template struct {
|
||||||
TemplatePath string `json:"template"`
|
TemplatePath string `json:"template"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
|
RequiredData []string `json:"requires,omitempty"`
|
||||||
Content []byte `json:"-"`
|
Content []byte `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,6 +148,17 @@ func (t *Template) Validate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateRequiredData checks that the given data contains all the keys
|
||||||
|
// required.
|
||||||
|
func (t *Template) ValidateRequiredData(data map[string]string) error {
|
||||||
|
for _, key := range t.RequiredData {
|
||||||
|
if _, ok := data[key]; !ok {
|
||||||
|
return errors.Errorf("required variable '%s' is missing", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Load loads the template in memory, returns an error if the parsing of the
|
// Load loads the template in memory, returns an error if the parsing of the
|
||||||
// template fails.
|
// template fails.
|
||||||
func (t *Template) Load() error {
|
func (t *Template) Load() error {
|
||||||
|
@ -166,7 +178,10 @@ func (t *Template) Load() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadBytes loads the template in memory, returns an error if the parsing of
|
||||||
|
// the template fails.
|
||||||
func (t *Template) LoadBytes(b []byte) error {
|
func (t *Template) LoadBytes(b []byte) error {
|
||||||
|
t.backfill(b)
|
||||||
tmpl, err := template.New(t.Name).Funcs(sprig.TxtFuncMap()).Parse(string(b))
|
tmpl, err := template.New(t.Name).Funcs(sprig.TxtFuncMap()).Parse(string(b))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "error parsing template %s", t.Name)
|
return errors.Wrapf(err, "error parsing template %s", t.Name)
|
||||||
|
@ -209,6 +224,20 @@ func (t *Template) Output(data interface{}) (Output, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// backfill updates old templates with the required data.
|
||||||
|
func (t *Template) backfill(b []byte) {
|
||||||
|
switch t.Name {
|
||||||
|
case "sshd_config.tpl":
|
||||||
|
if len(t.RequiredData) == 0 {
|
||||||
|
a := bytes.TrimSpace(b)
|
||||||
|
b := bytes.TrimSpace([]byte(DefaultSSHTemplateData[t.Name]))
|
||||||
|
if bytes.Equal(a, b) {
|
||||||
|
t.RequiredData = []string{"Certificate", "Key"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Output represents the text representation of a rendered template.
|
// Output represents the text representation of a rendered template.
|
||||||
type Output struct {
|
type Output struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
|
@ -197,6 +197,7 @@ func TestTemplate_Load(t *testing.T) {
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"ok", fields{"include.tpl", Snippet, "../authority/testdata/templates/include.tpl", "~/.ssh/config", "#"}, false},
|
{"ok", fields{"include.tpl", Snippet, "../authority/testdata/templates/include.tpl", "~/.ssh/config", "#"}, false},
|
||||||
|
{"ok backfill", fields{"sshd_config.tpl", Snippet, "../authority/testdata/templates/sshd_config.tpl", "/etc/ssh/sshd_config", "#"}, false},
|
||||||
{"error", fields{"error.tpl", Snippet, "../authority/testdata/templates/error.tpl", "/tmp/error", "#"}, true},
|
{"error", fields{"error.tpl", Snippet, "../authority/testdata/templates/error.tpl", "/tmp/error", "#"}, true},
|
||||||
{"missing", fields{"include.tpl", Snippet, "./testdata/include.tpl", "~/.ssh/config", "#"}, true},
|
{"missing", fields{"include.tpl", Snippet, "./testdata/include.tpl", "~/.ssh/config", "#"}, true},
|
||||||
}
|
}
|
||||||
|
@ -428,3 +429,39 @@ func TestOutput_Write(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTemplate_ValidateRequiredData(t *testing.T) {
|
||||||
|
data := map[string]string{
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "value2",
|
||||||
|
}
|
||||||
|
type fields struct {
|
||||||
|
RequiredData []string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
data map[string]string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok nil", fields{nil}, args{nil}, false},
|
||||||
|
{"ok empty", fields{[]string{}}, args{data}, false},
|
||||||
|
{"ok one", fields{[]string{"key1"}}, args{data}, false},
|
||||||
|
{"ok multiple", fields{[]string{"key1", "key2"}}, args{data}, false},
|
||||||
|
{"fail nil", fields{[]string{"missing"}}, args{nil}, true},
|
||||||
|
{"fail missing", fields{[]string{"missing"}}, args{data}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tmpl := &Template{
|
||||||
|
RequiredData: tt.fields.RequiredData,
|
||||||
|
}
|
||||||
|
if err := tmpl.ValidateRequiredData(tt.args.data); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Template.ValidateRequiredData() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,3 +16,114 @@ type StepSSH struct {
|
||||||
HostFederatedKeys []ssh.PublicKey
|
HostFederatedKeys []ssh.PublicKey
|
||||||
UserFederatedKeys []ssh.PublicKey
|
UserFederatedKeys []ssh.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultSSHTemplates contains the configuration of default templates used on ssh.
|
||||||
|
// Relative paths are relative to the StepPath.
|
||||||
|
var DefaultSSHTemplates = SSHTemplates{
|
||||||
|
User: []Template{
|
||||||
|
{
|
||||||
|
Name: "include.tpl",
|
||||||
|
Type: Snippet,
|
||||||
|
TemplatePath: "templates/ssh/include.tpl",
|
||||||
|
Path: "~/.ssh/config",
|
||||||
|
Comment: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "config.tpl",
|
||||||
|
Type: File,
|
||||||
|
TemplatePath: "templates/ssh/config.tpl",
|
||||||
|
Path: "ssh/config",
|
||||||
|
Comment: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "known_hosts.tpl",
|
||||||
|
Type: File,
|
||||||
|
TemplatePath: "templates/ssh/known_hosts.tpl",
|
||||||
|
Path: "ssh/known_hosts",
|
||||||
|
Comment: "#",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Host: []Template{
|
||||||
|
{
|
||||||
|
Name: "sshd_config.tpl",
|
||||||
|
Type: Snippet,
|
||||||
|
TemplatePath: "templates/ssh/sshd_config.tpl",
|
||||||
|
Path: "/etc/ssh/sshd_config",
|
||||||
|
Comment: "#",
|
||||||
|
RequiredData: []string{"Certificate", "Key"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ca.tpl",
|
||||||
|
Type: Snippet,
|
||||||
|
TemplatePath: "templates/ssh/ca.tpl",
|
||||||
|
Path: "/etc/ssh/ca.pub",
|
||||||
|
Comment: "#",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultSSHTemplateData contains the data of the default templates used on ssh.
|
||||||
|
var DefaultSSHTemplateData = map[string]string{
|
||||||
|
// include.tpl adds the step ssh config file.
|
||||||
|
//
|
||||||
|
// Note: on windows `Include C:\...` is treated as a relative path.
|
||||||
|
"include.tpl": `Host *
|
||||||
|
{{- if or .User.GOOS "none" | eq "windows" }}
|
||||||
|
Include "{{ .User.StepPath | replace "\\" "/" | trimPrefix "C:" }}/ssh/config"
|
||||||
|
{{- else }}
|
||||||
|
Include "{{.User.StepPath}}/ssh/config"
|
||||||
|
{{- end }}`,
|
||||||
|
|
||||||
|
// config.tpl is the step ssh config file, it includes the Match rule and
|
||||||
|
// references the step known_hosts file.
|
||||||
|
//
|
||||||
|
// Note: on windows ProxyCommand requires the full path
|
||||||
|
"config.tpl": `Match exec "step ssh check-host %h"
|
||||||
|
{{- if .User.User }}
|
||||||
|
User {{.User.User}}
|
||||||
|
{{- end }}
|
||||||
|
{{- if or .User.GOOS "none" | eq "windows" }}
|
||||||
|
UserKnownHostsFile "{{.User.StepPath}}\ssh\known_hosts"
|
||||||
|
ProxyCommand C:\Windows\System32\cmd.exe /c step ssh proxycommand %r %h %p
|
||||||
|
{{- else }}
|
||||||
|
UserKnownHostsFile "{{.User.StepPath}}/ssh/known_hosts"
|
||||||
|
ProxyCommand step ssh proxycommand %r %h %p
|
||||||
|
{{- end }}
|
||||||
|
`,
|
||||||
|
|
||||||
|
// known_hosts.tpl authorizes the ssh hosts key
|
||||||
|
"known_hosts.tpl": `@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}}
|
||||||
|
{{- range .Step.SSH.HostFederatedKeys}}
|
||||||
|
@cert-authority * {{.Type}} {{.Marshal | toString | b64enc}}
|
||||||
|
{{- end }}
|
||||||
|
`,
|
||||||
|
|
||||||
|
// sshd_config.tpl adds the configuration to support certificates
|
||||||
|
"sshd_config.tpl": `TrustedUserCAKeys /etc/ssh/ca.pub
|
||||||
|
HostCertificate /etc/ssh/{{.User.Certificate}}
|
||||||
|
HostKey /etc/ssh/{{.User.Key}}`,
|
||||||
|
|
||||||
|
// ca.tpl contains the public key used to authorized clients
|
||||||
|
"ca.tpl": `{{.Step.SSH.UserKey.Type}} {{.Step.SSH.UserKey.Marshal | toString | b64enc}}
|
||||||
|
{{- range .Step.SSH.UserFederatedKeys}}
|
||||||
|
{{.Type}} {{.Marshal | toString | b64enc}}
|
||||||
|
{{- end }}
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultTemplates returns the default templates.
|
||||||
|
func DefaultTemplates() *Templates {
|
||||||
|
sshTemplates := DefaultSSHTemplates
|
||||||
|
for i, t := range sshTemplates.User {
|
||||||
|
sshTemplates.User[i].TemplatePath = ""
|
||||||
|
sshTemplates.User[i].Content = []byte(DefaultSSHTemplateData[t.Name])
|
||||||
|
}
|
||||||
|
for i, t := range sshTemplates.Host {
|
||||||
|
sshTemplates.Host[i].TemplatePath = ""
|
||||||
|
sshTemplates.Host[i].Content = []byte(DefaultSSHTemplateData[t.Name])
|
||||||
|
}
|
||||||
|
return &Templates{
|
||||||
|
SSH: &sshTemplates,
|
||||||
|
Data: map[string]interface{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
52
templates/values_test.go
Normal file
52
templates/values_test.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultTemplates(t *testing.T) {
|
||||||
|
sshTemplates := DefaultSSHTemplates
|
||||||
|
sshTemplatesData := DefaultSSHTemplateData
|
||||||
|
t.Cleanup(func() {
|
||||||
|
DefaultSSHTemplates = sshTemplates
|
||||||
|
DefaultSSHTemplateData = sshTemplatesData
|
||||||
|
})
|
||||||
|
|
||||||
|
DefaultSSHTemplates = SSHTemplates{
|
||||||
|
User: []Template{
|
||||||
|
{Name: "foo.tpl", Type: Snippet, TemplatePath: "templates/ssh/foo.tpl", Path: "/tmp/foo", Comment: "#"},
|
||||||
|
},
|
||||||
|
Host: []Template{
|
||||||
|
{Name: "bar.tpl", Type: Snippet, TemplatePath: "templates/ssh/bar.tpl", Path: "/tmp/bar", Comment: "#"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
DefaultSSHTemplateData = map[string]string{
|
||||||
|
"foo.tpl": "foo",
|
||||||
|
"bar.tpl": "bar",
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want *Templates
|
||||||
|
}{
|
||||||
|
{"ok", &Templates{
|
||||||
|
SSH: &SSHTemplates{
|
||||||
|
User: []Template{
|
||||||
|
{Name: "foo.tpl", Type: Snippet, Content: []byte("foo"), Path: "/tmp/foo", Comment: "#"},
|
||||||
|
},
|
||||||
|
Host: []Template{
|
||||||
|
{Name: "bar.tpl", Type: Snippet, Content: []byte("bar"), Path: "/tmp/bar", Comment: "#"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Data: map[string]interface{}{},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := DefaultTemplates(); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("DefaultTemplates() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue