333 lines
8.8 KiB
Go
333 lines
8.8 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/user"
|
|
"strconv"
|
|
|
|
valid "github.com/asaskevich/govalidator"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
type (
|
|
/* *
|
|
* CONFIGURATION DATA *
|
|
* */
|
|
|
|
// UNIX socket configuration. This includes the full path to the socket
|
|
// as well as the group name and mode.
|
|
tSocketConfig struct {
|
|
Path string `yaml:"path"`
|
|
Group string `yaml:"group"`
|
|
Mode os.FileMode `yaml:"mode"`
|
|
}
|
|
|
|
// LDAP connection configuration, used for servers and as a way to specify
|
|
// defaults.
|
|
tLdapConnectionConfig struct {
|
|
Port uint16 `yaml:"port"`
|
|
TLS string `yaml:"tls"`
|
|
TLSNoVerify bool `yaml:"tls_skip_verify"`
|
|
CaChain string `yaml:"ca_chain"`
|
|
BindUser string `yaml:"bind_user"`
|
|
BindPassword string `yaml:"bind_password"`
|
|
}
|
|
|
|
// LDAP server configuration. This defines how to connect to a
|
|
// single, specific LDAP server.
|
|
tLdapServerConfig struct {
|
|
Host string `yaml:"host"`
|
|
tLdapConnectionConfig
|
|
}
|
|
|
|
// LDAP attributes and base DN configuration
|
|
tLdapStructureConfig struct {
|
|
BaseDN string `yaml:"base_dn"`
|
|
EndEntityCertificate string `yaml:"end_entity"`
|
|
CACertificate string `yaml:"ca_certificate"`
|
|
CAChaining string `yaml:"ca_chaining"`
|
|
}
|
|
|
|
// LDAP configuration: LDAP structure, connection defaults and server
|
|
// connections.
|
|
tLdapConfig struct {
|
|
Structure tLdapStructureConfig `yaml:"structure"`
|
|
Defaults tLdapConnectionConfig `yaml:"defaults"`
|
|
Servers []tLdapServerConfig `yaml:"servers"`
|
|
}
|
|
|
|
// Handlers. Each handler has a name and contains a list of commands.
|
|
tHandlers map[string][]string
|
|
|
|
// Certificate file updates configuration.
|
|
tCertFileUpdateConfig struct {
|
|
PreCommands []string `yaml:"pre_commands"`
|
|
Handlers []string `yaml:"handlers"`
|
|
PostCommands []string `yaml:"post_commands"`
|
|
}
|
|
|
|
// Certificate file configuration.
|
|
tCertificateFileConfig struct {
|
|
Path string `yaml:"path"`
|
|
Mode os.FileMode `yaml:"mode"`
|
|
Owner string `yaml:"owner"`
|
|
Group string `yaml:"group"`
|
|
PrependFiles []string `yaml:"prepend_files"`
|
|
Certificate string `yaml:"certificate"`
|
|
CACertificates []string `yaml:"ca"`
|
|
CAChainOf string `yaml:"ca_chain_of"`
|
|
Reverse bool `yaml:"reverse"`
|
|
AppendFiles []string `yaml:"append_files"`
|
|
AfterUpdate tCertFileUpdateConfig `yaml:"after_update"`
|
|
}
|
|
|
|
// Main configuration.
|
|
tConfiguration struct {
|
|
Socket *tSocketConfig `yaml:"socket"`
|
|
LdapConfig tLdapConfig `yaml:"ldap"`
|
|
Handlers tHandlers `yaml:"handlers"`
|
|
Certificates []tCertificateFileConfig `yaml:"certificates"`
|
|
}
|
|
)
|
|
|
|
// Helper function that checks whether a string corresponds to a group name.
|
|
func isValidGroup(name string) bool {
|
|
group, err := user.LookupGroup(name)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_, err = strconv.Atoi(group.Gid)
|
|
return err == nil
|
|
}
|
|
|
|
// Helper function that checks whether a string corresponds to a user name.
|
|
func isValidUser(name string) bool {
|
|
user, err := user.Lookup(name)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_, err = strconv.Atoi(user.Uid)
|
|
return err == nil
|
|
}
|
|
|
|
// Validate the UNIX socket configuration
|
|
func (c *tSocketConfig) Validate() error {
|
|
if c.Path == "" {
|
|
return fmt.Errorf("Missing socket path.")
|
|
}
|
|
if !valid.IsUnixFilePath(c.Path) {
|
|
return fmt.Errorf("Socket path '%s' is invalid.", c.Path)
|
|
}
|
|
if c.Group != "" && !isValidGroup(c.Group) {
|
|
return fmt.Errorf("Invalid group '%s'", c.Group)
|
|
}
|
|
if c.Mode == 0 {
|
|
c.Mode = 0640
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Check the LDAP structure configuration.
|
|
func (c *tLdapStructureConfig) Validate() error {
|
|
if c.EndEntityCertificate == "" {
|
|
return fmt.Errorf("Missing end entity certificate attribute name.")
|
|
}
|
|
if c.CACertificate == "" {
|
|
return fmt.Errorf("Missing CA certificate attribute name.")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Check the TLS field in LDAP configuration entries. If no port is specified,
|
|
// default it based on the TLS field.
|
|
func (c *tLdapConnectionConfig) Validate() error {
|
|
if c.TLS != "yes" && c.TLS != "starttls" && c.TLS != "no" {
|
|
return fmt.Errorf("Invalid TLS mode '%s' (valid values are 'yes', 'starttls' and 'no'", c.TLS)
|
|
}
|
|
if c.CaChain != "" {
|
|
data, err := ioutil.ReadFile(c.CaChain)
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to read CA chain from '%s': %w", c.CaChain, err)
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if !pool.AppendCertsFromPEM(data) {
|
|
return fmt.Errorf("Could not parse CA chain PEM from '%s'.", c.CaChain)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Copy defaults into a LDAP server configuration entry.
|
|
func (c *tLdapServerConfig) ApplyDefaults(dft tLdapConnectionConfig) {
|
|
if c.Port == 0 {
|
|
c.Port = dft.Port
|
|
}
|
|
if c.TLS == "" {
|
|
c.TLS = dft.TLS
|
|
}
|
|
// FIXME: I have no clue how I should handle TLSNoVerify
|
|
if c.CaChain == "" {
|
|
c.CaChain = dft.CaChain
|
|
}
|
|
if c.BindUser == "" {
|
|
c.BindUser = dft.BindUser
|
|
}
|
|
if c.BindPassword == "" {
|
|
c.BindPassword = dft.BindPassword
|
|
}
|
|
|
|
// Default port based on TLS mode
|
|
if c.Port == 0 {
|
|
if c.TLS == "starttls" {
|
|
c.Port = 636
|
|
} else {
|
|
c.Port = 389
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate a LDAP server configuration record.
|
|
func (c *tLdapServerConfig) Validate() error {
|
|
if c.Host == "" {
|
|
return fmt.Errorf("No host name in LDAP server configuration.")
|
|
}
|
|
if !valid.IsHost(c.Host) {
|
|
return fmt.Errorf("Invalid host name '%s'", c.Host)
|
|
}
|
|
return c.tLdapConnectionConfig.Validate()
|
|
}
|
|
|
|
// Validate the LDAP configuration
|
|
func (c *tLdapConfig) Validate() error {
|
|
err := c.Structure.Validate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = c.Defaults.Validate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(c.Servers) == 0 {
|
|
return fmt.Errorf("No LDAP servers have been configured.")
|
|
}
|
|
for i := range c.Servers {
|
|
c.Servers[i].ApplyDefaults(c.Defaults)
|
|
err = c.Servers[i].Validate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Check that a list of files contains only valid paths
|
|
func checkFileList(files []string) error {
|
|
for _, path := range files {
|
|
if !valid.IsUnixFilePath(path) {
|
|
return fmt.Errorf("Invalid path '%s'", path)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate the list of handles
|
|
func (c *tCertFileUpdateConfig) Validate(handlers *tHandlers) error {
|
|
set := make(map[string]bool)
|
|
for _, handler := range c.Handlers {
|
|
if _, exists := (*handlers)[handler]; !exists {
|
|
return fmt.Errorf("Handler '%s' does not exist.", handler)
|
|
}
|
|
if _, exists := set[handler]; exists {
|
|
return fmt.Errorf("Handler '%s' specified more than once.", handler)
|
|
}
|
|
set[handler] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate a certificate file configuration entry
|
|
func (c *tCertificateFileConfig) Validate(handlers *tHandlers) error {
|
|
if !valid.IsUnixFilePath(c.Path) {
|
|
return fmt.Errorf("Certificate file path '%s' is invalid.", c.Path)
|
|
}
|
|
if c.Owner != "" && !isValidUser(c.Owner) {
|
|
return fmt.Errorf("Unknown user '%s'", c.Owner)
|
|
}
|
|
if c.Group != "" && !isValidGroup(c.Group) {
|
|
return fmt.Errorf("Invalid group '%s'", c.Group)
|
|
}
|
|
err := checkFileList(c.PrependFiles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, path := range c.PrependFiles {
|
|
if !valid.IsUnixFilePath(path) {
|
|
return fmt.Errorf("Invalid path '%s'", path)
|
|
}
|
|
}
|
|
if c.Certificate == "" && len(c.CACertificates) == 0 && c.CAChainOf == "" {
|
|
return fmt.Errorf("Certificate path '%s' has no certificate or CA chain", c.Path)
|
|
}
|
|
if c.CAChainOf != "" && len(c.CACertificates) != 0 {
|
|
return fmt.Errorf("Certificate path '%s' uses both 'ca' and 'ca_chain_of'", c.Path)
|
|
}
|
|
err = checkFileList(c.AppendFiles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = c.AfterUpdate.Validate(handlers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate the configuration
|
|
func (c *tConfiguration) Validate() error {
|
|
if c.Socket != nil {
|
|
err := c.Socket.Validate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
err := c.LdapConfig.Validate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for idx, cfc := range c.Certificates {
|
|
if cfc.Path == "" {
|
|
return fmt.Errorf("Certificate file entry #%d has no path.", idx+1)
|
|
}
|
|
err = cfc.Validate(&c.Handlers)
|
|
if err != nil {
|
|
return fmt.Errorf("Certificate file %s (#%d): %s", cfc.Path, idx+1, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Create a configuration data structure containing default values.
|
|
func defaultConfiguration() tConfiguration {
|
|
cfg := tConfiguration{}
|
|
cfg.LdapConfig.Defaults.TLS = "no"
|
|
cfg.LdapConfig.Structure.CAChaining = "seeAlso"
|
|
return cfg
|
|
}
|
|
|
|
// Load and check the configuration file
|
|
func LoadConfiguration(file string) (tConfiguration, error) {
|
|
cfg := defaultConfiguration()
|
|
cfgData, err := ioutil.ReadFile(file)
|
|
if err != nil {
|
|
return cfg, fmt.Errorf("Could not load configuration: %w", err)
|
|
}
|
|
err = yaml.Unmarshal(cfgData, &cfg)
|
|
if err != nil {
|
|
return cfg, fmt.Errorf("Could not parse configuration: %w", err)
|
|
}
|
|
err = cfg.Validate()
|
|
return cfg, err
|
|
}
|