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
This commit is contained in:
parent
cbc46d11e5
commit
d9c4b0cd1c
2 changed files with 36 additions and 9 deletions
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -108,9 +109,13 @@ type AuthorizeRenewFunc func(ctx context.Context, p *Controller, cert *x509.Cert
|
||||||
// given SSH certificate is enabled.
|
// given SSH certificate is enabled.
|
||||||
type AuthorizeSSHRenewFunc func(ctx context.Context, p *Controller, cert *ssh.Certificate) error
|
type AuthorizeSSHRenewFunc func(ctx context.Context, p *Controller, cert *ssh.Certificate) error
|
||||||
|
|
||||||
// DefaultIdentityFunc return a default identity depending on the provisioner
|
// DefaultIdentityFunc returns a default identity depending on the provisioner
|
||||||
// type. For OIDC email is always present and the usernames might
|
// type. For OIDC email is always present and the usernames might contain empty
|
||||||
// contain empty strings.
|
// 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) {
|
func DefaultIdentityFunc(_ context.Context, p Interface, email string) (*Identity, error) {
|
||||||
switch k := p.(type) {
|
switch k := p.(type) {
|
||||||
case *OIDC:
|
case *OIDC:
|
||||||
|
@ -120,7 +125,7 @@ func DefaultIdentityFunc(_ context.Context, p Interface, email string) (*Identit
|
||||||
// 3. Raw local (if different).
|
// 3. Raw local (if different).
|
||||||
// 4. Email address.
|
// 4. Email address.
|
||||||
name := SanitizeSSHUserPrincipal(email)
|
name := SanitizeSSHUserPrincipal(email)
|
||||||
if !sshUserRegex.MatchString(name) {
|
if !sshUserRegexp.MatchString(name) {
|
||||||
return nil, errors.Errorf("invalid principal '%s' from email '%s'", name, email)
|
return nil, errors.Errorf("invalid principal '%s' from email '%s'", name, email)
|
||||||
}
|
}
|
||||||
usernames := []string{name}
|
usernames := []string{name}
|
||||||
|
@ -178,7 +183,16 @@ func DefaultAuthorizeSSHRenew(_ context.Context, p *Controller, cert *ssh.Certif
|
||||||
return nil
|
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.
|
// SanitizeStringSlices removes duplicated an empty strings.
|
||||||
func SanitizeStringSlices(original []string) []string {
|
func SanitizeStringSlices(original []string) []string {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -144,9 +145,15 @@ func TestNewController(t *testing.T) {
|
||||||
|
|
||||||
func TestController_GetIdentity(t *testing.T) {
|
func TestController_GetIdentity(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
defaultUserRegexp := sshUserRegexp
|
||||||
|
t.Cleanup(func() {
|
||||||
|
sshUserRegexp = defaultUserRegexp
|
||||||
|
})
|
||||||
|
|
||||||
type fields struct {
|
type fields struct {
|
||||||
Interface Interface
|
Interface Interface
|
||||||
IdentityFunc GetIdentityFunc
|
IdentityFunc GetIdentityFunc
|
||||||
|
SSHUserRegex *regexp.Regexp
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
@ -159,21 +166,27 @@ func TestController_GetIdentity(t *testing.T) {
|
||||||
want *Identity
|
want *Identity
|
||||||
wantErr bool
|
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"},
|
Usernames: []string{"jane", "jane@doe.org"},
|
||||||
}, false},
|
}, false},
|
||||||
{"ok custom", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) {
|
{"ok custom", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) {
|
||||||
return &Identity{Usernames: []string{"jane"}}, nil
|
return &Identity{Usernames: []string{"jane"}}, nil
|
||||||
}}, args{ctx, "jane@doe.org"}, &Identity{
|
}, defaultUserRegexp}, args{ctx, "jane@doe.org"}, &Identity{
|
||||||
Usernames: []string{"jane"},
|
Usernames: []string{"jane"},
|
||||||
}, false},
|
}, 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) {
|
{"fail custom", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) {
|
||||||
return nil, fmt.Errorf("an 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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
sshUserRegexp = tt.fields.SSHUserRegex
|
||||||
c := &Controller{
|
c := &Controller{
|
||||||
Interface: tt.fields.Interface,
|
Interface: tt.fields.Interface,
|
||||||
IdentityFunc: tt.fields.IdentityFunc,
|
IdentityFunc: tt.fields.IdentityFunc,
|
||||||
|
|
Loading…
Reference in a new issue