Skip to content

Commit

Permalink
pam: Use pam.ModuleTransaction to implement pam module (#89)
Browse files Browse the repository at this point in the history
Re-implement the PAM module bootstrap code by using pam-go and in
particular the code proposed at msteinert/pam#13

That allows us to have a more tested and testable base. Also it will be
a prerequisite for the gdm implementation and a pure-PAM protocol
implementation.

The module can now generated by just using `go generate -C pam` that
would generate in two steps both the go-glue code and the library
itself.

See single commits for further details.

UDENG-1647, UDENG-1604
  • Loading branch information
denisonbarbosa authored Nov 30, 2023
2 parents 75084c6 + c974f78 commit 9151507
Show file tree
Hide file tree
Showing 19 changed files with 1,650 additions and 257 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/qa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ jobs:
- name: Run tests (with race detector)
run: |
go test -race ./...
- name: Run PAM tests (with Address Sanitizer)
env:
# Do not optimize, keep debug symbols and frame pointer for better
# stack trace information in case of ASAN errors.
CGO_CFLAGS: "-O0 -g3 -fno-omit-frame-pointer"
run: |
# Use `-dwarflocationlists` to give ASAN a better time to unwind the stack trace
go test -C pam -asan -gcflags="-dwarflocationlists=true" ./...
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
Expand Down
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/godbus/dbus/v5 v5.1.0
github.com/google/uuid v1.4.0
github.com/msteinert/pam v1.2.0
github.com/sirupsen/logrus v1.9.3
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/cobra v1.8.0
Expand Down Expand Up @@ -62,3 +63,8 @@ require (
golang.org/x/text v0.13.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect
)

// FIXME: Use released version once we have one!
// The branch below includes changes from this upstream PR:
// - https://github.com/msteinert/pam/pull/13
replace github.com/msteinert/pam => github.com/3v1n0/go-pam v0.0.0-20231130030658-0f1cc6f16d45
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/3v1n0/go-pam v0.0.0-20231130030658-0f1cc6f16d45 h1:EmfxQhrTNAVT4rzbv6TcP2XScDl16Q8J0ZlCVbazl8c=
github.com/3v1n0/go-pam v0.0.0-20231130030658-0f1cc6f16d45/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
Expand Down
5 changes: 5 additions & 0 deletions internal/log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ func Info(_ context.Context, args ...interface{}) {
logrus.Info(args...)
}

// Warning is a temporary placeholder.
func Warning(_ context.Context, args ...interface{}) {
logrus.Warning(args...)
}

// Warningf is a temporary placeholder.
func Warningf(_ context.Context, format string, args ...interface{}) {
logrus.Warningf(format, args...)
Expand Down
2 changes: 2 additions & 0 deletions pam/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
./*.h
*.so
45 changes: 28 additions & 17 deletions pam/authentication.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package main is the package for the PAM library
package main

import (
Expand All @@ -7,6 +8,7 @@ import (

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/msteinert/pam"
"github.com/ubuntu/authd"
"github.com/ubuntu/authd/internal/brokers/responses"
"github.com/ubuntu/authd/internal/log"
Expand All @@ -32,8 +34,9 @@ func sendIsAuthenticated(ctx context.Context, client authd.PAMClient, sessionID,
access: responses.AuthCancelled,
}
}
return pamSystemError{
msg: fmt.Sprintf("Authentication status failure: %v", err),
return pamError{
status: pam.ErrSystem,
msg: fmt.Sprintf("authentication status failure: %v", err),
}
}

Expand Down Expand Up @@ -126,15 +129,22 @@ func (m *authenticationModel) Update(msg tea.Msg) (authenticationModel, tea.Cmd)
return *m, sendEvent(pamSuccess{brokerID: m.currentBrokerID})

case responses.AuthRetry:
m.errorMsg = dataToMsg(msg.msg)
errorMsg, err := dataToMsg(msg.msg)
if err != nil {
return *m, sendEvent(pamError{status: pam.ErrSystem, msg: err.Error()})
}
m.errorMsg = errorMsg
return *m, sendEvent(startAuthentication{})

case responses.AuthDenied:
errMsg := "Access denied"
if err := dataToMsg(msg.msg); err != "" {
errMsg = err
errMsg, err := dataToMsg(msg.msg)
if err != nil {
return *m, sendEvent(pamError{status: pam.ErrSystem, msg: err.Error()})
}
if errMsg == "" {
errMsg = "Access denied"
}
return *m, sendEvent(pamAuthError{msg: errMsg})
return *m, sendEvent(pamError{status: pam.ErrAuth, msg: errMsg})

case responses.AuthNext:
return *m, sendEvent(GetAuthenticationModesRequested{})
Expand Down Expand Up @@ -204,7 +214,7 @@ func (m *authenticationModel) Compose(brokerID, sessionID string, layout *authd.
case "qrcode":
qrcodeModel, err := newQRCodeModel(layout.GetContent(), layout.GetLabel(), layout.GetButton(), layout.GetWait() == "true")
if err != nil {
return sendEvent(pamSystemError{msg: err.Error()})
return sendEvent(pamError{status: pam.ErrSystem, msg: err.Error()})
}
m.currentModel = qrcodeModel

Expand All @@ -213,7 +223,10 @@ func (m *authenticationModel) Compose(brokerID, sessionID string, layout *authd.
m.currentModel = newPasswordModel

default:
return sendEvent(pamSystemError{msg: fmt.Sprintf("unknown layout type: %q", layout.Type)})
return sendEvent(pamError{
status: pam.ErrSystem,
msg: fmt.Sprintf("unknown layout type: %q", layout.Type),
})
}

return sendEvent(startAuthentication{})
Expand Down Expand Up @@ -246,24 +259,22 @@ func (m *authenticationModel) Reset() {
}

// dataToMsg returns the data message from a given JSON message.
func dataToMsg(data string) string {
func dataToMsg(data string) (string, error) {
if data == "" {
return ""
return "", nil
}

v := make(map[string]string)
if err := json.Unmarshal([]byte(data), &v); err != nil {
log.Infof(context.TODO(), "Invalid json data from provider: %v", data)
return ""
return "", fmt.Errorf("invalid json data from provider: %v", err)
}
if len(v) == 0 {
return ""
return "", nil
}

r, ok := v["message"]
if !ok {
log.Debugf(context.TODO(), "No message entry in json data from provider: %v", data)
return ""
return "", fmt.Errorf("no message entry in json data from provider: %v", v)
}
return r
return r, nil
}
11 changes: 5 additions & 6 deletions pam/authmodeselection.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/msteinert/pam"
"github.com/ubuntu/authd"
"github.com/ubuntu/authd/internal/log"
)
Expand Down Expand Up @@ -241,17 +242,15 @@ func getAuthenticationModes(client authd.PAMClient, sessionID string, uiLayouts

gamResp, err := client.GetAuthenticationModes(context.Background(), gamReq)
if err != nil {
return pamSystemError{
msg: fmt.Sprintf("could not get authentication modes: %v", err),
return pamError{
status: pam.ErrSystem,
msg: fmt.Sprintf("could not get authentication modes: %v", err),
}
}

authModes := gamResp.GetAuthenticationModes()
if len(authModes) == 0 {
return pamIgnore{
// TODO: probably go back to broker selection here
msg: "no supported authentication mode available for this provider",
}
return pamIgnore{msg: "no supported authentication mode available for this provider"}
}
log.Info(context.TODO(), authModes)

Expand Down
6 changes: 4 additions & 2 deletions pam/brokerselection.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/msteinert/pam"
"github.com/ubuntu/authd"
"github.com/ubuntu/authd/internal/log"
)
Expand Down Expand Up @@ -232,8 +233,9 @@ func getAvailableBrokers(client authd.PAMClient) tea.Cmd {
return func() tea.Msg {
brokersInfo, err := client.AvailableBrokers(context.TODO(), &authd.Empty{})
if err != nil {
return pamSystemError{
msg: fmt.Sprintf("could not get current available brokers: %v", err),
return pamError{
status: pam.ErrSystem,
msg: fmt.Sprintf("could not get current available brokers: %v", err),
}
}

Expand Down
21 changes: 12 additions & 9 deletions pam/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/msteinert/pam"
"github.com/ubuntu/authd"
"github.com/ubuntu/authd/internal/log"
)
Expand Down Expand Up @@ -44,16 +45,16 @@ func startBrokerSession(client authd.PAMClient, brokerID, username string) tea.C

sbResp, err := client.SelectBroker(context.TODO(), sbReq)
if err != nil {
return pamSystemError{msg: fmt.Sprintf("can't select broker: %v", err)}
return pamError{status: pam.ErrSystem, msg: fmt.Sprintf("can't select broker: %v", err)}
}

sessionID := sbResp.GetSessionId()
if sessionID == "" {
return pamSystemError{msg: "no session ID returned by broker"}
return pamError{status: pam.ErrSystem, msg: "no session ID returned by broker"}
}
encryptionKey := sbResp.GetEncryptionKey()
if encryptionKey == "" {
return pamSystemError{msg: "no encryption key returned by broker"}
return pamError{status: pam.ErrSystem, msg: "no encryption key returned by broker"}
}

return SessionStarted{
Expand All @@ -73,16 +74,18 @@ func getLayout(client authd.PAMClient, sessionID, authModeID string) tea.Cmd {
}
uiInfo, err := client.SelectAuthenticationMode(context.TODO(), samReq)
if err != nil {
return pamSystemError{
// TODO: probably go back to broker selection here
msg: fmt.Sprintf("can't select authentication mode: %v", err),
// TODO: probably go back to broker selection here
return pamError{
status: pam.ErrSystem,
msg: fmt.Sprintf("can't select authentication mode: %v", err),
}
}

if uiInfo.UiLayoutInfo == nil {
return pamSystemError{
// TODO: probably go back to broker selection here
msg: "invalid empty UI Layout information from broker",
// TODO: probably go back to broker selection here
return pamError{
status: pam.ErrSystem,
msg: "invalid empty UI Layout information from broker",
}
}

Expand Down
45 changes: 45 additions & 0 deletions pam/main-cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//go:build pam_binary_cli

package main

import (
"fmt"
"os"

"github.com/msteinert/pam"
"github.com/sirupsen/logrus"
"github.com/ubuntu/authd/internal/log"
"github.com/ubuntu/authd/pam/pam_test"
)

// Simulating pam on the CLI for manual testing.
func main() {
log.SetLevel(log.DebugLevel)
f, err := os.OpenFile("/tmp/logdebug", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
if err != nil {
panic(err)
}
defer f.Close()
logrus.SetOutput(f)

module := &pamModule{}
mTx := pam_test.NewModuleTransactionDummy(pam.ConversationFunc(
func(style pam.Style, msg string) (string, error) {
switch style {
case pam.TextInfo:
fmt.Fprintf(os.Stderr, "PAM INFO: %s\n", msg)
case pam.ErrorMsg:
fmt.Fprintf(os.Stderr, "PAM ERROR: %s\n", msg)
default:
return "", fmt.Errorf("pam style %d not implemented", style)
}
return "", nil
}))

authResult := module.Authenticate(mTx, pam.Flags(0), nil)
fmt.Println("Auth return:", authResult)

// Simulate setting auth broker as default.
accMgmtResult := module.AcctMgmt(mTx, pam.Flags(0), nil)
fmt.Println("Acct mgmt return:", accMgmtResult)
}
35 changes: 15 additions & 20 deletions pam/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package main

import (
"context"
"fmt"
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/msteinert/pam"
"github.com/ubuntu/authd"
"github.com/ubuntu/authd/internal/log"
)
Expand Down Expand Up @@ -35,7 +35,7 @@ type sessionInfo struct {

// model is the global models orchestrator.
type model struct {
pamh pamHandle
pamMTx pam.ModuleTransaction
client authd.PAMClient

height int
Expand All @@ -49,7 +49,7 @@ type model struct {
authModeSelectionModel authModeSelectionModel
authenticationModel authenticationModel

exitMsg fmt.Stringer
exitStatus pamReturnStatus
}

/* global events */
Expand Down Expand Up @@ -87,7 +87,8 @@ type SessionEnded struct{}

// Init initializes the main model orchestrator.
func (m *model) Init() tea.Cmd {
m.userSelectionModel = newUserSelectionModel(m.pamh)
m.exitStatus = pamError{status: pam.ErrSystem, msg: "model did not return anything"}
m.userSelectionModel = newUserSelectionModel(m.pamMTx)
var cmds []tea.Cmd
cmds = append(cmds, m.userSelectionModel.Init())

Expand All @@ -113,7 +114,10 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
return m, sendEvent(pamAbort{msg: "cancel requested"})
return m, sendEvent(pamError{
status: pam.ErrAbort,
msg: "cancel requested",
})
case "esc":
if m.brokerSelectionModel.WillCaptureEscape() || m.authModeSelectionModel.WillCaptureEscape() {
break
Expand All @@ -137,20 +141,8 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.brokerSelectionModel.SetWidth(m.width)

// Exit cases
case pamIgnore:
m.exitMsg = msg
return m, m.quit()
case pamAbort:
m.exitMsg = msg
return m, m.quit()
case pamSystemError:
m.exitMsg = msg
return m, m.quit()
case pamAuthError:
m.exitMsg = msg
return m, m.quit()
case pamSuccess:
m.exitMsg = msg
case pamReturnStatus:
m.exitStatus = msg
return m, m.quit()

// Events
Expand Down Expand Up @@ -194,7 +186,10 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
msg.ID = m.authModeSelectionModel.currentAuthModeSelectedID
}
if msg.ID == "" {
return m, sendEvent(pamSystemError{msg: "reselection of current auth mode without current ID"})
return m, sendEvent(pamError{
status: pam.ErrSystem,
msg: "reselection of current auth mode without current ID",
})
}
return m, getLayout(m.client, m.currentSession.sessionID, msg.ID)

Expand Down
Loading

0 comments on commit 9151507

Please sign in to comment.