forked from TrueCloudLab/certificates
195 lines
6.4 KiB
Go
195 lines
6.4 KiB
Go
|
package provisioner
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"crypto/x509"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/pkg/errors"
|
||
|
"github.com/smallstep/certificates/errs"
|
||
|
"golang.org/x/crypto/ssh"
|
||
|
)
|
||
|
|
||
|
// Controller wraps a provisioner with other attributes useful in callback
|
||
|
// functions.
|
||
|
type Controller struct {
|
||
|
Interface
|
||
|
Audiences *Audiences
|
||
|
Claimer *Claimer
|
||
|
IdentityFunc GetIdentityFunc
|
||
|
AuthorizeRenewFunc AuthorizeRenewFunc
|
||
|
AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc
|
||
|
}
|
||
|
|
||
|
// NewController initializes a new provisioner controller.
|
||
|
func NewController(p Interface, claims *Claims, config Config) (*Controller, error) {
|
||
|
claimer, err := NewClaimer(claims, config.Claims)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return &Controller{
|
||
|
Interface: p,
|
||
|
Audiences: &config.Audiences,
|
||
|
Claimer: claimer,
|
||
|
IdentityFunc: config.GetIdentityFunc,
|
||
|
AuthorizeRenewFunc: config.AuthorizeRenewFunc,
|
||
|
AuthorizeSSHRenewFunc: config.AuthorizeSSHRenewFunc,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// GetIdentity returns the identity for a given email.
|
||
|
func (c *Controller) GetIdentity(ctx context.Context, email string) (*Identity, error) {
|
||
|
if c.IdentityFunc != nil {
|
||
|
return c.IdentityFunc(ctx, c.Interface, email)
|
||
|
}
|
||
|
return DefaultIdentityFunc(ctx, c.Interface, email)
|
||
|
}
|
||
|
|
||
|
// AuthorizeRenew returns nil if the given cert can be renewed, returns an error
|
||
|
// otherwise.
|
||
|
func (c *Controller) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
|
||
|
if c.AuthorizeRenewFunc != nil {
|
||
|
return c.AuthorizeRenewFunc(ctx, c, cert)
|
||
|
}
|
||
|
return DefaultAuthorizeRenew(ctx, c, cert)
|
||
|
}
|
||
|
|
||
|
// AuthorizeSSHRenew returns nil if the given cert can be renewed, returns an
|
||
|
// error otherwise.
|
||
|
func (c *Controller) AuthorizeSSHRenew(ctx context.Context, cert *ssh.Certificate) error {
|
||
|
if c.AuthorizeSSHRenewFunc != nil {
|
||
|
return c.AuthorizeSSHRenewFunc(ctx, c, cert)
|
||
|
}
|
||
|
return DefaultAuthorizeSSHRenew(ctx, c, cert)
|
||
|
}
|
||
|
|
||
|
// Identity is the type representing an externally supplied identity that is used
|
||
|
// by provisioners to populate certificate fields.
|
||
|
type Identity struct {
|
||
|
Usernames []string `json:"usernames"`
|
||
|
Permissions `json:"permissions"`
|
||
|
}
|
||
|
|
||
|
// GetIdentityFunc is a function that returns an identity.
|
||
|
type GetIdentityFunc func(ctx context.Context, p Interface, email string) (*Identity, error)
|
||
|
|
||
|
// AuthorizeRenewFunc is a function that returns nil if the renewal of a
|
||
|
// certificate is enabled.
|
||
|
type AuthorizeRenewFunc func(ctx context.Context, p *Controller, cert *x509.Certificate) error
|
||
|
|
||
|
// AuthorizeSSHRenewFunc is a function that returns nil if the renewal of the
|
||
|
// 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.
|
||
|
func DefaultIdentityFunc(ctx context.Context, p Interface, email string) (*Identity, error) {
|
||
|
switch k := p.(type) {
|
||
|
case *OIDC:
|
||
|
// OIDC principals would be:
|
||
|
// ~~1. Preferred usernames.~~ Note: Under discussion, currently disabled
|
||
|
// 2. Sanitized local.
|
||
|
// 3. Raw local (if different).
|
||
|
// 4. Email address.
|
||
|
name := SanitizeSSHUserPrincipal(email)
|
||
|
if !sshUserRegex.MatchString(name) {
|
||
|
return nil, errors.Errorf("invalid principal '%s' from email '%s'", name, email)
|
||
|
}
|
||
|
usernames := []string{name}
|
||
|
if i := strings.LastIndex(email, "@"); i >= 0 {
|
||
|
usernames = append(usernames, email[:i])
|
||
|
}
|
||
|
usernames = append(usernames, email)
|
||
|
return &Identity{
|
||
|
Usernames: SanitizeStringSlices(usernames),
|
||
|
}, nil
|
||
|
default:
|
||
|
return nil, errors.Errorf("provisioner type '%T' not supported by identity function", k)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// DefaultAuthorizeRenew is the default implementation of AuthorizeRenew. It
|
||
|
// will return an error if the provisioner has the renewal disabled, if the
|
||
|
// certificate is not yet valid or if the certificate is expired and renew after
|
||
|
// expiry is disabled.
|
||
|
func DefaultAuthorizeRenew(ctx context.Context, p *Controller, cert *x509.Certificate) error {
|
||
|
if p.Claimer.IsDisableRenewal() {
|
||
|
return errs.Unauthorized("renew is disabled for provisioner '%s'", p.GetName())
|
||
|
}
|
||
|
|
||
|
now := time.Now().Truncate(time.Second)
|
||
|
if now.Before(cert.NotBefore) {
|
||
|
return errs.Unauthorized("certificate is not yet valid")
|
||
|
}
|
||
|
if now.After(cert.NotAfter) && !p.Claimer.IsRenewAfterExpiry() {
|
||
|
return errs.Unauthorized("certificate has expired")
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// DefaultAuthorizeSSHRenew is the default implementation of AuthorizeSSHRenew. It
|
||
|
// will return an error if the provisioner has the renewal disabled, if the
|
||
|
// certificate is not yet valid or if the certificate is expired and renew after
|
||
|
// expiry is disabled.
|
||
|
func DefaultAuthorizeSSHRenew(ctx context.Context, p *Controller, cert *ssh.Certificate) error {
|
||
|
if p.Claimer.IsDisableRenewal() {
|
||
|
return errs.Unauthorized("renew is disabled for provisioner '%s'", p.GetName())
|
||
|
}
|
||
|
|
||
|
unixNow := time.Now().Unix()
|
||
|
if after := int64(cert.ValidAfter); after < 0 || unixNow < int64(cert.ValidAfter) {
|
||
|
return errs.Unauthorized("certificate is not yet valid")
|
||
|
}
|
||
|
if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) && !p.Claimer.IsRenewAfterExpiry() {
|
||
|
return errs.Unauthorized("certificate has expired")
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
var sshUserRegex = regexp.MustCompile("^[a-z][-a-z0-9_]*$")
|
||
|
|
||
|
// SanitizeStringSlices removes duplicated an empty strings.
|
||
|
func SanitizeStringSlices(original []string) []string {
|
||
|
output := []string{}
|
||
|
seen := make(map[string]struct{})
|
||
|
for _, entry := range original {
|
||
|
if entry == "" {
|
||
|
continue
|
||
|
}
|
||
|
if _, value := seen[entry]; !value {
|
||
|
seen[entry] = struct{}{}
|
||
|
output = append(output, entry)
|
||
|
}
|
||
|
}
|
||
|
return output
|
||
|
}
|
||
|
|
||
|
// SanitizeSSHUserPrincipal grabs an email or a string with the format
|
||
|
// local@domain and returns a sanitized version of the local, valid to be used
|
||
|
// as a user name. If the email starts with a letter between a and z, the
|
||
|
// resulting string will match the regular expression `^[a-z][-a-z0-9_]*$`.
|
||
|
func SanitizeSSHUserPrincipal(email string) string {
|
||
|
if i := strings.LastIndex(email, "@"); i >= 0 {
|
||
|
email = email[:i]
|
||
|
}
|
||
|
return strings.Map(func(r rune) rune {
|
||
|
switch {
|
||
|
case r >= 'a' && r <= 'z':
|
||
|
return r
|
||
|
case r >= '0' && r <= '9':
|
||
|
return r
|
||
|
case r == '-':
|
||
|
return '-'
|
||
|
case r == '.': // drop dots
|
||
|
return -1
|
||
|
default:
|
||
|
return '_'
|
||
|
}
|
||
|
}, strings.ToLower(email))
|
||
|
}
|