Merge pull request #1111 from slingamn/shellauth.1

fix #1107
This commit is contained in:
Shivaram Lingamneni
2020-06-04 07:46:23 -07:00
committed by GitHub
7 changed files with 256 additions and 26 deletions
+72 -26
View File
@@ -1029,6 +1029,18 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
return
}
func (am *AccountManager) loadWithAutocreation(accountName string, autocreate bool) (account ClientAccount, err error) {
account, err = am.LoadAccount(accountName)
if err == errAccountDoesNotExist && autocreate {
err = am.SARegister(accountName, "")
if err != nil {
return
}
account, err = am.LoadAccount(accountName)
}
return
}
func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) (err error) {
// XXX check this now, so we don't allow a redundant login for an always-on client
// even for a brief period. the other potential source of nick-account conflicts
@@ -1048,19 +1060,29 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
}
}()
ldapConf := am.server.Config().Accounts.LDAP
if ldapConf.Enabled {
config := am.server.Config()
if config.Accounts.LDAP.Enabled {
ldapConf := am.server.Config().Accounts.LDAP
err = ldap.CheckLDAPPassphrase(ldapConf, accountName, passphrase, am.server.logger)
if err == nil {
account, err = am.LoadAccount(accountName)
// autocreate if necessary:
if err == errAccountDoesNotExist && ldapConf.Autocreate {
err = am.SARegister(accountName, "")
if err != nil {
return
}
account, err = am.LoadAccount(accountName)
if err != nil {
account, err = am.loadWithAutocreation(accountName, ldapConf.Autocreate)
return
}
}
if config.Accounts.AuthScript.Enabled {
var output AuthScriptOutput
output, err = CheckAuthScript(config.Accounts.AuthScript,
AuthScriptInput{AccountName: accountName, Passphrase: passphrase, IP: client.IP().String()})
if err != nil {
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
return err
}
if output.Success {
if output.AccountName != "" {
accountName = output.AccountName
}
account, err = am.loadWithAutocreation(accountName, config.Accounts.AuthScript.Autocreate)
return
}
}
@@ -1361,15 +1383,50 @@ func (am *AccountManager) ChannelsForAccount(account string) (channels []string)
return unmarshalRegisteredChannels(channelStr)
}
func (am *AccountManager) AuthenticateByCertFP(client *Client, certfp, authzid string) error {
func (am *AccountManager) AuthenticateByCertFP(client *Client, certfp, authzid string) (err error) {
if certfp == "" {
return errAccountInvalidCredentials
}
var clientAccount ClientAccount
defer func() {
if err != nil {
return
} else if !clientAccount.Verified {
err = errAccountUnverified
return
}
// TODO(#1109) clean this check up?
if client.registered {
if clientAlready := am.server.clients.Get(clientAccount.Name); clientAlready != nil && clientAlready.AlwaysOn() {
err = errNickAccountMismatch
return
}
}
am.Login(client, clientAccount)
return
}()
config := am.server.Config()
if config.Accounts.AuthScript.Enabled {
var output AuthScriptOutput
output, err = CheckAuthScript(config.Accounts.AuthScript,
AuthScriptInput{Certfp: certfp, IP: client.IP().String()})
if err != nil {
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
return err
}
if output.Success && output.AccountName != "" {
clientAccount, err = am.loadWithAutocreation(output.AccountName, config.Accounts.AuthScript.Autocreate)
return
}
}
var account string
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
err := am.server.store.View(func(tx *buntdb.Tx) error {
err = am.server.store.View(func(tx *buntdb.Tx) error {
account, _ = tx.Get(certFPKey)
if account == "" {
return errAccountInvalidCredentials
@@ -1386,19 +1443,8 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client, certfp, authzid s
}
// ok, we found an account corresponding to their certificate
clientAccount, err := am.LoadAccount(account)
if err != nil {
return err
} else if !clientAccount.Verified {
return errAccountUnverified
}
if client.registered {
if clientAlready := am.server.clients.Get(clientAccount.Name); clientAlready != nil && clientAlready.AlwaysOn() {
return errNickAccountMismatch
}
}
am.Login(client, clientAccount)
return nil
clientAccount, err = am.LoadAccount(account)
return err
}
type settingsMunger func(input AccountSettings) (output AccountSettings, err error)
+110
View File
@@ -0,0 +1,110 @@
// Copyright (c) 2020 Shivaram Lingamneni
// released under the MIT license
package irc
import (
"bufio"
"encoding/json"
"fmt"
"io"
"os/exec"
"syscall"
"time"
)
// JSON-serializable input and output types for the script
type AuthScriptInput struct {
AccountName string `json:"accountName,omitempty"`
Passphrase string `json:"passphrase,omitempty"`
Certfp string `json:"certfp,omitempty"`
IP string `json:"ip,omitempty"`
}
type AuthScriptOutput struct {
AccountName string `json:"accountName"`
Success bool `json:"success"`
Error string `json:"error"`
}
// internal tupling of output and error for passing over a channel
type authScriptResponse struct {
output AuthScriptOutput
err error
}
func CheckAuthScript(config AuthScriptConfig, input AuthScriptInput) (output AuthScriptOutput, err error) {
inputBytes, err := json.Marshal(input)
if err != nil {
return
}
cmd := exec.Command(config.Command, config.Args...)
stdin, err := cmd.StdinPipe()
if err != nil {
return
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return
}
channel := make(chan authScriptResponse, 1)
err = cmd.Start()
if err != nil {
return
}
stdin.Write(inputBytes)
stdin.Write([]byte{'\n'})
// lots of potential race conditions here. we want to ensure that Wait()
// will be called, and will return, on the other goroutine, no matter
// where it is blocked. If it's blocked on ReadBytes(), we will kill it
// (first with SIGTERM, then with SIGKILL) and ReadBytes will return
// with EOF. If it's blocked on Wait(), then one of the kill signals
// will succeed and unblock it.
go processAuthScriptOutput(cmd, stdout, channel)
outputTimer := time.NewTimer(config.Timeout)
select {
case response := <-channel:
return response.output, response.err
case <-outputTimer.C:
}
err = errTimedOut
cmd.Process.Signal(syscall.SIGTERM)
termTimer := time.NewTimer(config.Timeout)
select {
case <-channel:
return
case <-termTimer.C:
}
cmd.Process.Kill()
return
}
func processAuthScriptOutput(cmd *exec.Cmd, stdout io.Reader, channel chan authScriptResponse) {
var response authScriptResponse
var out AuthScriptOutput
reader := bufio.NewReader(stdout)
outBytes, err := reader.ReadBytes('\n')
if err == nil {
err = json.Unmarshal(outBytes, &out)
if err == nil {
response.output = out
if out.Error != "" {
err = fmt.Errorf("Authentication process reported error: %s", out.Error)
}
}
}
response.err = err
// always call Wait() to ensure resource cleanup
err = cmd.Wait()
if err != nil {
response.err = err
}
channel <- response
}
+10
View File
@@ -278,6 +278,16 @@ type AccountConfig struct {
Multiclient MulticlientConfig
Bouncer *MulticlientConfig // # handle old name for 'multiclient'
VHosts VHostConfig
AuthScript AuthScriptConfig `yaml:"auth-script"`
}
type AuthScriptConfig struct {
Enabled bool
Command string
Args []string
Autocreate bool
Timeout time.Duration
KillTimeout time.Duration `yaml:"kill-timeout"`
}
// AccountRegistrationConfig controls account registration.
+1
View File
@@ -64,6 +64,7 @@ var (
errEmptyCredentials = errors.New("No more credentials are approved")
errCredsExternallyManaged = errors.New("Credentials are externally managed and cannot be changed here")
errInvalidMultilineBatch = errors.New("Invalid multiline batch")
errTimedOut = errors.New("Operation timed out")
)
// Socket Errors