From d9c4b0cd1c49a47c7530b5bd90a8c114000b9ccb Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 18 Jul 2023 15:18:24 -0700 Subject: [PATCH] Customize principal validation using an environment variable By default, the OIDC user principal must validate the regular expression "^[a-z][-a-z0-9_]*$", but with this commit, a custom regular expression can be defined using the environment variable STEP_SSH_USER_REGEXP. Fixes #1436 --- authority/provisioner/controller.go | 24 +++++++++++++++++++----- authority/provisioner/controller_test.go | 21 +++++++++++++++++---- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/authority/provisioner/controller.go b/authority/provisioner/controller.go index 25030fbc..438150a9 100644 --- a/authority/provisioner/controller.go +++ b/authority/provisioner/controller.go @@ -4,6 +4,7 @@ import ( "context" "crypto/x509" "net/http" + "os" "regexp" "strings" "time" @@ -108,9 +109,13 @@ type AuthorizeRenewFunc func(ctx context.Context, p *Controller, cert *x509.Cert // given SSH certificate is enabled. type AuthorizeSSHRenewFunc func(ctx context.Context, p *Controller, cert *ssh.Certificate) error -// DefaultIdentityFunc return a default identity depending on the provisioner -// type. For OIDC email is always present and the usernames might -// contain empty strings. +// DefaultIdentityFunc returns a default identity depending on the provisioner +// type. For OIDC email is always present and the usernames might contain empty +// strings. +// +// By default, the user principal from the OIDC email must validate the regular +// expression "^[a-z][-a-z0-9_]*$", but a custom regular expression can be +// defined using the environment variable STEP_SSH_USER_REGEXP. func DefaultIdentityFunc(_ context.Context, p Interface, email string) (*Identity, error) { switch k := p.(type) { case *OIDC: @@ -120,7 +125,7 @@ func DefaultIdentityFunc(_ context.Context, p Interface, email string) (*Identit // 3. Raw local (if different). // 4. Email address. name := SanitizeSSHUserPrincipal(email) - if !sshUserRegex.MatchString(name) { + if !sshUserRegexp.MatchString(name) { return nil, errors.Errorf("invalid principal '%s' from email '%s'", name, email) } usernames := []string{name} @@ -178,7 +183,16 @@ func DefaultAuthorizeSSHRenew(_ context.Context, p *Controller, cert *ssh.Certif return nil } -var sshUserRegex = regexp.MustCompile("^[a-z][-a-z0-9_]*$") +// sshUserRegexp is the regular expression used to validate the SSH user +// principals in DefaultIdentityFunc. +var sshUserRegexp *regexp.Regexp + +func init() { + if v := os.Getenv("STEP_SSH_USER_REGEXP"); v != "" { + sshUserRegexp = regexp.MustCompile(v) + } + sshUserRegexp = regexp.MustCompile("^[a-z][-a-z0-9_]*$") +} // SanitizeStringSlices removes duplicated an empty strings. func SanitizeStringSlices(original []string) []string { diff --git a/authority/provisioner/controller_test.go b/authority/provisioner/controller_test.go index c628f074..d594ccee 100644 --- a/authority/provisioner/controller_test.go +++ b/authority/provisioner/controller_test.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "fmt" "reflect" + "regexp" "testing" "time" @@ -144,9 +145,15 @@ func TestNewController(t *testing.T) { func TestController_GetIdentity(t *testing.T) { ctx := context.Background() + defaultUserRegexp := sshUserRegexp + t.Cleanup(func() { + sshUserRegexp = defaultUserRegexp + }) + type fields struct { Interface Interface IdentityFunc GetIdentityFunc + SSHUserRegex *regexp.Regexp } type args struct { ctx context.Context @@ -159,21 +166,27 @@ func TestController_GetIdentity(t *testing.T) { want *Identity wantErr bool }{ - {"ok", fields{&OIDC{}, nil}, args{ctx, "jane@doe.org"}, &Identity{ + {"ok", fields{&OIDC{}, nil, defaultUserRegexp}, args{ctx, "jane@doe.org"}, &Identity{ Usernames: []string{"jane", "jane@doe.org"}, }, false}, {"ok custom", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) { return &Identity{Usernames: []string{"jane"}}, nil - }}, args{ctx, "jane@doe.org"}, &Identity{ + }, defaultUserRegexp}, args{ctx, "jane@doe.org"}, &Identity{ Usernames: []string{"jane"}, }, false}, - {"fail provisioner", fields{&JWK{}, nil}, args{ctx, "jane@doe.org"}, nil, true}, + {"ok custom regex", fields{&OIDC{}, nil, regexp.MustCompile("^[a-z0-9]*$")}, args{ctx, "1000@doe.org"}, &Identity{ + Usernames: []string{"1000", "1000@doe.org"}, + }, false}, + {"fail provisioner", fields{&JWK{}, nil, defaultUserRegexp}, args{ctx, "jane@doe.org"}, nil, true}, {"fail custom", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) { return nil, fmt.Errorf("an error") - }}, args{ctx, "jane@doe.org"}, nil, true}, + }, defaultUserRegexp}, args{ctx, "jane@doe.org"}, nil, true}, + {"fail regex", fields{&OIDC{}, nil, defaultUserRegexp}, args{ctx, "1000@doe.org"}, nil, true}, + {"fail custom regex", fields{&OIDC{}, nil, regexp.MustCompile("^[a-z]*$")}, args{ctx, "jane1000@doe.org"}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + sshUserRegexp = tt.fields.SSHUserRegex c := &Controller{ Interface: tt.fields.Interface, IdentityFunc: tt.fields.IdentityFunc,