forked from TrueCloudLab/certificates
fd6a2eeb9c
The provisioner controller has the implementation of the identity function as well as the renew methods with renew after expiry support.
194 lines
6.4 KiB
Go
194 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))
|
|
}
|