Merge branch 'pam-moduler'
This commit is contained in:
111
.clang-format
Normal file
111
.clang-format
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# clang-format configuration file. For more information, see:
|
||||||
|
#
|
||||||
|
# https://clang.llvm.org/docs/ClangFormat.html
|
||||||
|
# https://clang.llvm.org/docs/ClangFormatStyleOptions.html
|
||||||
|
#
|
||||||
|
---
|
||||||
|
AccessModifierOffset: -4
|
||||||
|
AlignAfterOpenBracket: Align
|
||||||
|
AlignConsecutiveAssignments: false
|
||||||
|
AlignConsecutiveDeclarations: false
|
||||||
|
AlignEscapedNewlines: Left
|
||||||
|
AlignOperands: true
|
||||||
|
AlignTrailingComments: false
|
||||||
|
AllowAllParametersOfDeclarationOnNextLine: false
|
||||||
|
AllowShortBlocksOnASingleLine: false
|
||||||
|
AllowShortCaseLabelsOnASingleLine: false
|
||||||
|
AllowShortFunctionsOnASingleLine: None
|
||||||
|
AllowShortIfStatementsOnASingleLine: false
|
||||||
|
AllowShortLoopsOnASingleLine: false
|
||||||
|
AlwaysBreakAfterDefinitionReturnType: None
|
||||||
|
AlwaysBreakAfterReturnType: None
|
||||||
|
AlwaysBreakBeforeMultilineStrings: false
|
||||||
|
AlwaysBreakTemplateDeclarations: false
|
||||||
|
BinPackArguments: true
|
||||||
|
BinPackParameters: true
|
||||||
|
BraceWrapping:
|
||||||
|
AfterClass: false
|
||||||
|
AfterControlStatement: false
|
||||||
|
AfterEnum: false
|
||||||
|
AfterFunction: true
|
||||||
|
AfterNamespace: true
|
||||||
|
AfterObjCDeclaration: false
|
||||||
|
AfterStruct: false
|
||||||
|
AfterUnion: false
|
||||||
|
AfterExternBlock: false
|
||||||
|
BeforeCatch: false
|
||||||
|
BeforeElse: false
|
||||||
|
IndentBraces: false
|
||||||
|
SplitEmptyFunction: true
|
||||||
|
SplitEmptyRecord: true
|
||||||
|
SplitEmptyNamespace: true
|
||||||
|
BreakBeforeBinaryOperators: None
|
||||||
|
BreakBeforeBraces: Custom
|
||||||
|
BreakBeforeInheritanceComma: false
|
||||||
|
BreakBeforeTernaryOperators: true
|
||||||
|
BreakConstructorInitializersBeforeComma: false
|
||||||
|
BreakConstructorInitializers: BeforeComma
|
||||||
|
BreakStringLiterals: false
|
||||||
|
ColumnLimit: 120
|
||||||
|
CompactNamespaces: false
|
||||||
|
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||||
|
ConstructorInitializerIndentWidth: 8
|
||||||
|
ContinuationIndentWidth: 8
|
||||||
|
DerivePointerAlignment: false
|
||||||
|
DisableFormat: false
|
||||||
|
ExperimentalAutoDetectBinPacking: false
|
||||||
|
FixNamespaceComments: false
|
||||||
|
IncludeBlocks: Regroup
|
||||||
|
IncludeCategories:
|
||||||
|
- Regex: '^"(allez)/'
|
||||||
|
Priority: 2
|
||||||
|
SortPriority: 2
|
||||||
|
CaseSensitive: true
|
||||||
|
- Regex: '.*'
|
||||||
|
Priority: 1
|
||||||
|
SortPriority: 0
|
||||||
|
IndentCaseLabels: false
|
||||||
|
IndentGotoLabels: false
|
||||||
|
IndentPPDirectives: None
|
||||||
|
IndentWidth: 8
|
||||||
|
IndentWrappedFunctionNames: false
|
||||||
|
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||||
|
MacroBlockBegin: ''
|
||||||
|
MacroBlockEnd: ''
|
||||||
|
MaxEmptyLinesToKeep: 1
|
||||||
|
NamespaceIndentation: None
|
||||||
|
ObjCBinPackProtocolList: Auto
|
||||||
|
ObjCBlockIndentWidth: 8
|
||||||
|
ObjCSpaceAfterProperty: true
|
||||||
|
ObjCSpaceBeforeProtocolList: true
|
||||||
|
|
||||||
|
# Taken from git's rules
|
||||||
|
PenaltyBreakAssignment: 10
|
||||||
|
PenaltyBreakBeforeFirstCallParameter: 30
|
||||||
|
PenaltyBreakComment: 10
|
||||||
|
PenaltyBreakFirstLessLess: 0
|
||||||
|
PenaltyBreakString: 10
|
||||||
|
PenaltyExcessCharacter: 2
|
||||||
|
PenaltyReturnTypeOnItsOwnLine: 60
|
||||||
|
|
||||||
|
PointerAlignment: Right
|
||||||
|
ReflowComments: false
|
||||||
|
SortIncludes: true
|
||||||
|
SortUsingDeclarations: false
|
||||||
|
SpaceAfterCStyleCast: false
|
||||||
|
SpaceAfterTemplateKeyword: true
|
||||||
|
SpaceBeforeAssignmentOperators: true
|
||||||
|
SpaceBeforeCtorInitializerColon: true
|
||||||
|
SpaceBeforeInheritanceColon: true
|
||||||
|
SpaceBeforeParens: ControlStatementsExceptForEachMacros
|
||||||
|
SpaceBeforeRangeBasedForLoopColon: true
|
||||||
|
SpaceInEmptyParentheses: false
|
||||||
|
SpacesBeforeTrailingComments: 1
|
||||||
|
SpacesInAngles: false
|
||||||
|
SpacesInContainerLiterals: false
|
||||||
|
SpacesInCStyleCastParentheses: false
|
||||||
|
SpacesInParentheses: false
|
||||||
|
SpacesInSquareBrackets: false
|
||||||
|
TabWidth: 8
|
||||||
|
UseTab: Always
|
||||||
|
...
|
||||||
3
.codecov.yml
Normal file
3
.codecov.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ignore:
|
||||||
|
# Ignore pam-moduler generated files
|
||||||
|
- "**/pam_module.go"
|
||||||
22
.github/workflows/lint.yaml
vendored
Normal file
22
.github/workflows/lint.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
on: [push, pull_request]
|
||||||
|
name: Lint
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
golangci:
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
cache: false
|
||||||
|
- name: Install PAM
|
||||||
|
run: sudo apt install -y libpam-dev
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: v1.54
|
||||||
39
.github/workflows/test.yaml
vendored
39
.github/workflows/test.yaml
vendored
@@ -4,19 +4,48 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.19.x, 1.20.x]
|
go-version: [1.20.x, 1.21.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
- name: Install PAM
|
- name: Install PAM
|
||||||
run: sudo apt install -y libpam-dev
|
run: |
|
||||||
|
sudo apt update -y
|
||||||
|
sudo apt install -y libpam-dev
|
||||||
|
- name: Install Debug symbols
|
||||||
|
run: |
|
||||||
|
sudo apt install -y ubuntu-dev-tools
|
||||||
|
(cd /tmp && pull-lp-ddebs libpam0g $(lsb_release -c -s))
|
||||||
|
(cd /tmp && pull-lp-ddebs libpam-modules $(lsb_release -c -s))
|
||||||
|
sudo dpkg -i /tmp/libpam*-dbgsym_*.ddeb
|
||||||
- name: Add a test user
|
- name: Add a test user
|
||||||
run: sudo useradd -d /tmp/test -p '$1$Qd8H95T5$RYSZQeoFbEB.gS19zS99A0' -s /bin/false test
|
run: sudo useradd -d /tmp/test -p '$1$Qd8H95T5$RYSZQeoFbEB.gS19zS99A0' -s /bin/false test
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Test
|
- name: Test
|
||||||
run: sudo go test -v ./...
|
run: sudo go test -v -cover -coverprofile=coverage.out -coverpkg=./... ./...
|
||||||
|
- name: Test with Address Sanitizer
|
||||||
|
env:
|
||||||
|
GO_PAM_TEST_WITH_ASAN: true
|
||||||
|
CGO_CFLAGS: "-O0 -g3 -fno-omit-frame-pointer"
|
||||||
|
run: |
|
||||||
|
# Do not run sudo-requiring go tests because as PAM has some leaks in 22.04
|
||||||
|
go test -v -asan -cover -coverprofile=coverage-asan-tx.out -coverpkg=./... -gcflags=all="-N -l"
|
||||||
|
|
||||||
|
# Run the rest of tests normally
|
||||||
|
sudo go test -v -cover -coverprofile=coverage-asan-module.out -coverpkg=./... -asan -gcflags=all="-N -l" -run Module
|
||||||
|
sudo go test -C cmd -coverprofile=coverage-asan.out -v -coverpkg=./... -asan -gcflags=all="-N -l" ./...
|
||||||
|
- name: Generate example module
|
||||||
|
run: |
|
||||||
|
rm -f example-module/pam_go.so
|
||||||
|
go generate -C example-module -v
|
||||||
|
test -e example-module/pam_go.so
|
||||||
|
git diff --exit-code example-module
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
coverage*.out
|
||||||
|
example-module/*.so
|
||||||
|
example-module/*.h
|
||||||
|
cmd/pam-moduler/tests/*/*.so
|
||||||
|
cmd/pam-moduler/tests/*/*.h
|
||||||
61
.golangci.yaml
Normal file
61
.golangci.yaml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# This is for linting. To run it, please use:
|
||||||
|
# golangci-lint run ${MODULE}/... [--fix]
|
||||||
|
|
||||||
|
linters:
|
||||||
|
# linters to run in addition to default ones
|
||||||
|
enable:
|
||||||
|
- dupl
|
||||||
|
- durationcheck
|
||||||
|
- errname
|
||||||
|
- errorlint
|
||||||
|
- exportloopref
|
||||||
|
- forbidigo
|
||||||
|
- forcetypeassert
|
||||||
|
- gci
|
||||||
|
- godot
|
||||||
|
- gofmt
|
||||||
|
- gosec
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- nolintlint
|
||||||
|
- revive
|
||||||
|
- thelper
|
||||||
|
- tparallel
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- whitespace
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
|
||||||
|
# Get all linter issues, even if duplicated
|
||||||
|
issues:
|
||||||
|
exclude-use-default: false
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
|
fix: false # we don’t want this in CI
|
||||||
|
exclude:
|
||||||
|
# EXC0001 errcheck: most errors are in defer calls, which are safe to ignore and idiomatic Go (would be good to only ignore defer ones though)
|
||||||
|
- 'Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv|w\.Stop). is not checked'
|
||||||
|
# EXC0008 gosec: duplicated of errcheck
|
||||||
|
- (G104|G307)
|
||||||
|
# EXC0010 gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)'
|
||||||
|
- Potential file inclusion via variable
|
||||||
|
# We want named parameters even if unused, as they help better document the function
|
||||||
|
- unused-parameter
|
||||||
|
# Sometimes it is more readable it do a `if err:=a(); err != nil` tha simpy `return a()`
|
||||||
|
- if-return
|
||||||
|
|
||||||
|
nolintlint:
|
||||||
|
require-explanation: true
|
||||||
|
require-specific: true
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
# Forbid the usage of deprecated ioutil and debug prints
|
||||||
|
forbidigo:
|
||||||
|
forbid:
|
||||||
|
- ioutil\.
|
||||||
|
- ^print.*$
|
||||||
|
# Never have naked return ever
|
||||||
|
nakedret:
|
||||||
|
max-func-lines: 1
|
||||||
127
README.md
127
README.md
@@ -1,10 +1,128 @@
|
|||||||
[](http://godoc.org/github.com/msteinert/pam)
|
[](http://godoc.org/github.com/msteinert/pam/v2)
|
||||||
[](https://goreportcard.com/report/github.com/msteinert/pam)
|
[](https://codecov.io/gh/msteinert/pam)
|
||||||
|
[](https://goreportcard.com/report/github.com/msteinert/pam/v2)
|
||||||
|
|
||||||
# Go PAM
|
# Go PAM
|
||||||
|
|
||||||
This is a Go wrapper for the PAM application API.
|
This is a Go wrapper for the PAM application API.
|
||||||
|
|
||||||
|
## Module support
|
||||||
|
|
||||||
|
Go PAM can also used to create PAM modules in a simple way, using the go.
|
||||||
|
|
||||||
|
The code can be generated using [pam-moduler](cmd/pam-moduler/moduler.go) and
|
||||||
|
an example how to use it using `go generate` create them is available as an
|
||||||
|
[example module](example-module/module.go).
|
||||||
|
|
||||||
|
### Modules and PAM applications
|
||||||
|
|
||||||
|
The modules generated with go can be used by any PAM application, however there
|
||||||
|
are some caveats, in fact a Go shared library could misbehave when loaded
|
||||||
|
improperly. In particular if a Go shared library is loaded and then the program
|
||||||
|
`fork`s, the library will have an undefined behavior.
|
||||||
|
|
||||||
|
This is the case of SSHd that loads a pam library before forking, making any
|
||||||
|
go PAM library to make it hang.
|
||||||
|
|
||||||
|
To solve this case, we can use a little workaround: to ensure that the go
|
||||||
|
library is loaded only after the program has forked, we can just `dload` it once
|
||||||
|
a PAM library is called, in this way go code will be loaded only after that the
|
||||||
|
PAM application has `fork`'ed.
|
||||||
|
|
||||||
|
To do this, we can use a very simple wrapper written in C:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <security/pam_modules.h>
|
||||||
|
#include <security/pam_ext.h>
|
||||||
|
|
||||||
|
typedef int (*PamHandler)(pam_handle_t *,
|
||||||
|
int flags,
|
||||||
|
int argc,
|
||||||
|
const char **argv);
|
||||||
|
|
||||||
|
static void
|
||||||
|
on_go_module_removed (pam_handle_t *pamh,
|
||||||
|
void *go_module,
|
||||||
|
int error_status)
|
||||||
|
{
|
||||||
|
dlclose (go_module);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *
|
||||||
|
load_module (pam_handle_t *pamh,
|
||||||
|
const char *module_path)
|
||||||
|
{
|
||||||
|
void *go_module;
|
||||||
|
|
||||||
|
if (pam_get_data (pamh, "go-module", (const void **) &go_module) == PAM_SUCCESS)
|
||||||
|
return go_module;
|
||||||
|
|
||||||
|
go_module = dlopen (module_path, RTLD_LAZY);
|
||||||
|
if (!go_module)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
pam_set_data (pamh, "go-module", go_module, on_go_module_removed);
|
||||||
|
|
||||||
|
return go_module;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int
|
||||||
|
call_pam_function (pam_handle_t *pamh,
|
||||||
|
const char *function,
|
||||||
|
int flags,
|
||||||
|
int argc,
|
||||||
|
const char **argv)
|
||||||
|
{
|
||||||
|
char module_path[PATH_MAX] = {0};
|
||||||
|
const char *sub_module;
|
||||||
|
PamHandler func;
|
||||||
|
void *go_module;
|
||||||
|
|
||||||
|
if (argc < 1)
|
||||||
|
{
|
||||||
|
pam_error (pamh, "%s: no module provided", function);
|
||||||
|
return PAM_MODULE_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_module = argv[0];
|
||||||
|
argc -= 1;
|
||||||
|
argv = (argc == 0) ? NULL : &argv[1];
|
||||||
|
|
||||||
|
strncpy (module_path, sub_module, PATH_MAX - 1);
|
||||||
|
|
||||||
|
go_module = load_module (pamh, module_path);
|
||||||
|
if (!go_module)
|
||||||
|
{
|
||||||
|
pam_error (pamh, "Impossible to load module %s", module_path);
|
||||||
|
return PAM_OPEN_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
*(void **) (&func) = dlsym (go_module, function);
|
||||||
|
if (!func)
|
||||||
|
{
|
||||||
|
pam_error (pamh, "Symbol %s not found in %s", function, module_path);
|
||||||
|
return PAM_OPEN_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return func (pamh, flags, argc, argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define DEFINE_PAM_WRAPPER(name) \
|
||||||
|
PAM_EXTERN int \
|
||||||
|
(pam_sm_ ## name) (pam_handle_t * pamh, int flags, int argc, const char **argv) \
|
||||||
|
{ \
|
||||||
|
return call_pam_function (pamh, "pam_sm_" #name, flags, argc, argv); \
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINE_PAM_WRAPPER (authenticate)
|
||||||
|
DEFINE_PAM_WRAPPER (chauthtok)
|
||||||
|
DEFINE_PAM_WRAPPER (close_session)
|
||||||
|
DEFINE_PAM_WRAPPER (open_session)
|
||||||
|
DEFINE_PAM_WRAPPER (setcred)
|
||||||
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
To run the full suite, the tests must be run as the root user. To setup your
|
To run the full suite, the tests must be run as the root user. To setup your
|
||||||
@@ -24,5 +142,8 @@ Then execute the tests:
|
|||||||
$ sudo GOPATH=$GOPATH $(which go) test -v
|
$ sudo GOPATH=$GOPATH $(which go) test -v
|
||||||
```
|
```
|
||||||
|
|
||||||
[1]: http://godoc.org/github.com/msteinert/pam
|
Other tests can instead run as user without any setup with
|
||||||
|
normal `go test ./...`
|
||||||
|
|
||||||
|
[1]: http://godoc.org/github.com/msteinert/pam/v2
|
||||||
[2]: http://www.linux-pam.org/Linux-PAM-html/Linux-PAM_ADG.html
|
[2]: http://www.linux-pam.org/Linux-PAM-html/Linux-PAM_ADG.html
|
||||||
|
|||||||
292
app-transaction.go
Normal file
292
app-transaction.go
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
//go:build !go_pam_module
|
||||||
|
|
||||||
|
package pam
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include "transaction.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime/cgo"
|
||||||
|
"sync/atomic"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConversationHandler is an interface for objects that can be used as
|
||||||
|
// conversation callbacks during PAM authentication.
|
||||||
|
type ConversationHandler interface {
|
||||||
|
// RespondPAM receives a message style and a message string. If the
|
||||||
|
// message Style is PromptEchoOff or PromptEchoOn then the function
|
||||||
|
// should return a response string.
|
||||||
|
RespondPAM(Style, string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinaryConversationHandler is an interface for objects that can be used as
|
||||||
|
// conversation callbacks during PAM authentication if binary protocol is going
|
||||||
|
// to be supported.
|
||||||
|
type BinaryConversationHandler interface {
|
||||||
|
ConversationHandler
|
||||||
|
// RespondPAMBinary receives a pointer to the binary message. It's up to
|
||||||
|
// the receiver to parse it according to the protocol specifications.
|
||||||
|
// The function can return a byte array that will passed as pointer back
|
||||||
|
// to the module.
|
||||||
|
RespondPAMBinary(BinaryPointer) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinaryPointerConversationHandler is an interface for objects that can be used as
|
||||||
|
// conversation callbacks during PAM authentication if binary protocol is going
|
||||||
|
// to be supported.
|
||||||
|
type BinaryPointerConversationHandler interface {
|
||||||
|
ConversationHandler
|
||||||
|
// RespondPAMBinary receives a pointer to the binary message. It's up to
|
||||||
|
// the receiver to parse it according to the protocol specifications.
|
||||||
|
// The function must return a pointer that is allocated via malloc or
|
||||||
|
// similar, as it's expected to be free'd by the conversation handler.
|
||||||
|
RespondPAMBinary(BinaryPointer) (BinaryPointer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConversationFunc is an adapter to allow the use of ordinary functions as
|
||||||
|
// conversation callbacks.
|
||||||
|
type ConversationFunc func(Style, string) (string, error)
|
||||||
|
|
||||||
|
// RespondPAM is a conversation callback adapter.
|
||||||
|
func (f ConversationFunc) RespondPAM(s Style, msg string) (string, error) {
|
||||||
|
return f(s, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinaryConversationFunc is an adapter to allow the use of ordinary functions
|
||||||
|
// as binary (only) conversation callbacks.
|
||||||
|
type BinaryConversationFunc func(BinaryPointer) ([]byte, error)
|
||||||
|
|
||||||
|
// RespondPAMBinary is a conversation callback adapter.
|
||||||
|
func (f BinaryConversationFunc) RespondPAMBinary(ptr BinaryPointer) ([]byte, error) {
|
||||||
|
return f(ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RespondPAM is a dummy conversation callback adapter.
|
||||||
|
func (f BinaryConversationFunc) RespondPAM(Style, string) (string, error) {
|
||||||
|
return "", ErrConv
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinaryPointerConversationFunc is an adapter to allow the use of ordinary
|
||||||
|
// functions as binary pointer (only) conversation callbacks.
|
||||||
|
type BinaryPointerConversationFunc func(BinaryPointer) (BinaryPointer, error)
|
||||||
|
|
||||||
|
// RespondPAMBinary is a conversation callback adapter.
|
||||||
|
func (f BinaryPointerConversationFunc) RespondPAMBinary(ptr BinaryPointer) (BinaryPointer, error) {
|
||||||
|
return f(ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RespondPAM is a dummy conversation callback adapter.
|
||||||
|
func (f BinaryPointerConversationFunc) RespondPAM(Style, string) (string, error) {
|
||||||
|
return "", ErrConv
|
||||||
|
}
|
||||||
|
|
||||||
|
// _go_pam_conv_handler is a C wrapper for the conversation callback function.
|
||||||
|
//
|
||||||
|
//export _go_pam_conv_handler
|
||||||
|
func _go_pam_conv_handler(msg *C.struct_pam_message, c C.uintptr_t, outMsg **C.char) C.int {
|
||||||
|
convHandler, ok := cgo.Handle(c).Value().(ConversationHandler)
|
||||||
|
if !ok || convHandler == nil {
|
||||||
|
return C.int(ErrConv)
|
||||||
|
}
|
||||||
|
replyMsg, r := pamConvHandler(Style(msg.msg_style), msg.msg, convHandler)
|
||||||
|
*outMsg = replyMsg
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// pamConvHandler is a Go wrapper for the conversation callback function.
|
||||||
|
func pamConvHandler(style Style, msg *C.char, handler ConversationHandler) (*C.char, C.int) {
|
||||||
|
var r string
|
||||||
|
var err error
|
||||||
|
switch cb := handler.(type) {
|
||||||
|
case BinaryConversationHandler:
|
||||||
|
if style == BinaryPrompt {
|
||||||
|
bytes, err := cb.RespondPAMBinary(BinaryPointer(msg))
|
||||||
|
if err != nil {
|
||||||
|
return nil, C.int(ErrConv)
|
||||||
|
}
|
||||||
|
if bytes == nil {
|
||||||
|
return nil, success
|
||||||
|
}
|
||||||
|
return (*C.char)(C.CBytes(bytes)), success
|
||||||
|
}
|
||||||
|
handler = cb
|
||||||
|
case BinaryPointerConversationHandler:
|
||||||
|
if style == BinaryPrompt {
|
||||||
|
ptr, err := cb.RespondPAMBinary(BinaryPointer(msg))
|
||||||
|
if err != nil {
|
||||||
|
defer C.free(unsafe.Pointer(ptr))
|
||||||
|
return nil, C.int(ErrConv)
|
||||||
|
}
|
||||||
|
return (*C.char)(ptr), success
|
||||||
|
}
|
||||||
|
handler = cb
|
||||||
|
case ConversationHandler:
|
||||||
|
if style == BinaryPrompt {
|
||||||
|
return nil, C.int(ErrConv)
|
||||||
|
}
|
||||||
|
handler = cb
|
||||||
|
default:
|
||||||
|
return nil, C.int(ErrConv)
|
||||||
|
}
|
||||||
|
r, err = handler.RespondPAM(style, C.GoString(msg))
|
||||||
|
if err != nil {
|
||||||
|
return nil, C.int(ErrConv)
|
||||||
|
}
|
||||||
|
return C.CString(r), success
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction is the application's handle for a PAM transaction.
|
||||||
|
type Transaction struct {
|
||||||
|
transactionBase
|
||||||
|
|
||||||
|
conv *C.struct_pam_conv
|
||||||
|
c cgo.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initiates a new PAM transaction. Service is treated identically to
|
||||||
|
// how pam_start treats it internally.
|
||||||
|
//
|
||||||
|
// All application calls to PAM begin with Start*. The returned
|
||||||
|
// transaction provides an interface to the remainder of the API.
|
||||||
|
//
|
||||||
|
// It's responsibility of the Transaction owner to release all the resources
|
||||||
|
// allocated underneath by PAM by calling End() once done.
|
||||||
|
//
|
||||||
|
// It's not advised to End the transaction using a runtime.SetFinalizer unless
|
||||||
|
// you're absolutely sure that your stack is multi-thread friendly (normally it
|
||||||
|
// is not!) and using a LockOSThread/UnlockOSThread pair.
|
||||||
|
func Start(service, user string, handler ConversationHandler) (*Transaction, error) {
|
||||||
|
return start(service, user, handler, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartFunc registers the handler func as a conversation handler and starts
|
||||||
|
// the transaction (see Start() documentation).
|
||||||
|
func StartFunc(service, user string, handler func(Style, string) (string, error)) (*Transaction, error) {
|
||||||
|
return start(service, user, ConversationFunc(handler), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartConfDir initiates a new PAM transaction. Service is treated identically to
|
||||||
|
// how pam_start treats it internally.
|
||||||
|
// confdir allows to define where all pam services are defined. This is used to provide
|
||||||
|
// custom paths for tests.
|
||||||
|
//
|
||||||
|
// All application calls to PAM begin with Start*. The returned
|
||||||
|
// transaction provides an interface to the remainder of the API.
|
||||||
|
//
|
||||||
|
// It's responsibility of the Transaction owner to release all the resources
|
||||||
|
// allocated underneath by PAM by calling End() once done.
|
||||||
|
//
|
||||||
|
// It's not advised to End the transaction using a runtime.SetFinalizer unless
|
||||||
|
// you're absolutely sure that your stack is multi-thread friendly (normally it
|
||||||
|
// is not!) and using a LockOSThread/UnlockOSThread pair.
|
||||||
|
func StartConfDir(service, user string, handler ConversationHandler, confDir string) (*Transaction, error) {
|
||||||
|
if !CheckPamHasStartConfdir() {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%w: StartConfDir was used, but the pam version on the system is not recent enough",
|
||||||
|
ErrSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return start(service, user, handler, confDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func start(service, user string, handler ConversationHandler, confDir string) (*Transaction, error) {
|
||||||
|
switch handler.(type) {
|
||||||
|
case BinaryConversationHandler:
|
||||||
|
if !CheckPamHasBinaryProtocol() {
|
||||||
|
return nil, fmt.Errorf("%w: BinaryConversationHandler was used, but it is not supported by this platform",
|
||||||
|
ErrSystem)
|
||||||
|
}
|
||||||
|
case BinaryPointerConversationHandler:
|
||||||
|
if !CheckPamHasBinaryProtocol() {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%w: BinaryPointerConversationHandler was used, but it is not supported by this platform",
|
||||||
|
ErrSystem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t := &Transaction{
|
||||||
|
conv: &C.struct_pam_conv{},
|
||||||
|
c: cgo.NewHandle(handler),
|
||||||
|
}
|
||||||
|
|
||||||
|
C.init_pam_conv(t.conv, C.uintptr_t(t.c))
|
||||||
|
s := C.CString(service)
|
||||||
|
defer C.free(unsafe.Pointer(s))
|
||||||
|
var u *C.char
|
||||||
|
if len(user) != 0 {
|
||||||
|
u = C.CString(user)
|
||||||
|
defer C.free(unsafe.Pointer(u))
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if confDir == "" {
|
||||||
|
err = t.handlePamStatus(C.pam_start(s, u, t.conv, &t.handle))
|
||||||
|
} else {
|
||||||
|
c := C.CString(confDir)
|
||||||
|
defer C.free(unsafe.Pointer(c))
|
||||||
|
err = t.handlePamStatus(C.pam_start_confdir(s, u, t.conv, c, &t.handle))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
var _ = t.End()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate is used to authenticate the user.
|
||||||
|
//
|
||||||
|
// Valid flags: Silent, DisallowNullAuthtok
|
||||||
|
func (t *Transaction) Authenticate(f Flags) error {
|
||||||
|
return t.handlePamStatus(C.pam_authenticate(t.handle, C.int(f)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCred is used to establish, maintain and delete the credentials of a
|
||||||
|
// user.
|
||||||
|
//
|
||||||
|
// Valid flags: EstablishCred, DeleteCred, ReinitializeCred, RefreshCred
|
||||||
|
func (t *Transaction) SetCred(f Flags) error {
|
||||||
|
return t.handlePamStatus(C.pam_setcred(t.handle, C.int(f)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcctMgmt is used to determine if the user's account is valid.
|
||||||
|
//
|
||||||
|
// Valid flags: Silent, DisallowNullAuthtok
|
||||||
|
func (t *Transaction) AcctMgmt(f Flags) error {
|
||||||
|
return t.handlePamStatus(C.pam_acct_mgmt(t.handle, C.int(f)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeAuthTok is used to change the authentication token.
|
||||||
|
//
|
||||||
|
// Valid flags: Silent, ChangeExpiredAuthtok
|
||||||
|
func (t *Transaction) ChangeAuthTok(f Flags) error {
|
||||||
|
return t.handlePamStatus(C.pam_chauthtok(t.handle, C.int(f)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenSession sets up a user session for an authenticated user.
|
||||||
|
//
|
||||||
|
// Valid flags: Slient
|
||||||
|
func (t *Transaction) OpenSession(f Flags) error {
|
||||||
|
return t.handlePamStatus(C.pam_open_session(t.handle, C.int(f)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseSession closes a previously opened session.
|
||||||
|
//
|
||||||
|
// Valid flags: Silent
|
||||||
|
func (t *Transaction) CloseSession(f Flags) error {
|
||||||
|
return t.handlePamStatus(C.pam_close_session(t.handle, C.int(f)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// End cleans up the PAM handle and deletes the callback function.
|
||||||
|
// It must be called when done with the transaction.
|
||||||
|
func (t *Transaction) End() error {
|
||||||
|
handle := atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&t.handle)), nil)
|
||||||
|
if handle == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer t.c.Delete()
|
||||||
|
return t.handlePamStatus(C.pam_end((*C.pam_handle_t)(handle),
|
||||||
|
C.int(t.lastStatus.Load())))
|
||||||
|
}
|
||||||
313
cmd/pam-moduler/moduler.go
Normal file
313
cmd/pam-moduler/moduler.go
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
// pam-moduler is a tool to automate the creation of PAM Modules in go
|
||||||
|
//
|
||||||
|
// The file is created in the same package and directory as the package that
|
||||||
|
// creates the module
|
||||||
|
//
|
||||||
|
// The module implementation should define a pamModuleHandler object that
|
||||||
|
// implements the pam.ModuleHandler type and that will be used for each callback
|
||||||
|
//
|
||||||
|
// Otherwise it's possible to provide a typename from command line that will
|
||||||
|
// be used for this purpose
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
//
|
||||||
|
// //go:generate go run github.com/msteinert/pam/v2/pam-moduler
|
||||||
|
// //go:generate go generate --skip="pam_module"
|
||||||
|
// package main
|
||||||
|
//
|
||||||
|
// import "github.com/msteinert/pam/v2"
|
||||||
|
//
|
||||||
|
// type ExampleHandler struct{}
|
||||||
|
// var pamModuleHandler pam.ModuleHandler = &ExampleHandler{}
|
||||||
|
//
|
||||||
|
// func (h *ExampleHandler) AcctMgmt(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (h *ExampleHandler) Authenticate(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (h *ExampleHandler) ChangeAuthTok(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (h *ExampleHandler) OpenSession(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (h *ExampleHandler) CloseSession(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (h *ExampleHandler) SetCred(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Package main provides the module shared library.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"go/format"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const toolName = "pam-moduler"
|
||||||
|
|
||||||
|
var (
|
||||||
|
output = flag.String("output", "", "output file name; default srcdir/pam_module.go")
|
||||||
|
libName = flag.String("libname", "", "output library name; default pam_go.so")
|
||||||
|
typeName = flag.String("type", "", "type name to be used as pam.ModuleHandler")
|
||||||
|
buildTags = flag.String("tags", "", "build tags expression to append to use in the go:build directive")
|
||||||
|
skipGenerator = flag.Bool("no-generator", false, "whether to add go:generator directives to the generated source")
|
||||||
|
moduleBuildFlags = flag.String("build-flags", "", "comma-separated list of go build flags to use when generating the module")
|
||||||
|
moduleBuildTags = flag.String("build-tags", "", "comma-separated list of build tags to use when generating the module")
|
||||||
|
noMain = flag.Bool("no-main", false, "whether to add an empty main to generated file")
|
||||||
|
parallelConv = flag.Bool("parallel-conv", false, "whether to support performing PAM conversations in parallel")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Usage is a replacement usage function for the flags package.
|
||||||
|
func Usage() {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage of %s:\n", toolName)
|
||||||
|
fmt.Fprintf(os.Stderr, "\t%s [flags] [-output O] [-libname pam_go] [-type N]\n", toolName)
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.SetPrefix(toolName + ": ")
|
||||||
|
flag.Usage = Usage
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *skipGenerator {
|
||||||
|
if *libName != "" {
|
||||||
|
fmt.Fprintf(os.Stderr,
|
||||||
|
"Generator directives disabled, libname will have no effect\n")
|
||||||
|
}
|
||||||
|
if *moduleBuildTags != "" {
|
||||||
|
fmt.Fprintf(os.Stderr,
|
||||||
|
"Generator directives disabled, build-tags will have no effect\n")
|
||||||
|
}
|
||||||
|
if *moduleBuildFlags != "" {
|
||||||
|
fmt.Fprintf(os.Stderr,
|
||||||
|
"Generator directives disabled, build-flags will have no effect\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lib := *libName
|
||||||
|
if lib == "" {
|
||||||
|
lib = "pam_go"
|
||||||
|
} else {
|
||||||
|
lib, _ = strings.CutSuffix(lib, ".so")
|
||||||
|
lib, _ = strings.CutPrefix(lib, "lib")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputName, _ := strings.CutSuffix(*output, ".go")
|
||||||
|
if outputName == "" {
|
||||||
|
baseName := "pam_module"
|
||||||
|
outputName = filepath.Join(".", strings.ToLower(baseName))
|
||||||
|
}
|
||||||
|
outputName = outputName + ".go"
|
||||||
|
|
||||||
|
var tags string
|
||||||
|
if *buildTags != "" {
|
||||||
|
tags = *buildTags
|
||||||
|
}
|
||||||
|
|
||||||
|
generateTags := []string{"go_pam_module"}
|
||||||
|
if len(*moduleBuildTags) > 0 {
|
||||||
|
generateTags = append(generateTags, strings.Split(*moduleBuildTags, ",")...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buildFlags []string
|
||||||
|
if *moduleBuildFlags != "" {
|
||||||
|
buildFlags = strings.Split(*moduleBuildFlags, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
g := Generator{
|
||||||
|
outputName: outputName,
|
||||||
|
libName: lib,
|
||||||
|
tags: tags,
|
||||||
|
buildFlags: buildFlags,
|
||||||
|
generateTags: generateTags,
|
||||||
|
noMain: *noMain,
|
||||||
|
typeName: *typeName,
|
||||||
|
parallelConv: *parallelConv,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the header and package clause.
|
||||||
|
g.printf("// Code generated by \"%s %s\"; DO NOT EDIT.\n",
|
||||||
|
toolName, strings.Join(os.Args[1:], " "))
|
||||||
|
g.printf("\n")
|
||||||
|
|
||||||
|
// Generate the code
|
||||||
|
g.generate()
|
||||||
|
|
||||||
|
// Format the output.
|
||||||
|
src := g.format()
|
||||||
|
|
||||||
|
// Write to file.
|
||||||
|
err := os.WriteFile(outputName, src, 0600)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("writing output: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generator holds the state of the analysis. Primarily used to buffer
|
||||||
|
// the output for format.Source.
|
||||||
|
type Generator struct {
|
||||||
|
buf bytes.Buffer // Accumulated output.
|
||||||
|
|
||||||
|
libName string
|
||||||
|
outputName string
|
||||||
|
typeName string
|
||||||
|
tags string
|
||||||
|
generateTags []string
|
||||||
|
buildFlags []string
|
||||||
|
noMain bool
|
||||||
|
parallelConv bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) printf(format string, args ...interface{}) {
|
||||||
|
fmt.Fprintf(&g.buf, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate produces the String method for the named type.
|
||||||
|
func (g *Generator) generate() {
|
||||||
|
if g.tags != "" {
|
||||||
|
g.printf("//go:build %s\n", g.tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buildTagsArg string
|
||||||
|
if len(g.generateTags) > 0 {
|
||||||
|
buildTagsArg = fmt.Sprintf("-tags %s", strings.Join(g.generateTags, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
var transactionCreator = "NewModuleTransactionInvoker"
|
||||||
|
if g.parallelConv {
|
||||||
|
transactionCreator = "NewModuleTransactionInvokerParallelConv"
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use a slice since we want to keep order, for reproducible builds.
|
||||||
|
vFuncs := []struct {
|
||||||
|
cName string
|
||||||
|
goName string
|
||||||
|
}{
|
||||||
|
{"authenticate", "Authenticate"},
|
||||||
|
{"setcred", "SetCred"},
|
||||||
|
{"acct_mgmt", "AcctMgmt"},
|
||||||
|
{"open_session", "OpenSession"},
|
||||||
|
{"close_session", "CloseSession"},
|
||||||
|
{"chauthtok", "ChangeAuthTok"},
|
||||||
|
}
|
||||||
|
|
||||||
|
g.printf(`//go:generate go build "-ldflags=-extldflags -Wl,-soname,%[2]s.so" `+
|
||||||
|
`-buildmode=c-shared -o %[2]s.so %[3]s %[4]s
|
||||||
|
`,
|
||||||
|
g.outputName, g.libName, buildTagsArg, strings.Join(g.buildFlags, " "))
|
||||||
|
|
||||||
|
g.printf(`
|
||||||
|
// Package main is the package for the PAM module library.
|
||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo LDFLAGS: -lpam -fPIC
|
||||||
|
#include <security/pam_modules.h>
|
||||||
|
|
||||||
|
typedef const char _const_char_t;
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"unsafe"
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
|
||||||
|
if g.typeName != "" {
|
||||||
|
g.printf(`
|
||||||
|
var pamModuleHandler pam.ModuleHandler = &%[1]s{}
|
||||||
|
`, g.typeName)
|
||||||
|
} else {
|
||||||
|
g.printf(`
|
||||||
|
// Do a typecheck at compile time
|
||||||
|
var _ pam.ModuleHandler = pamModuleHandler;
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.printf(`
|
||||||
|
// sliceFromArgv returns a slice of strings given to the PAM module.
|
||||||
|
func sliceFromArgv(argc C.int, argv **C._const_char_t) []string {
|
||||||
|
r := make([]string, 0, argc)
|
||||||
|
for _, s := range unsafe.Slice(argv, argc) {
|
||||||
|
r = append(r, C.GoString(s))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePamCall is the function that translates C pam requests to Go.
|
||||||
|
func handlePamCall(pamh *C.pam_handle_t, flags C.int, argc C.int,
|
||||||
|
argv **C._const_char_t, moduleFunc pam.ModuleHandlerFunc) C.int {
|
||||||
|
if pamModuleHandler == nil {
|
||||||
|
return C.int(pam.ErrNoModuleData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if moduleFunc == nil {
|
||||||
|
return C.int(pam.ErrIgnore)
|
||||||
|
}
|
||||||
|
|
||||||
|
mt := pam.%s(pam.NativeHandle(pamh))
|
||||||
|
err := mt.InvokeHandler(moduleFunc, pam.Flags(flags),
|
||||||
|
sliceFromArgv(argc, argv))
|
||||||
|
if err == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pam.Flags(flags) & pam.Silent) == 0 && !errors.Is(err, pam.ErrIgnore) {
|
||||||
|
fmt.Fprintf(os.Stderr, "module returned error: %%v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pamErr pam.Error
|
||||||
|
if errors.As(err, &pamErr) {
|
||||||
|
return C.int(pamErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return C.int(pam.ErrSystem)
|
||||||
|
}
|
||||||
|
`, transactionCreator)
|
||||||
|
|
||||||
|
for _, f := range vFuncs {
|
||||||
|
g.printf(`
|
||||||
|
//export pam_sm_%[1]s
|
||||||
|
func pam_sm_%[1]s(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.%[2]s)
|
||||||
|
}
|
||||||
|
`, f.cName, f.goName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !g.noMain {
|
||||||
|
g.printf("\nfunc main() {}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// format returns the gofmt-ed contents of the Generator's buffer.
|
||||||
|
func (g *Generator) format() []byte {
|
||||||
|
src, err := format.Source(g.buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
// Should never happen, but can arise when developing this code.
|
||||||
|
// The user can compile the output to see the error.
|
||||||
|
log.Printf("warning: internal error: invalid Go generated: %s", err)
|
||||||
|
log.Printf("warning: compile the package to analyze the error")
|
||||||
|
return g.buf.Bytes()
|
||||||
|
}
|
||||||
|
return src
|
||||||
|
}
|
||||||
119
cmd/pam-moduler/tests/debug-module/debug-module.go
Normal file
119
cmd/pam-moduler/tests/debug-module/debug-module.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
//go:generate go run github.com/msteinert/pam/v2/cmd/pam-moduler -libname "pam_godebug.so"
|
||||||
|
//go:generate go generate --skip="pam_module.go"
|
||||||
|
|
||||||
|
// This is a similar implementation of pam_debug.so
|
||||||
|
|
||||||
|
// Package main is the package for the debug PAM module library
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
"github.com/msteinert/pam/v2/cmd/pam-moduler/tests/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pamModuleHandler pam.ModuleHandler = &DebugModule{}
|
||||||
|
var _ = pamModuleHandler
|
||||||
|
|
||||||
|
var moduleArgsRetTypes = map[string]error{
|
||||||
|
"success": nil,
|
||||||
|
"open_err": pam.ErrOpen,
|
||||||
|
"symbol_err": pam.ErrSymbol,
|
||||||
|
"service_err": pam.ErrService,
|
||||||
|
"system_err": pam.ErrSystem,
|
||||||
|
"buf_err": pam.ErrBuf,
|
||||||
|
"perm_denied": pam.ErrPermDenied,
|
||||||
|
"auth_err": pam.ErrAuth,
|
||||||
|
"cred_insufficient": pam.ErrCredInsufficient,
|
||||||
|
"authinfo_unavail": pam.ErrAuthinfoUnavail,
|
||||||
|
"user_unknown": pam.ErrUserUnknown,
|
||||||
|
"maxtries": pam.ErrMaxtries,
|
||||||
|
"new_authtok_reqd": pam.ErrNewAuthtokReqd,
|
||||||
|
"acct_expired": pam.ErrAcctExpired,
|
||||||
|
"session_err": pam.ErrSession,
|
||||||
|
"cred_unavail": pam.ErrCredUnavail,
|
||||||
|
"cred_expired": pam.ErrCredExpired,
|
||||||
|
"cred_err": pam.ErrCred,
|
||||||
|
"no_module_data": pam.ErrNoModuleData,
|
||||||
|
"conv_err": pam.ErrConv,
|
||||||
|
"authtok_err": pam.ErrAuthtok,
|
||||||
|
"authtok_recover_err": pam.ErrAuthtokRecovery,
|
||||||
|
"authtok_lock_busy": pam.ErrAuthtokLockBusy,
|
||||||
|
"authtok_disable_aging": pam.ErrAuthtokDisableAging,
|
||||||
|
"try_again": pam.ErrTryAgain,
|
||||||
|
"ignore": pam.ErrIgnore,
|
||||||
|
"abort": pam.ErrAbort,
|
||||||
|
"authtok_expired": pam.ErrAuthtokExpired,
|
||||||
|
"module_unknown": pam.ErrModuleUnknown,
|
||||||
|
"bad_item": pam.ErrBadItem,
|
||||||
|
"conv_again": pam.ErrConvAgain,
|
||||||
|
"incomplete": pam.ErrIncomplete,
|
||||||
|
}
|
||||||
|
|
||||||
|
var debugModuleArgs = []string{"auth", "cred", "acct", "prechauthtok",
|
||||||
|
"chauthtok", "open_session", "close_session"}
|
||||||
|
|
||||||
|
// DebugModule is the PAM module structure.
|
||||||
|
type DebugModule struct {
|
||||||
|
utils.BaseModule
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *DebugModule) getReturnType(args []string, key string) error {
|
||||||
|
var value string
|
||||||
|
for _, a := range args {
|
||||||
|
v, found := strings.CutPrefix(a, key+"=")
|
||||||
|
if found {
|
||||||
|
value = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value == "" {
|
||||||
|
return fmt.Errorf("Value not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret, found := moduleArgsRetTypes[value]; found {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Parameter %s not known", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *DebugModule) handleCall(args []string, action string) error {
|
||||||
|
err := dm.getReturnType(args, action)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("error %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcctMgmt is a PAM handler.
|
||||||
|
func (dm *DebugModule) AcctMgmt(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return dm.handleCall(args, "acct")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate is a PAM handler.
|
||||||
|
func (dm *DebugModule) Authenticate(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return dm.handleCall(args, "auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeAuthTok is a PAM handler.
|
||||||
|
func (dm *DebugModule) ChangeAuthTok(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return dm.handleCall(args, "chauthtok")
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenSession is a PAM handler.
|
||||||
|
func (dm *DebugModule) OpenSession(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return dm.handleCall(args, "open_session")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseSession is a PAM handler.
|
||||||
|
func (dm *DebugModule) CloseSession(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return dm.handleCall(args, "close_session")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCred is a PAM handler.
|
||||||
|
func (dm *DebugModule) SetCred(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return dm.handleCall(args, "cred")
|
||||||
|
}
|
||||||
120
cmd/pam-moduler/tests/debug-module/debug-module_test.go
Normal file
120
cmd/pam-moduler/tests/debug-module/debug-module_test.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
"github.com/msteinert/pam/v2/cmd/pam-moduler/tests/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_DebugModule_ActionStatus(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
module := DebugModule{}
|
||||||
|
|
||||||
|
for ret, expected := range moduleArgsRetTypes {
|
||||||
|
ret := ret
|
||||||
|
expected := expected
|
||||||
|
for actionName, action := range utils.Actions {
|
||||||
|
actionName := actionName
|
||||||
|
action := action
|
||||||
|
t.Run(fmt.Sprintf("%s %s", ret, actionName), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
moduleArgs := make([]string, 0)
|
||||||
|
for _, a := range debugModuleArgs {
|
||||||
|
moduleArgs = append(moduleArgs, fmt.Sprintf("%s=%s", a, ret))
|
||||||
|
}
|
||||||
|
|
||||||
|
mt := pam.ModuleTransactionInvoker(nil)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case utils.Account:
|
||||||
|
err = module.AcctMgmt(mt, 0, moduleArgs)
|
||||||
|
case utils.Auth:
|
||||||
|
err = module.Authenticate(mt, 0, moduleArgs)
|
||||||
|
case utils.Password:
|
||||||
|
err = module.ChangeAuthTok(mt, 0, moduleArgs)
|
||||||
|
case utils.Session:
|
||||||
|
err = module.OpenSession(mt, 0, moduleArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, expected) {
|
||||||
|
t.Fatalf("error #unexpected %#v vs %#v", expected, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_DebugModuleTransaction_ActionStatus(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if !pam.CheckPamHasStartConfdir() {
|
||||||
|
t.Skip("this requires PAM with Conf dir support")
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := utils.NewTestSetup(t, utils.WithWorkDir())
|
||||||
|
modulePath := ts.GenerateModule(".", "pam_godebug.so")
|
||||||
|
|
||||||
|
for ret, expected := range moduleArgsRetTypes {
|
||||||
|
ret := ret
|
||||||
|
expected := expected
|
||||||
|
for actionName, action := range utils.Actions {
|
||||||
|
ret := ret
|
||||||
|
expected := expected
|
||||||
|
actionName := actionName
|
||||||
|
action := action
|
||||||
|
t.Run(fmt.Sprintf("%s %s", ret, actionName), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
serviceName := ret + "-" + actionName
|
||||||
|
moduleArgs := make([]string, 0)
|
||||||
|
for _, a := range debugModuleArgs {
|
||||||
|
moduleArgs = append(moduleArgs, fmt.Sprintf("%s=%s", a, ret))
|
||||||
|
}
|
||||||
|
control := utils.Requisite
|
||||||
|
fallbackModule := utils.Permit
|
||||||
|
if ret == "success" {
|
||||||
|
fallbackModule = utils.Deny
|
||||||
|
control = utils.Sufficient
|
||||||
|
}
|
||||||
|
ts.CreateService(serviceName, []utils.ServiceLine{
|
||||||
|
{Action: action, Control: control, Module: modulePath, Args: moduleArgs},
|
||||||
|
{Action: action, Control: control, Module: fallbackModule.String(), Args: []string{}},
|
||||||
|
})
|
||||||
|
|
||||||
|
tx, err := pam.StartConfDir(serviceName, "user", nil, ts.WorkDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("start #error: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := tx.End()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("end #error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case utils.Account:
|
||||||
|
err = tx.AcctMgmt(pam.Silent)
|
||||||
|
case utils.Auth:
|
||||||
|
err = tx.Authenticate(pam.Silent)
|
||||||
|
case utils.Password:
|
||||||
|
err = tx.ChangeAuthTok(pam.Silent)
|
||||||
|
case utils.Session:
|
||||||
|
err = tx.OpenSession(pam.Silent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(expected, pam.ErrIgnore) {
|
||||||
|
// Ignore can't be returned
|
||||||
|
expected = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, expected) {
|
||||||
|
t.Fatalf("error #unexpected %#v vs %#v", expected, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
cmd/pam-moduler/tests/debug-module/pam_module.go
Normal file
96
cmd/pam-moduler/tests/debug-module/pam_module.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// Code generated by "pam-moduler -libname pam_godebug.so"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:generate go build "-ldflags=-extldflags -Wl,-soname,pam_godebug.so" -buildmode=c-shared -o pam_godebug.so -tags go_pam_module
|
||||||
|
|
||||||
|
// Package main is the package for the PAM module library.
|
||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo LDFLAGS: -lpam -fPIC
|
||||||
|
#include <security/pam_modules.h>
|
||||||
|
|
||||||
|
typedef const char _const_char_t;
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
"os"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Do a typecheck at compile time
|
||||||
|
var _ pam.ModuleHandler = pamModuleHandler
|
||||||
|
|
||||||
|
// sliceFromArgv returns a slice of strings given to the PAM module.
|
||||||
|
func sliceFromArgv(argc C.int, argv **C._const_char_t) []string {
|
||||||
|
r := make([]string, 0, argc)
|
||||||
|
for _, s := range unsafe.Slice(argv, argc) {
|
||||||
|
r = append(r, C.GoString(s))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePamCall is the function that translates C pam requests to Go.
|
||||||
|
func handlePamCall(pamh *C.pam_handle_t, flags C.int, argc C.int,
|
||||||
|
argv **C._const_char_t, moduleFunc pam.ModuleHandlerFunc) C.int {
|
||||||
|
if pamModuleHandler == nil {
|
||||||
|
return C.int(pam.ErrNoModuleData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if moduleFunc == nil {
|
||||||
|
return C.int(pam.ErrIgnore)
|
||||||
|
}
|
||||||
|
|
||||||
|
mt := pam.NewModuleTransactionInvoker(pam.NativeHandle(pamh))
|
||||||
|
err := mt.InvokeHandler(moduleFunc, pam.Flags(flags),
|
||||||
|
sliceFromArgv(argc, argv))
|
||||||
|
if err == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pam.Flags(flags)&pam.Silent) == 0 && !errors.Is(err, pam.ErrIgnore) {
|
||||||
|
fmt.Fprintf(os.Stderr, "module returned error: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pamErr pam.Error
|
||||||
|
if errors.As(err, &pamErr) {
|
||||||
|
return C.int(pamErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return C.int(pam.ErrSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_authenticate
|
||||||
|
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.Authenticate)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_setcred
|
||||||
|
func pam_sm_setcred(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.SetCred)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_acct_mgmt
|
||||||
|
func pam_sm_acct_mgmt(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.AcctMgmt)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_open_session
|
||||||
|
func pam_sm_open_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.OpenSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_close_session
|
||||||
|
func pam_sm_close_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.CloseSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_chauthtok
|
||||||
|
func pam_sm_chauthtok(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.ChangeAuthTok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {}
|
||||||
230
cmd/pam-moduler/tests/integration-tester-module/communication.go
Normal file
230
cmd/pam-moduler/tests/integration-tester-module/communication.go
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
// Package main is the package for the integration tester module PAM shared library.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Request is a serializable integration module tester structure request.
|
||||||
|
type Request struct {
|
||||||
|
Action string
|
||||||
|
ActionArgs []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result is a serializable integration module tester structure result.
|
||||||
|
type Result = Request
|
||||||
|
|
||||||
|
// NewRequest returns a new Request.
|
||||||
|
func NewRequest(action string, actionArgs ...interface{}) Request {
|
||||||
|
return Request{action, actionArgs}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOB serializes the request in binary format.
|
||||||
|
func (r *Request) GOB() ([]byte, error) {
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
e := gob.NewEncoder(&b)
|
||||||
|
if err := e.Encode(r); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRequestFromGOB gets a Request from a serialized binary.
|
||||||
|
func NewRequestFromGOB(data []byte) (*Request, error) {
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
b.Write(data)
|
||||||
|
d := gob.NewDecoder(&b)
|
||||||
|
|
||||||
|
var req Request
|
||||||
|
if err := d.Decode(&req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const bufSize = 1024
|
||||||
|
|
||||||
|
type connectionHandler struct {
|
||||||
|
inOutData chan []byte
|
||||||
|
outErr chan error
|
||||||
|
SocketPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listener is a socket listener.
|
||||||
|
type Listener struct {
|
||||||
|
connectionHandler
|
||||||
|
listener net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewListener creates a new Listener.
|
||||||
|
func NewListener(socketPath string) *Listener {
|
||||||
|
if len(socketPath) > 90 {
|
||||||
|
// See https://manpages.ubuntu.com/manpages/jammy/man7/sys_un.h.7posix.html#application%20usage
|
||||||
|
panic(fmt.Sprintf("Socket path %s too long", socketPath))
|
||||||
|
}
|
||||||
|
return &Listener{connectionHandler{SocketPath: socketPath}, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForData waits for result data (or an error) on connection to be returned.
|
||||||
|
func (c *connectionHandler) WaitForData() (*Result, error) {
|
||||||
|
data, err := <-c.inOutData, <-c.outErr
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := NewRequestFromGOB(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRequest sends a request to the connection.
|
||||||
|
func (c *connectionHandler) SendRequest(req *Request) error {
|
||||||
|
bytes, err := req.GOB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.inOutData <- bytes
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendResult sends the Result to the connection.
|
||||||
|
func (c *connectionHandler) SendResult(res *Result) error {
|
||||||
|
return c.SendRequest(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoRequest performs a Request on the connection, waiting for data.
|
||||||
|
func (c *connectionHandler) DoRequest(req *Request) (*Result, error) {
|
||||||
|
if err := c.SendRequest(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.WaitForData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send performs a request.
|
||||||
|
func (r *Request) Send(c *connectionHandler) error {
|
||||||
|
return c.SendRequest(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrAlreadyListening is the error if a listener is already set.
|
||||||
|
var ErrAlreadyListening = errors.New("listener already set")
|
||||||
|
|
||||||
|
// StartListening initiates the unix listener.
|
||||||
|
func (l *Listener) StartListening() error {
|
||||||
|
if l.listener != nil {
|
||||||
|
return ErrAlreadyListening
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.Listen("unix", l.SocketPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
l.listener = listener
|
||||||
|
l.inOutData, l.outErr = make(chan []byte), make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
bytes, err := func() ([]byte, error) {
|
||||||
|
for {
|
||||||
|
c, err := l.listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
buf := make([]byte, bufSize)
|
||||||
|
nr, err := c.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return buf, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := buf[0:nr]
|
||||||
|
l.inOutData <- data
|
||||||
|
l.outErr <- nil
|
||||||
|
|
||||||
|
_, err = c.Write(<-l.inOutData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
l.inOutData <- bytes
|
||||||
|
l.outErr <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connector is a connection type.
|
||||||
|
type Connector struct {
|
||||||
|
connectionHandler
|
||||||
|
connection net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConnector creates a new connection.
|
||||||
|
func NewConnector(socketPath string) *Connector {
|
||||||
|
return &Connector{connectionHandler{SocketPath: socketPath}, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrAlreadyConnected is the error if a connection is already set.
|
||||||
|
var ErrAlreadyConnected = errors.New("connection already set")
|
||||||
|
|
||||||
|
// Connect connects to a listening unix socket.
|
||||||
|
func (c *Connector) Connect() error {
|
||||||
|
if c.connection != nil {
|
||||||
|
return ErrAlreadyConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
connection, err := net.Dial("unix", c.SocketPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.SetFinalizer(c, func(c *Connector) {
|
||||||
|
c.connection.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
c.connection = connection
|
||||||
|
c.inOutData, c.outErr = make(chan []byte), make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, bufSize)
|
||||||
|
writeAndRead := func() ([]byte, error) {
|
||||||
|
data := <-c.inOutData
|
||||||
|
_, err := c.connection.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := c.connection.Read(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf[0:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
bytes, err := writeAndRead()
|
||||||
|
c.inOutData <- bytes
|
||||||
|
c.outErr <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/msteinert/pam/v2/cmd/pam-moduler/tests/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ensureNoError(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureError(t *testing.T, err error, expected error) {
|
||||||
|
t.Helper()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("error was expected, got none")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, expected) {
|
||||||
|
t.Fatalf("error %v was expected, got %v", err, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureEqual(t *testing.T, a any, b any) {
|
||||||
|
t.Helper()
|
||||||
|
if !reflect.DeepEqual(a, b) {
|
||||||
|
t.Fatalf("values mismatch %#v vs %#v", a, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Communication(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ts := utils.NewTestSetup(t, utils.WithWorkDir())
|
||||||
|
|
||||||
|
for _, name := range []string{"test-1", "test-2"} {
|
||||||
|
name := name
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
socketPath := filepath.Join(ts.WorkDir(), name+".socket")
|
||||||
|
|
||||||
|
listener := NewListener(socketPath)
|
||||||
|
connector := NewConnector(socketPath)
|
||||||
|
|
||||||
|
ensureNoError(t, listener.StartListening())
|
||||||
|
ensureNoError(t, connector.Connect())
|
||||||
|
|
||||||
|
ensureError(t, listener.StartListening(), ErrAlreadyListening)
|
||||||
|
ensureError(t, connector.Connect(), ErrAlreadyConnected)
|
||||||
|
|
||||||
|
resChan, errChan := make(chan *Result), make(chan error)
|
||||||
|
go func() {
|
||||||
|
res, err := listener.WaitForData()
|
||||||
|
resChan <- res
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
req := NewRequest("A Request")
|
||||||
|
ensureNoError(t, connector.SendRequest(&req))
|
||||||
|
|
||||||
|
res, err := <-resChan, <-errChan
|
||||||
|
ensureNoError(t, err)
|
||||||
|
ensureEqual(t, *res, req)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
res := NewRequest("Listener result")
|
||||||
|
ensureNoError(t, listener.SendResult(&res))
|
||||||
|
}()
|
||||||
|
|
||||||
|
res, err = connector.WaitForData()
|
||||||
|
ensureNoError(t, err)
|
||||||
|
ensureEqual(t, *res, NewRequest("Listener result"))
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
req, err := listener.WaitForData()
|
||||||
|
res := NewRequest("Response", *req)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
resChan <- &res
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
ensureNoError(t, listener.SendResult(&res))
|
||||||
|
}()
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
req = NewRequest("Requesting...")
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
res, err := connector.DoRequest(&req)
|
||||||
|
ensureNoError(t, err)
|
||||||
|
ensureEqual(t, *res, NewRequest("Response", req))
|
||||||
|
}()
|
||||||
|
|
||||||
|
res, err = <-resChan, <-errChan
|
||||||
|
ensureNoError(t, err)
|
||||||
|
ensureEqual(t, *res, NewRequest("Response", req))
|
||||||
|
<-done
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
//go:generate go run github.com/msteinert/pam/v2/cmd/pam-moduler -type integrationTesterModule -parallel-conv
|
||||||
|
//go:generate go generate --skip="pam_module.go"
|
||||||
|
|
||||||
|
// Package main is the package for the integration tester module PAM shared library.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
"github.com/msteinert/pam/v2/cmd/pam-moduler/tests/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type integrationTesterModule struct {
|
||||||
|
utils.BaseModule
|
||||||
|
}
|
||||||
|
|
||||||
|
type authRequest struct {
|
||||||
|
mt pam.ModuleTransaction
|
||||||
|
lastError error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *integrationTesterModule) handleRequest(authReq *authRequest, r *Request) (res *Result, err error) {
|
||||||
|
switch r.Action {
|
||||||
|
case "bye":
|
||||||
|
return nil, authReq.lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
if s, ok := p.(string); ok {
|
||||||
|
if strings.HasPrefix(s, "reflect:") {
|
||||||
|
res = nil
|
||||||
|
err = &utils.SerializableError{Msg: fmt.Sprintf(
|
||||||
|
"error on request %v: %v", *r, p)}
|
||||||
|
authReq.lastError = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
authReq.lastError = err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
method := reflect.ValueOf(authReq.mt).MethodByName(r.Action)
|
||||||
|
if method == (reflect.Value{}) {
|
||||||
|
return nil, &utils.SerializableError{Msg: fmt.Sprintf(
|
||||||
|
"no method %s found", r.Action)}
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []reflect.Value
|
||||||
|
for i, arg := range r.ActionArgs {
|
||||||
|
switch v := arg.(type) {
|
||||||
|
case SerializableStringConvRequest:
|
||||||
|
args = append(args, reflect.ValueOf(
|
||||||
|
pam.NewStringConvRequest(v.Style, v.Request)))
|
||||||
|
case SerializableBinaryConvRequest:
|
||||||
|
args = append(args, reflect.ValueOf(
|
||||||
|
pam.NewBinaryConvRequestFromBytes(v.Request)))
|
||||||
|
default:
|
||||||
|
if arg == nil {
|
||||||
|
args = append(args, reflect.Zero(method.Type().In(i)))
|
||||||
|
} else {
|
||||||
|
args = append(args, reflect.ValueOf(arg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res = &Result{Action: "return"}
|
||||||
|
for _, ret := range method.Call(args) {
|
||||||
|
iface := ret.Interface()
|
||||||
|
switch value := iface.(type) {
|
||||||
|
case pam.StringConvResponse:
|
||||||
|
res.ActionArgs = append(res.ActionArgs,
|
||||||
|
SerializableStringConvResponse{value.Style(), value.Response()})
|
||||||
|
case pam.BinaryConvResponse:
|
||||||
|
data, err := value.Decode(utils.TestBinaryDataDecoder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res.ActionArgs = append(res.ActionArgs, SerializableBinaryConvResponse{data})
|
||||||
|
case pam.Error:
|
||||||
|
authReq.lastError = value
|
||||||
|
res.ActionArgs = append(res.ActionArgs, value)
|
||||||
|
case error:
|
||||||
|
var pamError pam.Error
|
||||||
|
if errors.As(value, &pamError) {
|
||||||
|
retErr := &SerializablePamError{Msg: value.Error(),
|
||||||
|
RetStatus: pamError}
|
||||||
|
authReq.lastError = retErr
|
||||||
|
res.ActionArgs = append(res.ActionArgs, retErr)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
authReq.lastError = value
|
||||||
|
res.ActionArgs = append(res.ActionArgs,
|
||||||
|
&utils.SerializableError{Msg: value.Error()})
|
||||||
|
default:
|
||||||
|
res.ActionArgs = append(res.ActionArgs, iface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *integrationTesterModule) handleError(err error) *Result {
|
||||||
|
return &Result{
|
||||||
|
Action: "error",
|
||||||
|
ActionArgs: []interface{}{&utils.SerializableError{Msg: err.Error()}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *integrationTesterModule) Authenticate(mt pam.ModuleTransaction, _ pam.Flags, args []string) error {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return errors.New("Invalid arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
authRequest := authRequest{mt, nil}
|
||||||
|
connection := NewConnector(args[0])
|
||||||
|
if err := connection.Connect(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionHandler := func() error {
|
||||||
|
if err := connection.SendRequest(&Request{Action: "hello"}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
req, err := connection.WaitForData()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := m.handleRequest(&authRequest, req)
|
||||||
|
if err != nil {
|
||||||
|
_ = connection.SendResult(m.handleError(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := connection.SendResult(res); err != nil {
|
||||||
|
_ = connection.SendResult(m.handleError(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := connectionHandler(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
|||||||
|
// Code generated by "pam-moduler -type integrationTesterModule -parallel-conv"; DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:generate go build "-ldflags=-extldflags -Wl,-soname,pam_go.so" -buildmode=c-shared -o pam_go.so -tags go_pam_module
|
||||||
|
|
||||||
|
// Package main is the package for the PAM module library.
|
||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo LDFLAGS: -lpam -fPIC
|
||||||
|
#include <security/pam_modules.h>
|
||||||
|
|
||||||
|
typedef const char _const_char_t;
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
"os"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pamModuleHandler pam.ModuleHandler = &integrationTesterModule{}
|
||||||
|
|
||||||
|
// sliceFromArgv returns a slice of strings given to the PAM module.
|
||||||
|
func sliceFromArgv(argc C.int, argv **C._const_char_t) []string {
|
||||||
|
r := make([]string, 0, argc)
|
||||||
|
for _, s := range unsafe.Slice(argv, argc) {
|
||||||
|
r = append(r, C.GoString(s))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePamCall is the function that translates C pam requests to Go.
|
||||||
|
func handlePamCall(pamh *C.pam_handle_t, flags C.int, argc C.int,
|
||||||
|
argv **C._const_char_t, moduleFunc pam.ModuleHandlerFunc) C.int {
|
||||||
|
if pamModuleHandler == nil {
|
||||||
|
return C.int(pam.ErrNoModuleData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if moduleFunc == nil {
|
||||||
|
return C.int(pam.ErrIgnore)
|
||||||
|
}
|
||||||
|
|
||||||
|
mt := pam.NewModuleTransactionInvokerParallelConv(pam.NativeHandle(pamh))
|
||||||
|
err := mt.InvokeHandler(moduleFunc, pam.Flags(flags),
|
||||||
|
sliceFromArgv(argc, argv))
|
||||||
|
if err == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pam.Flags(flags)&pam.Silent) == 0 && !errors.Is(err, pam.ErrIgnore) {
|
||||||
|
fmt.Fprintf(os.Stderr, "module returned error: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pamErr pam.Error
|
||||||
|
if errors.As(err, &pamErr) {
|
||||||
|
return C.int(pamErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return C.int(pam.ErrSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_authenticate
|
||||||
|
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.Authenticate)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_setcred
|
||||||
|
func pam_sm_setcred(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.SetCred)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_acct_mgmt
|
||||||
|
func pam_sm_acct_mgmt(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.AcctMgmt)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_open_session
|
||||||
|
func pam_sm_open_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.OpenSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_close_session
|
||||||
|
func pam_sm_close_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.CloseSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_chauthtok
|
||||||
|
func pam_sm_chauthtok(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.ChangeAuthTok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
"github.com/msteinert/pam/v2/cmd/pam-moduler/tests/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SerializablePamError represents a [pam.Error] in a
|
||||||
|
// serializable way that splits message and return code.
|
||||||
|
type SerializablePamError struct {
|
||||||
|
Msg string
|
||||||
|
RetStatus pam.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSerializablePamError initializes a SerializablePamError from
|
||||||
|
// the default status error message.
|
||||||
|
func NewSerializablePamError(status pam.Error) SerializablePamError {
|
||||||
|
return SerializablePamError{Msg: status.Error(), RetStatus: status}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SerializablePamError) Error() string {
|
||||||
|
return e.RetStatus.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializableStringConvRequest is a serializable string request.
|
||||||
|
type SerializableStringConvRequest struct {
|
||||||
|
Style pam.Style
|
||||||
|
Request string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializableStringConvResponse is a serializable string response.
|
||||||
|
type SerializableStringConvResponse struct {
|
||||||
|
Style pam.Style
|
||||||
|
Response string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializableBinaryConvRequest is a serializable binary request.
|
||||||
|
type SerializableBinaryConvRequest struct {
|
||||||
|
Request []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializableBinaryConvResponse is a serializable binary response.
|
||||||
|
type SerializableBinaryConvResponse struct {
|
||||||
|
Response []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gob.Register(map[string]string{})
|
||||||
|
gob.Register(Request{})
|
||||||
|
gob.Register(pam.Item(0))
|
||||||
|
gob.Register(pam.Error(0))
|
||||||
|
gob.Register(pam.Style(0))
|
||||||
|
gob.Register([]pam.ConvResponse{})
|
||||||
|
gob.RegisterName("main.SerializablePamError",
|
||||||
|
SerializablePamError{})
|
||||||
|
gob.RegisterName("main.SerializableStringConvRequest",
|
||||||
|
SerializableStringConvRequest{})
|
||||||
|
gob.RegisterName("main.SerializableStringConvResponse",
|
||||||
|
SerializableStringConvResponse{})
|
||||||
|
gob.RegisterName("main.SerializableBinaryConvRequest",
|
||||||
|
SerializableBinaryConvRequest{})
|
||||||
|
gob.RegisterName("main.SerializableBinaryConvResponse",
|
||||||
|
SerializableBinaryConvResponse{})
|
||||||
|
gob.Register(utils.SerializableError{})
|
||||||
|
}
|
||||||
38
cmd/pam-moduler/tests/internal/utils/base-module.go
Normal file
38
cmd/pam-moduler/tests/internal/utils/base-module.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "github.com/msteinert/pam/v2"
|
||||||
|
|
||||||
|
// BaseModule is the type for a base PAM module.
|
||||||
|
type BaseModule struct{}
|
||||||
|
|
||||||
|
// AcctMgmt is the handler function for PAM AcctMgmt.
|
||||||
|
func (h *BaseModule) AcctMgmt(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate is the handler function for PAM Authenticate.
|
||||||
|
func (h *BaseModule) Authenticate(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeAuthTok is the handler function for PAM ChangeAuthTok.
|
||||||
|
func (h *BaseModule) ChangeAuthTok(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenSession is the handler function for PAM OpenSession.
|
||||||
|
func (h *BaseModule) OpenSession(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseSession is the handler function for PAM CloseSession.
|
||||||
|
func (h *BaseModule) CloseSession(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCred is the handler function for PAM SetCred.
|
||||||
|
func (h *BaseModule) SetCred(pam.ModuleTransaction, pam.Flags, []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ pam.ModuleHandler = &BaseModule{}
|
||||||
35
cmd/pam-moduler/tests/internal/utils/base-module_test.go
Normal file
35
cmd/pam-moduler/tests/internal/utils/base-module_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(t *testing.T) {
|
||||||
|
bm := BaseModule{}
|
||||||
|
|
||||||
|
if bm.AcctMgmt(nil, pam.Flags(0), nil) != nil {
|
||||||
|
t.Fatalf("Unexpected non-nil value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bm.Authenticate(nil, pam.Flags(0), nil) != nil {
|
||||||
|
t.Fatalf("Unexpected non-nil value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bm.ChangeAuthTok(nil, pam.Flags(0), nil) != nil {
|
||||||
|
t.Fatalf("Unexpected non-nil value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bm.OpenSession(nil, pam.Flags(0), nil) != nil {
|
||||||
|
t.Fatalf("Unexpected non-nil value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bm.CloseSession(nil, pam.Flags(0), nil) != nil {
|
||||||
|
t.Fatalf("Unexpected non-nil value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bm.SetCred(nil, pam.Flags(0), nil) != nil {
|
||||||
|
t.Fatalf("Unexpected non-nil value")
|
||||||
|
}
|
||||||
|
}
|
||||||
135
cmd/pam-moduler/tests/internal/utils/test-setup.go
Normal file
135
cmd/pam-moduler/tests/internal/utils/test-setup.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// Package utils contains the internal test utils
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSetup is an utility type for having a playground for test PAM modules.
|
||||||
|
type TestSetup struct {
|
||||||
|
t *testing.T
|
||||||
|
workDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
type withWorkDir struct{}
|
||||||
|
|
||||||
|
//nolint:revive
|
||||||
|
func WithWorkDir() withWorkDir {
|
||||||
|
return withWorkDir{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestSetup creates a new TestSetup.
|
||||||
|
func NewTestSetup(t *testing.T, args ...interface{}) *TestSetup {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ts := &TestSetup{t: t}
|
||||||
|
for _, arg := range args {
|
||||||
|
switch argType := arg.(type) {
|
||||||
|
case withWorkDir:
|
||||||
|
ts.ensureWorkDir()
|
||||||
|
default:
|
||||||
|
t.Fatalf("Unknown parameter of type %v", argType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTemporaryDir creates a temporary directory with provided basename.
|
||||||
|
func (ts *TestSetup) CreateTemporaryDir(basename string) string {
|
||||||
|
tmpDir, err := os.MkdirTemp(os.TempDir(), basename)
|
||||||
|
if err != nil {
|
||||||
|
ts.t.Fatalf("can't create service path %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.t.Cleanup(func() { os.RemoveAll(tmpDir) })
|
||||||
|
return tmpDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *TestSetup) ensureWorkDir() string {
|
||||||
|
if ts.workDir != "" {
|
||||||
|
return ts.workDir
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.workDir = ts.CreateTemporaryDir("go-pam-*")
|
||||||
|
return ts.workDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkDir returns the test setup work directory.
|
||||||
|
func (ts TestSetup) WorkDir() string {
|
||||||
|
return ts.workDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateModule generates a PAM module for the provided path and name.
|
||||||
|
func (ts *TestSetup) GenerateModule(testModulePath string, moduleName string) string {
|
||||||
|
cmd := exec.Command("go", "generate", "-C", testModulePath)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
ts.t.Fatalf("can't build pam module %v: %s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
builtFile := filepath.Join(cmd.Dir, testModulePath, moduleName)
|
||||||
|
modulePath := filepath.Join(ts.ensureWorkDir(), filepath.Base(builtFile))
|
||||||
|
if err = os.Rename(builtFile, modulePath); err != nil {
|
||||||
|
ts.t.Fatalf("can't move module: %v", err)
|
||||||
|
os.Remove(builtFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return modulePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts TestSetup) currentFile(skip int) string {
|
||||||
|
_, currentFile, _, ok := runtime.Caller(skip)
|
||||||
|
if !ok {
|
||||||
|
ts.t.Fatalf("can't get current binary path")
|
||||||
|
}
|
||||||
|
return currentFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentFile returns the current file path.
|
||||||
|
func (ts TestSetup) GetCurrentFile() string {
|
||||||
|
// This is a library so we care about the caller location
|
||||||
|
return ts.currentFile(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentFileDir returns the current file directory.
|
||||||
|
func (ts TestSetup) GetCurrentFileDir() string {
|
||||||
|
return filepath.Dir(ts.currentFile(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateModuleDefault generates a default module.
|
||||||
|
func (ts *TestSetup) GenerateModuleDefault(testModulePath string) string {
|
||||||
|
return ts.GenerateModule(testModulePath, "pam_go.so")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateService creates a service file.
|
||||||
|
func (ts *TestSetup) CreateService(serviceName string, services []ServiceLine) string {
|
||||||
|
if !pam.CheckPamHasStartConfdir() {
|
||||||
|
ts.t.Skip("PAM has no support for custom service paths")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceName = strings.ToLower(serviceName)
|
||||||
|
serviceFile := filepath.Join(ts.ensureWorkDir(), serviceName)
|
||||||
|
var contents = []string{}
|
||||||
|
|
||||||
|
for _, s := range services {
|
||||||
|
contents = append(contents, strings.TrimRight(strings.Join([]string{
|
||||||
|
s.Action.String(), s.Control.String(), s.Module, strings.Join(s.Args, " "),
|
||||||
|
}, "\t"), "\t"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(serviceFile,
|
||||||
|
[]byte(strings.Join(contents, "\n")), 0600); err != nil {
|
||||||
|
ts.t.Fatalf("can't create service file %v: %v", serviceFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceFile
|
||||||
|
}
|
||||||
180
cmd/pam-moduler/tests/internal/utils/test-setup_test.go
Normal file
180
cmd/pam-moduler/tests/internal/utils/test-setup_test.go
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isDir(t *testing.T, path string) bool {
|
||||||
|
t.Helper()
|
||||||
|
if file, err := os.Open(path); err == nil {
|
||||||
|
if fileInfo, err := file.Stat(); err == nil {
|
||||||
|
return fileInfo.IsDir()
|
||||||
|
}
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
} else {
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_CreateTemporaryDir(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ts := NewTestSetup(t)
|
||||||
|
dir := ts.CreateTemporaryDir("")
|
||||||
|
if !isDir(t, dir) {
|
||||||
|
t.Fatalf("%s not a directory", dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = ts.CreateTemporaryDir("foo-prefix-*")
|
||||||
|
if !isDir(t, dir) {
|
||||||
|
t.Fatalf("%s not a directory", dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_TestSetupWithWorkDir(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ts := NewTestSetup(t, WithWorkDir())
|
||||||
|
if !isDir(t, ts.WorkDir()) {
|
||||||
|
t.Fatalf("%s not a directory", ts.WorkDir())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_CreateService(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ts := NewTestSetup(t)
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
services []ServiceLine
|
||||||
|
expectedContent string
|
||||||
|
}{
|
||||||
|
"empty": {},
|
||||||
|
"CApital-Empty": {},
|
||||||
|
"auth-sufficient-permit": {
|
||||||
|
services: []ServiceLine{
|
||||||
|
{Auth, Sufficient, Permit.String(), []string{}},
|
||||||
|
},
|
||||||
|
expectedContent: "auth sufficient pam_permit.so",
|
||||||
|
},
|
||||||
|
"auth-sufficient-permit-args": {
|
||||||
|
services: []ServiceLine{
|
||||||
|
{Auth, Required, Deny.String(), []string{"a b c [d e]"}},
|
||||||
|
},
|
||||||
|
expectedContent: "auth required pam_deny.so a b c [d e]",
|
||||||
|
},
|
||||||
|
"complete-custom": {
|
||||||
|
services: []ServiceLine{
|
||||||
|
{Account, Required, "pam_account_module.so", []string{"a", "b", "c", "[d e]"}},
|
||||||
|
{Account, Required, Deny.String(), []string{}},
|
||||||
|
{Auth, Requisite, "pam_auth_module.so", []string{}},
|
||||||
|
{Auth, Requisite, Deny.String(), []string{}},
|
||||||
|
{Password, Sufficient, "pam_password_module.so", []string{"arg"}},
|
||||||
|
{Password, Sufficient, Deny.String(), []string{}},
|
||||||
|
{Session, Optional, "pam_session_module.so", []string{""}},
|
||||||
|
{Session, Optional, Deny.String(), []string{}},
|
||||||
|
},
|
||||||
|
expectedContent: `account required pam_account_module.so a b c [d e]
|
||||||
|
account required pam_deny.so
|
||||||
|
auth requisite pam_auth_module.so
|
||||||
|
auth requisite pam_deny.so
|
||||||
|
password sufficient pam_password_module.so arg
|
||||||
|
password sufficient pam_deny.so
|
||||||
|
session optional pam_session_module.so
|
||||||
|
session optional pam_deny.so`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
name := name
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
service := ts.CreateService(name, tc.services)
|
||||||
|
|
||||||
|
if filepath.Base(service) != strings.ToLower(name) {
|
||||||
|
t.Fatalf("Invalid service name %s", service)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes, err := os.ReadFile(service); err != nil {
|
||||||
|
t.Fatalf("Failed reading %s: %v", service, err)
|
||||||
|
} else {
|
||||||
|
if string(bytes) != tc.expectedContent {
|
||||||
|
t.Fatalf("Unexpected file content:\n%s\n---\n%s",
|
||||||
|
tc.expectedContent, string(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GenerateModule(t *testing.T) {
|
||||||
|
ts := NewTestSetup(t)
|
||||||
|
dir := ts.CreateTemporaryDir("")
|
||||||
|
if !isDir(t, dir) {
|
||||||
|
t.Fatalf("%s not a directory", dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(filepath.Join(dir, "test-generate.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't create file %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
randomName := ""
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
// #nosec:G404 - it's a test, we don't care.
|
||||||
|
randomName += string(byte('a' + rand.Intn('z'-'a')))
|
||||||
|
}
|
||||||
|
|
||||||
|
wantFile := randomName + ".so"
|
||||||
|
fmt.Fprintf(f, `//go:generate touch %s
|
||||||
|
package generate_file
|
||||||
|
`, wantFile)
|
||||||
|
|
||||||
|
mod, err := os.Create(filepath.Join(dir, "go.mod"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't create file %v", err)
|
||||||
|
}
|
||||||
|
defer mod.Close()
|
||||||
|
|
||||||
|
fmt.Fprintf(mod, `module example.com/greetings
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
`)
|
||||||
|
|
||||||
|
fakeModule := ts.GenerateModule(dir, wantFile)
|
||||||
|
if _, err := os.Stat(fakeModule); err != nil {
|
||||||
|
t.Fatalf("module not generated %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(f, `//go:generate touch pam_go.so
|
||||||
|
package generate_file
|
||||||
|
`, wantFile)
|
||||||
|
|
||||||
|
fakeModule = ts.GenerateModuleDefault(dir)
|
||||||
|
if _, err := os.Stat(fakeModule); err != nil {
|
||||||
|
t.Fatalf("module not generated %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GetCurrentFileDir(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ts := NewTestSetup(t)
|
||||||
|
if !strings.HasSuffix(ts.GetCurrentFileDir(), filepath.Join("internal", "utils")) {
|
||||||
|
t.Fatalf("unexpected file %v", ts.GetCurrentFileDir())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GetCurrentFile(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ts := NewTestSetup(t)
|
||||||
|
if !strings.HasSuffix(ts.GetCurrentFile(), filepath.Join("utils", "test-setup_test.go")) {
|
||||||
|
t.Fatalf("unexpected file %v", ts.GetCurrentFile())
|
||||||
|
}
|
||||||
|
}
|
||||||
263
cmd/pam-moduler/tests/internal/utils/test-utils.go
Normal file
263
cmd/pam-moduler/tests/internal/utils/test-utils.go
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
// Package utils contains the internal test utils
|
||||||
|
package utils
|
||||||
|
|
||||||
|
//#include <stdint.h>
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Action represents a PAM action to perform.
|
||||||
|
type Action int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Account is the account.
|
||||||
|
Account Action = iota + 1
|
||||||
|
// Auth is the auth.
|
||||||
|
Auth
|
||||||
|
// Password is the password.
|
||||||
|
Password
|
||||||
|
// Session is the session.
|
||||||
|
Session
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a Action) String() string {
|
||||||
|
switch a {
|
||||||
|
case Account:
|
||||||
|
return "account"
|
||||||
|
case Auth:
|
||||||
|
return "auth"
|
||||||
|
case Password:
|
||||||
|
return "password"
|
||||||
|
case Session:
|
||||||
|
return "session"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions is a map with all the available Actions by their name.
|
||||||
|
var Actions = map[string]Action{
|
||||||
|
Account.String(): Account,
|
||||||
|
Auth.String(): Auth,
|
||||||
|
Password.String(): Password,
|
||||||
|
Session.String(): Session,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control represents how a PAM module should controlled in PAM service file.
|
||||||
|
type Control int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Required implies that the module is required.
|
||||||
|
Required Control = iota + 1
|
||||||
|
// Requisite implies that the module is requisite.
|
||||||
|
Requisite
|
||||||
|
// Sufficient implies that the module is sufficient.
|
||||||
|
Sufficient
|
||||||
|
// Optional implies that the module is optional.
|
||||||
|
Optional
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c Control) String() string {
|
||||||
|
switch c {
|
||||||
|
case Required:
|
||||||
|
return "required"
|
||||||
|
case Requisite:
|
||||||
|
return "requisite"
|
||||||
|
case Sufficient:
|
||||||
|
return "sufficient"
|
||||||
|
case Optional:
|
||||||
|
return "optional"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceLine is the representation of a PAM module service file line.
|
||||||
|
type ServiceLine struct {
|
||||||
|
Action Action
|
||||||
|
Control Control
|
||||||
|
Module string
|
||||||
|
Args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FallBackModule is a type to represent the module that should be used as fallback.
|
||||||
|
type FallBackModule int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NoFallback add no fallback module.
|
||||||
|
NoFallback FallBackModule = iota + 1
|
||||||
|
// Permit uses a module that always permits.
|
||||||
|
Permit
|
||||||
|
// Deny uses a module that always denys.
|
||||||
|
Deny
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a FallBackModule) String() string {
|
||||||
|
switch a {
|
||||||
|
case Permit:
|
||||||
|
return "pam_permit.so"
|
||||||
|
case Deny:
|
||||||
|
return "pam_deny.so"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializableError is a representation of an error in a way can be serialized.
|
||||||
|
type SerializableError struct {
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SerializableError) Error() string {
|
||||||
|
return e.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Credentials is a test [pam.ConversationHandler] implementation.
|
||||||
|
type Credentials struct {
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
EchoOn string
|
||||||
|
EchoOff string
|
||||||
|
TextInfo string
|
||||||
|
ErrorMsg string
|
||||||
|
ExpectedMessage string
|
||||||
|
CheckEmptyMessage bool
|
||||||
|
ExpectedStyle pam.Style
|
||||||
|
CheckZeroStyle bool
|
||||||
|
Context interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RespondPAM handles PAM string conversations.
|
||||||
|
func (c Credentials) RespondPAM(s pam.Style, msg string) (string, error) {
|
||||||
|
if (c.ExpectedMessage != "" || c.CheckEmptyMessage) &&
|
||||||
|
msg != c.ExpectedMessage {
|
||||||
|
return "", errors.Join(pam.ErrConv,
|
||||||
|
&SerializableError{
|
||||||
|
fmt.Sprintf("unexpected prompt: %s vs %s", msg, c.ExpectedMessage),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.ExpectedStyle != 0 || c.CheckZeroStyle) &&
|
||||||
|
s != c.ExpectedStyle {
|
||||||
|
return "", errors.Join(pam.ErrConv,
|
||||||
|
&SerializableError{
|
||||||
|
fmt.Sprintf("unexpected style: %#v vs %#v", s, c.ExpectedStyle),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case pam.PromptEchoOn:
|
||||||
|
if c.User != "" {
|
||||||
|
return c.User, nil
|
||||||
|
}
|
||||||
|
return c.EchoOn, nil
|
||||||
|
case pam.PromptEchoOff:
|
||||||
|
if c.Password != "" {
|
||||||
|
return c.Password, nil
|
||||||
|
}
|
||||||
|
return c.EchoOff, nil
|
||||||
|
case pam.TextInfo:
|
||||||
|
return c.TextInfo, nil
|
||||||
|
case pam.ErrorMsg:
|
||||||
|
return c.ErrorMsg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.Join(pam.ErrConv,
|
||||||
|
&SerializableError{fmt.Sprintf("unhandled style: %v", s)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinaryTransaction represents a binary PAM transaction handler struct.
|
||||||
|
type BinaryTransaction struct {
|
||||||
|
data []byte
|
||||||
|
ExpectedNull bool
|
||||||
|
ReturnedData []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBinaryDataEncoder encodes a test binary data.
|
||||||
|
func TestBinaryDataEncoder(bytes []byte) []byte {
|
||||||
|
if len(bytes) > 0xff {
|
||||||
|
panic("Binary transaction size not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes == nil {
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, 0, len(bytes)+1)
|
||||||
|
data = append(data, byte(len(bytes)))
|
||||||
|
data = append(data, bytes...)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBinaryDataDecoder decodes a test binary data.
|
||||||
|
func TestBinaryDataDecoder(ptr pam.BinaryPointer) ([]byte, error) {
|
||||||
|
if ptr == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
length := uint8(*((*C.uint8_t)(ptr)))
|
||||||
|
if length == 0 {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
return C.GoBytes(unsafe.Pointer(ptr), C.int(length+1))[1:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBinaryTransactionWithData creates a new [pam.BinaryTransaction] from bytes.
|
||||||
|
func NewBinaryTransactionWithData(data []byte, retData []byte) BinaryTransaction {
|
||||||
|
t := BinaryTransaction{ReturnedData: retData}
|
||||||
|
t.data = TestBinaryDataEncoder(data)
|
||||||
|
t.ExpectedNull = data == nil
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBinaryTransactionWithRandomData creates a new [pam.BinaryTransaction] with random data.
|
||||||
|
func NewBinaryTransactionWithRandomData(size uint8, retData []byte) BinaryTransaction {
|
||||||
|
t := BinaryTransaction{ReturnedData: retData}
|
||||||
|
randomData := make([]byte, size)
|
||||||
|
if err := binary.Read(rand.Reader, binary.LittleEndian, &randomData); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.data = TestBinaryDataEncoder(randomData)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data returns the bytes of the transaction.
|
||||||
|
func (b BinaryTransaction) Data() []byte {
|
||||||
|
return b.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// RespondPAM (not) handles the PAM string conversations.
|
||||||
|
func (b BinaryTransaction) RespondPAM(s pam.Style, msg string) (string, error) {
|
||||||
|
return "", errors.Join(pam.ErrConv,
|
||||||
|
&SerializableError{"unexpected non-binary request"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RespondPAMBinary handles the PAM binary conversations.
|
||||||
|
func (b BinaryTransaction) RespondPAMBinary(ptr pam.BinaryPointer) ([]byte, error) {
|
||||||
|
if ptr == nil && !b.ExpectedNull {
|
||||||
|
return nil, errors.Join(pam.ErrConv,
|
||||||
|
&SerializableError{"unexpected null binary data"})
|
||||||
|
} else if ptr == nil {
|
||||||
|
return TestBinaryDataEncoder(b.ReturnedData), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, _ := TestBinaryDataDecoder(ptr)
|
||||||
|
if !reflect.DeepEqual(bytes, b.data[1:]) {
|
||||||
|
return nil, errors.Join(pam.ErrConv,
|
||||||
|
&SerializableError{
|
||||||
|
fmt.Sprintf("data mismatch %#v vs %#v", bytes, b.data[1:]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return TestBinaryDataEncoder(b.ReturnedData), nil
|
||||||
|
}
|
||||||
94
errors.go
Normal file
94
errors.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package pam
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include <security/pam_appl.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
// Error is the Type for PAM Return types
|
||||||
|
type Error int
|
||||||
|
|
||||||
|
// Pam Return types
|
||||||
|
const (
|
||||||
|
// OpenErr indicates a dlopen() failure when dynamically loading a
|
||||||
|
// service module.
|
||||||
|
ErrOpen Error = C.PAM_OPEN_ERR
|
||||||
|
// ErrSymbol indicates a symbol not found.
|
||||||
|
ErrSymbol Error = C.PAM_SYMBOL_ERR
|
||||||
|
// ErrService indicates a error in service module.
|
||||||
|
ErrService Error = C.PAM_SERVICE_ERR
|
||||||
|
// ErrSystem indicates a system error.
|
||||||
|
ErrSystem Error = C.PAM_SYSTEM_ERR
|
||||||
|
// ErrBuf indicates a memory buffer error.
|
||||||
|
ErrBuf Error = C.PAM_BUF_ERR
|
||||||
|
// ErrPermDenied indicates a permission denied.
|
||||||
|
ErrPermDenied Error = C.PAM_PERM_DENIED
|
||||||
|
// ErrAuth indicates a authentication failure.
|
||||||
|
ErrAuth Error = C.PAM_AUTH_ERR
|
||||||
|
// ErrCredInsufficient indicates a can not access authentication data due to
|
||||||
|
// insufficient credentials.
|
||||||
|
ErrCredInsufficient Error = C.PAM_CRED_INSUFFICIENT
|
||||||
|
// ErrAuthinfoUnavail indicates that the underlying authentication service
|
||||||
|
// can not retrieve authentication information.
|
||||||
|
ErrAuthinfoUnavail Error = C.PAM_AUTHINFO_UNAVAIL
|
||||||
|
// ErrUserUnknown indicates a user not known to the underlying authentication
|
||||||
|
// module.
|
||||||
|
ErrUserUnknown Error = C.PAM_USER_UNKNOWN
|
||||||
|
// ErrMaxtries indicates that an authentication service has maintained a retry
|
||||||
|
// count which has been reached. No further retries should be attempted.
|
||||||
|
ErrMaxtries Error = C.PAM_MAXTRIES
|
||||||
|
// ErrNewAuthtokReqd indicates a new authentication token required. This is
|
||||||
|
// normally returned if the machine security policies require that the
|
||||||
|
// password should be changed because the password is nil or it has aged.
|
||||||
|
ErrNewAuthtokReqd Error = C.PAM_NEW_AUTHTOK_REQD
|
||||||
|
// ErrAcctExpired indicates that an user account has expired.
|
||||||
|
ErrAcctExpired Error = C.PAM_ACCT_EXPIRED
|
||||||
|
// ErrSession indicates a can not make/remove an entry for the
|
||||||
|
// specified session.
|
||||||
|
ErrSession Error = C.PAM_SESSION_ERR
|
||||||
|
// ErrCredUnavail indicates that an underlying authentication service can not
|
||||||
|
// retrieve user credentials.
|
||||||
|
ErrCredUnavail Error = C.PAM_CRED_UNAVAIL
|
||||||
|
// ErrCredExpired indicates that an user credentials expired.
|
||||||
|
ErrCredExpired Error = C.PAM_CRED_EXPIRED
|
||||||
|
// ErrCred indicates a failure setting user credentials.
|
||||||
|
ErrCred Error = C.PAM_CRED_ERR
|
||||||
|
// ErrNoModuleData indicates a no module specific data is present.
|
||||||
|
ErrNoModuleData Error = C.PAM_NO_MODULE_DATA
|
||||||
|
// ErrConv indicates a conversation error.
|
||||||
|
ErrConv Error = C.PAM_CONV_ERR
|
||||||
|
// ErrAuthtokErr indicates an authentication token manipulation error.
|
||||||
|
ErrAuthtok Error = C.PAM_AUTHTOK_ERR
|
||||||
|
// ErrAuthtokRecoveryErr indicates an authentication information cannot
|
||||||
|
// be recovered.
|
||||||
|
ErrAuthtokRecovery Error = C.PAM_AUTHTOK_RECOVERY_ERR
|
||||||
|
// ErrAuthtokLockBusy indicates am authentication token lock busy.
|
||||||
|
ErrAuthtokLockBusy Error = C.PAM_AUTHTOK_LOCK_BUSY
|
||||||
|
// ErrAuthtokDisableAging indicates an authentication token aging disabled.
|
||||||
|
ErrAuthtokDisableAging Error = C.PAM_AUTHTOK_DISABLE_AGING
|
||||||
|
// ErrTryAgain indicates a preliminary check by password service.
|
||||||
|
ErrTryAgain Error = C.PAM_TRY_AGAIN
|
||||||
|
// ErrIgnore indicates to ignore underlying account module regardless of
|
||||||
|
// whether the control flag is required, optional, or sufficient.
|
||||||
|
ErrIgnore Error = C.PAM_IGNORE
|
||||||
|
// ErrAbort indicates a critical error (module fail now request).
|
||||||
|
ErrAbort Error = C.PAM_ABORT
|
||||||
|
// ErrAuthtokExpired indicates an user's authentication token has expired.
|
||||||
|
ErrAuthtokExpired Error = C.PAM_AUTHTOK_EXPIRED
|
||||||
|
// ErrModuleUnknown indicates a module is not known.
|
||||||
|
ErrModuleUnknown Error = C.PAM_MODULE_UNKNOWN
|
||||||
|
// ErrBadItem indicates a bad item passed to pam_*_item().
|
||||||
|
ErrBadItem Error = C.PAM_BAD_ITEM
|
||||||
|
// ErrConvAgain indicates a conversation function is event driven and data
|
||||||
|
// is not available yet.
|
||||||
|
ErrConvAgain Error = C.PAM_CONV_AGAIN
|
||||||
|
// ErrIncomplete indicates to please call this function again to complete
|
||||||
|
// authentication stack. Before calling again, verify that conversation
|
||||||
|
// is completed.
|
||||||
|
ErrIncomplete Error = C.PAM_INCOMPLETE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error returns the error message for the given status.
|
||||||
|
func (status Error) Error() string {
|
||||||
|
return C.GoString(C.pam_strerror(nil, C.int(status)))
|
||||||
|
}
|
||||||
50
example-module/module.go
Normal file
50
example-module/module.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// These go:generate directive allow to generate the module by just using
|
||||||
|
// `go generate` once in the module directory.
|
||||||
|
// This is not strictly needed
|
||||||
|
|
||||||
|
//go:generate go run github.com/msteinert/pam/v2/cmd/pam-moduler
|
||||||
|
//go:generate go generate --skip="pam_module.go"
|
||||||
|
|
||||||
|
// Package main provides the module shared library.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type exampleHandler struct{}
|
||||||
|
|
||||||
|
var pamModuleHandler pam.ModuleHandler = &exampleHandler{}
|
||||||
|
var _ = pamModuleHandler
|
||||||
|
|
||||||
|
// AcctMgmt is the module handle function for account management.
|
||||||
|
func (h *exampleHandler) AcctMgmt(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return fmt.Errorf("AcctMgmt not implemented: %w", pam.ErrIgnore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate is the module handle function for authentication.
|
||||||
|
func (h *exampleHandler) Authenticate(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return pam.ErrAuthinfoUnavail
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeAuthTok is the module handle function for changing authentication token.
|
||||||
|
func (h *exampleHandler) ChangeAuthTok(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return fmt.Errorf("ChangeAuthTok not implemented: %w", pam.ErrIgnore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenSession is the module handle function for open session.
|
||||||
|
func (h *exampleHandler) OpenSession(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return fmt.Errorf("OpenSession not implemented: %w", pam.ErrIgnore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseSession is the module handle function for close session.
|
||||||
|
func (h *exampleHandler) CloseSession(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return fmt.Errorf("CloseSession not implemented: %w", pam.ErrIgnore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCred is the module handle function for set credentials.
|
||||||
|
func (h *exampleHandler) SetCred(mt pam.ModuleTransaction, flags pam.Flags, args []string) error {
|
||||||
|
return fmt.Errorf("SetCred not implemented: %w", pam.ErrIgnore)
|
||||||
|
}
|
||||||
96
example-module/pam_module.go
Normal file
96
example-module/pam_module.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// Code generated by "pam-moduler "; DO NOT EDIT.
|
||||||
|
|
||||||
|
//go:generate go build "-ldflags=-extldflags -Wl,-soname,pam_go.so" -buildmode=c-shared -o pam_go.so -tags go_pam_module
|
||||||
|
|
||||||
|
// Package main is the package for the PAM module library.
|
||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo LDFLAGS: -lpam -fPIC
|
||||||
|
#include <security/pam_modules.h>
|
||||||
|
|
||||||
|
typedef const char _const_char_t;
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/msteinert/pam/v2"
|
||||||
|
"os"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Do a typecheck at compile time
|
||||||
|
var _ pam.ModuleHandler = pamModuleHandler
|
||||||
|
|
||||||
|
// sliceFromArgv returns a slice of strings given to the PAM module.
|
||||||
|
func sliceFromArgv(argc C.int, argv **C._const_char_t) []string {
|
||||||
|
r := make([]string, 0, argc)
|
||||||
|
for _, s := range unsafe.Slice(argv, argc) {
|
||||||
|
r = append(r, C.GoString(s))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePamCall is the function that translates C pam requests to Go.
|
||||||
|
func handlePamCall(pamh *C.pam_handle_t, flags C.int, argc C.int,
|
||||||
|
argv **C._const_char_t, moduleFunc pam.ModuleHandlerFunc) C.int {
|
||||||
|
if pamModuleHandler == nil {
|
||||||
|
return C.int(pam.ErrNoModuleData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if moduleFunc == nil {
|
||||||
|
return C.int(pam.ErrIgnore)
|
||||||
|
}
|
||||||
|
|
||||||
|
mt := pam.NewModuleTransactionInvoker(pam.NativeHandle(pamh))
|
||||||
|
err := mt.InvokeHandler(moduleFunc, pam.Flags(flags),
|
||||||
|
sliceFromArgv(argc, argv))
|
||||||
|
if err == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pam.Flags(flags)&pam.Silent) == 0 && !errors.Is(err, pam.ErrIgnore) {
|
||||||
|
fmt.Fprintf(os.Stderr, "module returned error: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pamErr pam.Error
|
||||||
|
if errors.As(err, &pamErr) {
|
||||||
|
return C.int(pamErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return C.int(pam.ErrSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_authenticate
|
||||||
|
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.Authenticate)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_setcred
|
||||||
|
func pam_sm_setcred(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.SetCred)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_acct_mgmt
|
||||||
|
func pam_sm_acct_mgmt(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.AcctMgmt)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_open_session
|
||||||
|
func pam_sm_open_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.OpenSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_close_session
|
||||||
|
func pam_sm_close_session(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.CloseSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export pam_sm_chauthtok
|
||||||
|
func pam_sm_chauthtok(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C._const_char_t) C.int {
|
||||||
|
return handlePamCall(pamh, flags, argc, argv, pamModuleHandler.ChangeAuthTok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {}
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/msteinert/pam"
|
"github.com/msteinert/pam/v2"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
// should cause PAM to ask its conversation handler for a username and password
|
// should cause PAM to ask its conversation handler for a username and password
|
||||||
// in sequence.
|
// in sequence.
|
||||||
func Example() {
|
func Example() {
|
||||||
t, err := pam.StartFunc("", "", func(s pam.Style, msg string) (string, error) {
|
t, err := pam.StartFunc("passwd", "", func(s pam.Style, msg string) (string, error) {
|
||||||
switch s {
|
switch s {
|
||||||
case pam.PromptEchoOff:
|
case pam.PromptEchoOff:
|
||||||
fmt.Print(msg)
|
fmt.Print(msg)
|
||||||
@@ -40,12 +40,19 @@ func Example() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "start: %s\n", err.Error())
|
fmt.Fprintf(os.Stderr, "start: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
err := t.End()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "end: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
err = t.Authenticate(0)
|
err = t.Authenticate(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "authenticate: %s\n", err.Error())
|
fmt.Fprintf(os.Stderr, "authenticate: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fmt.Println("authentication succeeded!")
|
fmt.Println("authentication succeeded!")
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module github.com/msteinert/pam
|
module github.com/msteinert/pam/v2
|
||||||
|
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
|
|||||||
233
module-transaction-mock.go
Normal file
233
module-transaction-mock.go
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
//go:build !go_pam_module
|
||||||
|
|
||||||
|
package pam
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -Wall -std=c99
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <security/pam_modules.h>
|
||||||
|
|
||||||
|
void init_pam_conv(struct pam_conv *conv, uintptr_t appdata);
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"runtime/cgo"
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockModuleTransactionExpectations struct {
|
||||||
|
UserPrompt string
|
||||||
|
DataKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockModuleTransactionReturnedData struct {
|
||||||
|
User string
|
||||||
|
InteractiveUser bool
|
||||||
|
Status Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockModuleTransaction struct {
|
||||||
|
moduleTransaction
|
||||||
|
T *testing.T
|
||||||
|
Expectations mockModuleTransactionExpectations
|
||||||
|
RetData mockModuleTransactionReturnedData
|
||||||
|
ConversationHandler ConversationHandler
|
||||||
|
moduleData map[string]uintptr
|
||||||
|
allocatedData []unsafe.Pointer
|
||||||
|
binaryProtocol bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockModuleTransaction(m *mockModuleTransaction) *mockModuleTransaction {
|
||||||
|
m.moduleData = make(map[string]uintptr)
|
||||||
|
m.binaryProtocol = true
|
||||||
|
runtime.SetFinalizer(m, func(m *mockModuleTransaction) {
|
||||||
|
for _, ptr := range m.allocatedData {
|
||||||
|
C.free(ptr)
|
||||||
|
}
|
||||||
|
for _, handle := range m.moduleData {
|
||||||
|
_go_pam_data_cleanup(nil, C.uintptr_t(handle), C.PAM_DATA_SILENT)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockModuleTransaction) getUser(outUser **C.char, prompt *C.char) C.int {
|
||||||
|
goPrompt := C.GoString(prompt)
|
||||||
|
if goPrompt != m.Expectations.UserPrompt {
|
||||||
|
m.T.Fatalf("unexpected prompt: %s vs %s", goPrompt, m.Expectations.UserPrompt)
|
||||||
|
return C.int(ErrAbort)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := m.RetData.User
|
||||||
|
if m.RetData.InteractiveUser || (m.RetData.User == "" && m.ConversationHandler != nil) {
|
||||||
|
if m.ConversationHandler == nil {
|
||||||
|
m.T.Fatalf("no conversation handler provided")
|
||||||
|
}
|
||||||
|
u, err := m.ConversationHandler.RespondPAM(PromptEchoOn, goPrompt)
|
||||||
|
user = u
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
var pamErr Error
|
||||||
|
if errors.As(err, &pamErr) {
|
||||||
|
return C.int(pamErr)
|
||||||
|
}
|
||||||
|
return C.int(ErrAbort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cUser := C.CString(user)
|
||||||
|
m.allocatedData = append(m.allocatedData, unsafe.Pointer(cUser))
|
||||||
|
|
||||||
|
*outUser = cUser
|
||||||
|
return C.int(m.RetData.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockModuleTransaction) getData(key *C.char, outHandle *C.uintptr_t) C.int {
|
||||||
|
goKey := C.GoString(key)
|
||||||
|
if m.Expectations.DataKey != "" && goKey != m.Expectations.DataKey {
|
||||||
|
m.T.Fatalf("data key mismatch: %#v vs %#v", goKey, m.Expectations.DataKey)
|
||||||
|
}
|
||||||
|
if handle, ok := m.moduleData[goKey]; ok {
|
||||||
|
*outHandle = C.uintptr_t(handle)
|
||||||
|
} else {
|
||||||
|
*outHandle = 0
|
||||||
|
}
|
||||||
|
return C.int(m.RetData.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockModuleTransaction) setData(key *C.char, handle C.uintptr_t) C.int {
|
||||||
|
goKey := C.GoString(key)
|
||||||
|
if m.Expectations.DataKey != "" && goKey != m.Expectations.DataKey {
|
||||||
|
m.T.Fatalf("data key mismatch: %#v vs %#v", goKey, m.Expectations.DataKey)
|
||||||
|
}
|
||||||
|
if oldHandle, ok := m.moduleData[goKey]; ok {
|
||||||
|
_go_pam_data_cleanup(nil, C.uintptr_t(oldHandle), C.PAM_DATA_REPLACE)
|
||||||
|
}
|
||||||
|
if handle != 0 {
|
||||||
|
m.moduleData[goKey] = uintptr(handle)
|
||||||
|
}
|
||||||
|
return C.int(m.RetData.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockModuleTransaction) getConv() (*C.struct_pam_conv, error) {
|
||||||
|
if m.ConversationHandler != nil {
|
||||||
|
conv := C.struct_pam_conv{}
|
||||||
|
handler := cgo.NewHandle(m.ConversationHandler)
|
||||||
|
C.init_pam_conv(&conv, C.uintptr_t(handler))
|
||||||
|
return &conv, nil
|
||||||
|
}
|
||||||
|
if C.int(m.RetData.Status) != success {
|
||||||
|
return nil, m.RetData.Status
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockModuleTransaction) hasBinaryProtocol() bool {
|
||||||
|
return m.binaryProtocol
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockConversationHandler struct {
|
||||||
|
User string
|
||||||
|
PromptEchoOn string
|
||||||
|
PromptEchoOff string
|
||||||
|
TextInfo string
|
||||||
|
ErrorMsg string
|
||||||
|
Binary []byte
|
||||||
|
ExpectedMessage string
|
||||||
|
ExpectedMessagesByStyle map[Style]string
|
||||||
|
ExpectedNil bool
|
||||||
|
ExpectedBinary []byte
|
||||||
|
CheckEmptyMessage bool
|
||||||
|
ExpectedStyle Style
|
||||||
|
CheckZeroStyle bool
|
||||||
|
IgnoreUnknownStyle bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c mockConversationHandler) RespondPAM(s Style, msg string) (string, error) {
|
||||||
|
var expectedMsg = c.ExpectedMessage
|
||||||
|
if msg, ok := c.ExpectedMessagesByStyle[s]; ok {
|
||||||
|
expectedMsg = msg
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedMsg != "" || c.CheckEmptyMessage) &&
|
||||||
|
msg != expectedMsg {
|
||||||
|
return "", fmt.Errorf("%w: unexpected prompt: %s vs %s",
|
||||||
|
ErrConv, msg, c.ExpectedMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.ExpectedStyle != 0 || c.CheckZeroStyle) &&
|
||||||
|
s != c.ExpectedStyle {
|
||||||
|
return "", fmt.Errorf("%w: unexpected style: %#v vs %#v",
|
||||||
|
ErrConv, s, c.ExpectedStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case PromptEchoOn:
|
||||||
|
if c.User != "" {
|
||||||
|
return c.User, nil
|
||||||
|
}
|
||||||
|
return c.PromptEchoOn, nil
|
||||||
|
case PromptEchoOff:
|
||||||
|
return c.PromptEchoOff, nil
|
||||||
|
case TextInfo:
|
||||||
|
return c.TextInfo, nil
|
||||||
|
case ErrorMsg:
|
||||||
|
return c.ErrorMsg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IgnoreUnknownStyle {
|
||||||
|
return c.ExpectedMessage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("%w: unhandled style: %v", ErrConv, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBinaryDataEncoder(bytes []byte) []byte {
|
||||||
|
if len(bytes) > 0xff {
|
||||||
|
panic("Binary transaction size not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes == nil {
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, 0, len(bytes)+1)
|
||||||
|
data = append(data, byte(len(bytes)))
|
||||||
|
data = append(data, bytes...)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBinaryDataDecoder(ptr BinaryPointer) ([]byte, error) {
|
||||||
|
if ptr == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
length := uint8(*((*C.uint8_t)(ptr)))
|
||||||
|
if length == 0 {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
return C.GoBytes(unsafe.Pointer(ptr), C.int(length+1))[1:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c mockConversationHandler) RespondPAMBinary(ptr BinaryPointer) ([]byte, error) {
|
||||||
|
if ptr == nil && !c.ExpectedNil {
|
||||||
|
return nil, fmt.Errorf("%w: unexpected null binary data", ErrConv)
|
||||||
|
} else if ptr == nil {
|
||||||
|
return testBinaryDataEncoder(c.Binary), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, _ := testBinaryDataDecoder(ptr)
|
||||||
|
if !reflect.DeepEqual(bytes, c.ExpectedBinary) {
|
||||||
|
return nil, fmt.Errorf("%w: data mismatch %#v vs %#v",
|
||||||
|
ErrConv, bytes, c.ExpectedBinary)
|
||||||
|
}
|
||||||
|
|
||||||
|
return testBinaryDataEncoder(c.Binary), nil
|
||||||
|
}
|
||||||
627
module-transaction.go
Normal file
627
module-transaction.go
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
// Package pam provides a wrapper for the PAM application API.
|
||||||
|
package pam
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include "transaction.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"runtime/cgo"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxNumMsg = C.PAM_MAX_NUM_MSG
|
||||||
|
|
||||||
|
// ModuleTransaction is an interface that a pam module transaction
|
||||||
|
// should implement.
|
||||||
|
type ModuleTransaction interface {
|
||||||
|
SetItem(Item, string) error
|
||||||
|
GetItem(Item) (string, error)
|
||||||
|
PutEnv(nameVal string) error
|
||||||
|
GetEnv(name string) string
|
||||||
|
GetEnvList() (map[string]string, error)
|
||||||
|
GetUser(prompt string) (string, error)
|
||||||
|
SetData(key string, data any) error
|
||||||
|
GetData(key string) (any, error)
|
||||||
|
StartStringConv(style Style, prompt string) (StringConvResponse, error)
|
||||||
|
StartStringConvf(style Style, format string, args ...interface{}) (
|
||||||
|
StringConvResponse, error)
|
||||||
|
StartBinaryConv([]byte) (BinaryConvResponse, error)
|
||||||
|
StartConv(ConvRequest) (ConvResponse, error)
|
||||||
|
StartConvMulti([]ConvRequest) ([]ConvResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModuleHandlerFunc is a function type used by the ModuleHandler.
|
||||||
|
type ModuleHandlerFunc func(ModuleTransaction, Flags, []string) error
|
||||||
|
|
||||||
|
// ModuleTransaction is the module-side handle for a PAM transaction.
|
||||||
|
type moduleTransaction struct {
|
||||||
|
transactionBase
|
||||||
|
convMutex *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModuleHandler is an interface for objects that can be used to create
|
||||||
|
// PAM modules from go.
|
||||||
|
type ModuleHandler interface {
|
||||||
|
AcctMgmt(ModuleTransaction, Flags, []string) error
|
||||||
|
Authenticate(ModuleTransaction, Flags, []string) error
|
||||||
|
ChangeAuthTok(ModuleTransaction, Flags, []string) error
|
||||||
|
CloseSession(ModuleTransaction, Flags, []string) error
|
||||||
|
OpenSession(ModuleTransaction, Flags, []string) error
|
||||||
|
SetCred(ModuleTransaction, Flags, []string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModuleTransactionInvoker is an interface that a pam module transaction
|
||||||
|
// should implement to redirect requests from C handlers to go,
|
||||||
|
type ModuleTransactionInvoker interface {
|
||||||
|
ModuleTransaction
|
||||||
|
InvokeHandler(handler ModuleHandlerFunc, flags Flags, args []string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewModuleTransactionParallelConv allows initializing a transaction from the
|
||||||
|
// module side. Conversations using this transaction can be multi-thread, but
|
||||||
|
// this requires the application loading the module to support this, otherwise
|
||||||
|
// we may just break their assumptions.
|
||||||
|
func NewModuleTransactionParallelConv(handle NativeHandle) ModuleTransaction {
|
||||||
|
return &moduleTransaction{transactionBase{handle: handle}, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewModuleTransactionInvoker allows initializing a transaction invoker from the
|
||||||
|
// module side.
|
||||||
|
func NewModuleTransactionInvoker(handle NativeHandle) ModuleTransactionInvoker {
|
||||||
|
return &moduleTransaction{transactionBase{handle: handle}, &sync.Mutex{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewModuleTransactionInvokerParallelConv allows initializing a transaction invoker
|
||||||
|
// from the module side.
|
||||||
|
// Conversations using this transaction can be multi-thread, but this requires
|
||||||
|
// the application loading the module to support this, otherwise we may just
|
||||||
|
// break their assumptions.
|
||||||
|
func NewModuleTransactionInvokerParallelConv(handle NativeHandle) ModuleTransactionInvoker {
|
||||||
|
return &moduleTransaction{transactionBase{handle: handle}, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *moduleTransaction) InvokeHandler(handler ModuleHandlerFunc,
|
||||||
|
flags Flags, args []string) error {
|
||||||
|
invoker := func() error {
|
||||||
|
if handler == nil {
|
||||||
|
return ErrIgnore
|
||||||
|
}
|
||||||
|
err := handler(m, flags, args)
|
||||||
|
if err != nil {
|
||||||
|
service, _ := m.GetItem(Service)
|
||||||
|
|
||||||
|
var pamErr Error
|
||||||
|
if !errors.As(err, &pamErr) {
|
||||||
|
err = ErrSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
if pamErr == ErrIgnore || service == "" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%s failed: %w", service, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := invoker()
|
||||||
|
if errors.Is(err, Error(0)) {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
var status int32
|
||||||
|
if err != nil {
|
||||||
|
status = int32(ErrSystem)
|
||||||
|
|
||||||
|
var pamErr Error
|
||||||
|
if errors.As(err, &pamErr) {
|
||||||
|
status = int32(pamErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.lastStatus.Store(status)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type moduleTransactionIface interface {
|
||||||
|
getUser(outUser **C.char, prompt *C.char) C.int
|
||||||
|
setData(key *C.char, handle C.uintptr_t) C.int
|
||||||
|
getData(key *C.char, outHandle *C.uintptr_t) C.int
|
||||||
|
getConv() (*C.struct_pam_conv, error)
|
||||||
|
hasBinaryProtocol() bool
|
||||||
|
startConv(conv *C.struct_pam_conv, nMsg C.int,
|
||||||
|
messages **C.struct_pam_message,
|
||||||
|
outResponses **C.struct_pam_response) C.int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *moduleTransaction) getUser(outUser **C.char, prompt *C.char) C.int {
|
||||||
|
return C.pam_get_user(m.handle, outUser, prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUserImpl is the default implementation for GetUser, but kept as private so
|
||||||
|
// that can be used to test the pam package
|
||||||
|
func (m *moduleTransaction) getUserImpl(iface moduleTransactionIface,
|
||||||
|
prompt string) (string, error) {
|
||||||
|
var user *C.char
|
||||||
|
var cPrompt = C.CString(prompt)
|
||||||
|
defer C.free(unsafe.Pointer(cPrompt))
|
||||||
|
err := m.handlePamStatus(iface.getUser(&user, cPrompt))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return C.GoString(user), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser is similar to GetItem(User), but it would start a conversation if
|
||||||
|
// no user is currently set in PAM.
|
||||||
|
func (m *moduleTransaction) GetUser(prompt string) (string, error) {
|
||||||
|
return m.getUserImpl(m, prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetData allows to save any value in the module data that is preserved
|
||||||
|
// during the whole time the module is loaded.
|
||||||
|
func (m *moduleTransaction) SetData(key string, data any) error {
|
||||||
|
return m.setDataImpl(m, key, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *moduleTransaction) setData(key *C.char, handle C.uintptr_t) C.int {
|
||||||
|
return C.set_data(m.handle, key, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDataImpl is the implementation for SetData for testing purposes.
|
||||||
|
func (m *moduleTransaction) setDataImpl(iface moduleTransactionIface,
|
||||||
|
key string, data any) error {
|
||||||
|
var cKey = C.CString(key)
|
||||||
|
defer C.free(unsafe.Pointer(cKey))
|
||||||
|
var handle cgo.Handle
|
||||||
|
if data != nil {
|
||||||
|
handle = cgo.NewHandle(data)
|
||||||
|
}
|
||||||
|
return m.handlePamStatus(iface.setData(cKey, C.uintptr_t(handle)))
|
||||||
|
}
|
||||||
|
|
||||||
|
//export _go_pam_data_cleanup
|
||||||
|
func _go_pam_data_cleanup(h NativeHandle, handle C.uintptr_t, status C.int) {
|
||||||
|
cgo.Handle(handle).Delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetData allows to get any value from the module data saved using SetData
|
||||||
|
// that is preserved across the whole time the module is loaded.
|
||||||
|
func (m *moduleTransaction) GetData(key string) (any, error) {
|
||||||
|
return m.getDataImpl(m, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *moduleTransaction) getData(key *C.char, outHandle *C.uintptr_t) C.int {
|
||||||
|
return C.get_data(m.handle, key, outHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDataImpl is the implementation for GetData for testing purposes.
|
||||||
|
func (m *moduleTransaction) getDataImpl(iface moduleTransactionIface,
|
||||||
|
key string) (any, error) {
|
||||||
|
var cKey = C.CString(key)
|
||||||
|
defer C.free(unsafe.Pointer(cKey))
|
||||||
|
var handle C.uintptr_t
|
||||||
|
if err := m.handlePamStatus(iface.getData(cKey, &handle)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if goHandle := cgo.Handle(handle); goHandle != cgo.Handle(0) {
|
||||||
|
return goHandle.Value(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, m.handlePamStatus(C.int(ErrNoModuleData))
|
||||||
|
}
|
||||||
|
|
||||||
|
// getConv is a private function to get the conversation pointer to be used
|
||||||
|
// with C.do_conv() to initiate conversations.
|
||||||
|
func (m *moduleTransaction) getConv() (*C.struct_pam_conv, error) {
|
||||||
|
var convPtr unsafe.Pointer
|
||||||
|
|
||||||
|
if err := m.handlePamStatus(
|
||||||
|
C.pam_get_item(m.handle, C.PAM_CONV, &convPtr)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return (*C.struct_pam_conv)(convPtr), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvRequest is an interface that all the Conversation requests should
|
||||||
|
// implement.
|
||||||
|
type ConvRequest interface {
|
||||||
|
Style() Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvResponse is an interface that all the Conversation responses should
|
||||||
|
// implement.
|
||||||
|
type ConvResponse interface {
|
||||||
|
Style() Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringConvRequest is a ConvRequest for performing text-based conversations.
|
||||||
|
type StringConvRequest struct {
|
||||||
|
style Style
|
||||||
|
prompt string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStringConvRequest creates a new StringConvRequest.
|
||||||
|
func NewStringConvRequest(style Style, prompt string) StringConvRequest {
|
||||||
|
return StringConvRequest{style, prompt}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style returns the conversation style of the StringConvRequest.
|
||||||
|
func (s StringConvRequest) Style() Style {
|
||||||
|
return s.style
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt returns the conversation style of the StringConvRequest.
|
||||||
|
func (s StringConvRequest) Prompt() string {
|
||||||
|
return s.prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringConvResponse is an interface that string Conversation responses implements.
|
||||||
|
type StringConvResponse interface {
|
||||||
|
ConvResponse
|
||||||
|
Response() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringConvResponse is a StringConvResponse implementation used for text-based
|
||||||
|
// conversation responses.
|
||||||
|
type stringConvResponse struct {
|
||||||
|
style Style
|
||||||
|
response string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style returns the conversation style of the StringConvResponse.
|
||||||
|
func (s stringConvResponse) Style() Style {
|
||||||
|
return s.style
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response returns the string response of the conversation.
|
||||||
|
func (s stringConvResponse) Response() string {
|
||||||
|
return s.response
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinaryFinalizer is a type of function that can be used to release
|
||||||
|
// the binary when it's not required anymore
|
||||||
|
type BinaryFinalizer func(BinaryPointer)
|
||||||
|
|
||||||
|
// BinaryConvRequester is the interface that binary ConvRequests should
|
||||||
|
// implement
|
||||||
|
type BinaryConvRequester interface {
|
||||||
|
ConvRequest
|
||||||
|
Pointer() BinaryPointer
|
||||||
|
CreateResponse(BinaryPointer) BinaryConvResponse
|
||||||
|
Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinaryConvRequest is a ConvRequest for performing binary conversations.
|
||||||
|
type BinaryConvRequest struct {
|
||||||
|
ptr atomic.Uintptr
|
||||||
|
finalizer BinaryFinalizer
|
||||||
|
responseFinalizer BinaryFinalizer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBinaryConvRequestFull creates a new BinaryConvRequest with finalizer
|
||||||
|
// for response BinaryResponse.
|
||||||
|
func NewBinaryConvRequestFull(ptr BinaryPointer, finalizer BinaryFinalizer,
|
||||||
|
responseFinalizer BinaryFinalizer) *BinaryConvRequest {
|
||||||
|
b := &BinaryConvRequest{finalizer: finalizer, responseFinalizer: responseFinalizer}
|
||||||
|
b.ptr.Store(uintptr(ptr))
|
||||||
|
if ptr == nil || finalizer == nil {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ownership of the data here is temporary
|
||||||
|
runtime.SetFinalizer(b, func(b *BinaryConvRequest) { b.Release() })
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBinaryConvRequest creates a new BinaryConvRequest
|
||||||
|
func NewBinaryConvRequest(ptr BinaryPointer, finalizer BinaryFinalizer) *BinaryConvRequest {
|
||||||
|
return NewBinaryConvRequestFull(ptr, finalizer, finalizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBinaryConvRequestFromBytes creates a new BinaryConvRequest from an array
|
||||||
|
// of bytes.
|
||||||
|
func NewBinaryConvRequestFromBytes(bytes []byte) *BinaryConvRequest {
|
||||||
|
if bytes == nil {
|
||||||
|
return &BinaryConvRequest{}
|
||||||
|
}
|
||||||
|
return NewBinaryConvRequest(BinaryPointer(C.CBytes(bytes)),
|
||||||
|
func(ptr BinaryPointer) { C.free(unsafe.Pointer(ptr)) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style returns the response style for the request, so always BinaryPrompt.
|
||||||
|
func (b *BinaryConvRequest) Style() Style {
|
||||||
|
return BinaryPrompt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pointer returns the conversation style of the StringConvRequest.
|
||||||
|
func (b *BinaryConvRequest) Pointer() BinaryPointer {
|
||||||
|
ptr := b.ptr.Load()
|
||||||
|
return *(*BinaryPointer)(unsafe.Pointer(&ptr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateResponse creates a new BinaryConvResponse from the request
|
||||||
|
func (b *BinaryConvRequest) CreateResponse(ptr BinaryPointer) BinaryConvResponse {
|
||||||
|
bcr := &binaryConvResponse{ptr, b.responseFinalizer, &sync.Mutex{}}
|
||||||
|
runtime.SetFinalizer(bcr, func(bcr *binaryConvResponse) {
|
||||||
|
bcr.Release()
|
||||||
|
})
|
||||||
|
return bcr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release releases the resources allocated by the request
|
||||||
|
func (b *BinaryConvRequest) Release() {
|
||||||
|
ptr := b.ptr.Swap(0)
|
||||||
|
if b.finalizer != nil {
|
||||||
|
b.finalizer(*(*BinaryPointer)(unsafe.Pointer(&ptr)))
|
||||||
|
runtime.SetFinalizer(b, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinaryDecoder is a function type for decode the a binary pointer data into
|
||||||
|
// bytes
|
||||||
|
type BinaryDecoder func(BinaryPointer) ([]byte, error)
|
||||||
|
|
||||||
|
// BinaryConvResponse is a subtype of ConvResponse used for binary
|
||||||
|
// conversation responses.
|
||||||
|
type BinaryConvResponse interface {
|
||||||
|
ConvResponse
|
||||||
|
Data() BinaryPointer
|
||||||
|
Decode(BinaryDecoder) ([]byte, error)
|
||||||
|
Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
type binaryConvResponse struct {
|
||||||
|
ptr BinaryPointer
|
||||||
|
finalizer BinaryFinalizer
|
||||||
|
mutex *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style returns the response style for the response, so always BinaryPrompt.
|
||||||
|
func (b binaryConvResponse) Style() Style {
|
||||||
|
return BinaryPrompt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data returns the response native pointer, it's up to the protocol to parse
|
||||||
|
// it accordingly.
|
||||||
|
func (b *binaryConvResponse) Data() BinaryPointer {
|
||||||
|
b.mutex.Lock()
|
||||||
|
defer b.mutex.Unlock()
|
||||||
|
return b.ptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes the binary data using the provided decoder function.
|
||||||
|
func (b *binaryConvResponse) Decode(decoder BinaryDecoder) (
|
||||||
|
[]byte, error) {
|
||||||
|
if decoder == nil {
|
||||||
|
return nil, errors.New("nil decoder provided")
|
||||||
|
}
|
||||||
|
b.mutex.Lock()
|
||||||
|
defer b.mutex.Unlock()
|
||||||
|
return decoder(b.ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release releases the binary conversation response data.
|
||||||
|
// This is also automatically via a finalizer, but applications may control
|
||||||
|
// this explicitly deferring execution of this.
|
||||||
|
func (b *binaryConvResponse) Release() {
|
||||||
|
b.mutex.Lock()
|
||||||
|
defer b.mutex.Unlock()
|
||||||
|
ptr := b.ptr
|
||||||
|
b.ptr = nil
|
||||||
|
if b.finalizer != nil {
|
||||||
|
b.finalizer(ptr)
|
||||||
|
} else {
|
||||||
|
C.free(unsafe.Pointer(ptr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartStringConv starts a text-based conversation using the provided style
|
||||||
|
// and prompt.
|
||||||
|
func (m *moduleTransaction) StartStringConv(style Style, prompt string) (
|
||||||
|
StringConvResponse, error) {
|
||||||
|
return m.startStringConvImpl(m, style, prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *moduleTransaction) startStringConvImpl(iface moduleTransactionIface,
|
||||||
|
style Style, prompt string) (
|
||||||
|
StringConvResponse, error) {
|
||||||
|
switch style {
|
||||||
|
case BinaryPrompt:
|
||||||
|
return nil, fmt.Errorf("%w: binary style is not supported", ErrConv)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := m.startConvImpl(iface, NewStringConvRequest(style, prompt))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stringRes, _ := res.(stringConvResponse)
|
||||||
|
return stringRes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartStringConvf allows to start string conversation with formatting support.
|
||||||
|
func (m *moduleTransaction) StartStringConvf(style Style, format string, args ...interface{}) (
|
||||||
|
StringConvResponse, error) {
|
||||||
|
return m.StartStringConv(style, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasBinaryProtocol checks if binary protocol is supported.
|
||||||
|
func (m *moduleTransaction) hasBinaryProtocol() bool {
|
||||||
|
return CheckPamHasBinaryProtocol()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartBinaryConv starts a binary conversation using the provided bytes.
|
||||||
|
func (m *moduleTransaction) StartBinaryConv(bytes []byte) (
|
||||||
|
BinaryConvResponse, error) {
|
||||||
|
return m.startBinaryConvImpl(m, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *moduleTransaction) startBinaryConvImpl(iface moduleTransactionIface,
|
||||||
|
bytes []byte) (
|
||||||
|
BinaryConvResponse, error) {
|
||||||
|
res, err := m.startConvImpl(iface, NewBinaryConvRequestFromBytes(bytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
binaryRes, _ := res.(BinaryConvResponse)
|
||||||
|
return binaryRes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartConv initiates a PAM conversation using the provided ConvRequest.
|
||||||
|
func (m *moduleTransaction) StartConv(req ConvRequest) (
|
||||||
|
ConvResponse, error) {
|
||||||
|
return m.startConvImpl(m, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *moduleTransaction) startConvImpl(iface moduleTransactionIface, req ConvRequest) (
|
||||||
|
ConvResponse, error) {
|
||||||
|
resp, err := m.startConvMultiImpl(iface, []ConvRequest{req})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(resp) != 1 {
|
||||||
|
return nil, fmt.Errorf("%w: not enough values returned", ErrConv)
|
||||||
|
}
|
||||||
|
return resp[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *moduleTransaction) startConv(conv *C.struct_pam_conv, nMsg C.int,
|
||||||
|
messages **C.struct_pam_message, outResponses **C.struct_pam_response) C.int {
|
||||||
|
return C.start_pam_conv(conv, nMsg, messages, outResponses)
|
||||||
|
}
|
||||||
|
|
||||||
|
// startConvMultiImpl is the implementation for GetData for testing purposes.
|
||||||
|
func (m *moduleTransaction) startConvMultiImpl(iface moduleTransactionIface,
|
||||||
|
requests []ConvRequest) (responses []ConvResponse, err error) {
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
_ = m.handlePamStatus(success)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var pamErr Error
|
||||||
|
if !errors.As(err, &pamErr) {
|
||||||
|
err = errors.Join(ErrConv, err)
|
||||||
|
pamErr = ErrConv
|
||||||
|
}
|
||||||
|
_ = m.handlePamStatus(C.int(pamErr))
|
||||||
|
}()
|
||||||
|
|
||||||
|
if len(requests) == 0 {
|
||||||
|
return nil, errors.New("no requests defined")
|
||||||
|
}
|
||||||
|
if len(requests) > maxNumMsg {
|
||||||
|
return nil, errors.New("too many requests")
|
||||||
|
}
|
||||||
|
|
||||||
|
conv, err := iface.getConv()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if conv == nil || conv.conv == nil {
|
||||||
|
return nil, errors.New("impossible to find conv handler")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Just use make([]C.struct_pam_message, 0, len(requests))
|
||||||
|
// and append, when it's possible to use runtime.Pinner
|
||||||
|
var cMessagePtr *C.struct_pam_message
|
||||||
|
cMessages := (**C.struct_pam_message)(C.calloc(C.size_t(len(requests)),
|
||||||
|
(C.size_t)(unsafe.Sizeof(cMessagePtr))))
|
||||||
|
defer C.free(unsafe.Pointer(cMessages))
|
||||||
|
goMsgs := unsafe.Slice(cMessages, len(requests))
|
||||||
|
|
||||||
|
for i, req := range requests {
|
||||||
|
var cBytes unsafe.Pointer
|
||||||
|
switch r := req.(type) {
|
||||||
|
case StringConvRequest:
|
||||||
|
cBytes = unsafe.Pointer(C.CString(r.Prompt()))
|
||||||
|
defer C.free(cBytes)
|
||||||
|
case BinaryConvRequester:
|
||||||
|
if !iface.hasBinaryProtocol() {
|
||||||
|
return nil, errors.New("%w: binary protocol is not supported")
|
||||||
|
}
|
||||||
|
cBytes = unsafe.Pointer(r.Pointer())
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported conversation type %#v", r)
|
||||||
|
}
|
||||||
|
|
||||||
|
cMessage := (*C.struct_pam_message)(C.calloc(1,
|
||||||
|
(C.size_t)(unsafe.Sizeof(*goMsgs[i]))))
|
||||||
|
defer C.free(unsafe.Pointer(cMessage))
|
||||||
|
cMessage.msg_style = C.int(req.Style())
|
||||||
|
cMessage.msg = (*C.char)(cBytes)
|
||||||
|
goMsgs[i] = cMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.convMutex != nil {
|
||||||
|
m.convMutex.Lock()
|
||||||
|
defer m.convMutex.Unlock()
|
||||||
|
}
|
||||||
|
var cResponses *C.struct_pam_response
|
||||||
|
ret := iface.startConv(conv, C.int(len(requests)), cMessages, &cResponses)
|
||||||
|
if ret != success {
|
||||||
|
return nil, Error(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
goResponses := unsafe.Slice(cResponses, len(requests))
|
||||||
|
defer func() {
|
||||||
|
for i, resp := range goResponses {
|
||||||
|
if resp.resp == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch req := requests[i].(type) {
|
||||||
|
case BinaryConvRequester:
|
||||||
|
// In the binary prompt case, we need to rely on the provided
|
||||||
|
// finalizer to release the response, so let's create a new one.
|
||||||
|
req.CreateResponse(BinaryPointer(resp.resp)).Release()
|
||||||
|
default:
|
||||||
|
C.free(unsafe.Pointer(resp.resp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
C.free(unsafe.Pointer(cResponses))
|
||||||
|
}()
|
||||||
|
|
||||||
|
responses = make([]ConvResponse, 0, len(requests))
|
||||||
|
for i, resp := range goResponses {
|
||||||
|
request := requests[i]
|
||||||
|
msgStyle := request.Style()
|
||||||
|
switch msgStyle {
|
||||||
|
case PromptEchoOff:
|
||||||
|
fallthrough
|
||||||
|
case PromptEchoOn:
|
||||||
|
fallthrough
|
||||||
|
case ErrorMsg:
|
||||||
|
fallthrough
|
||||||
|
case TextInfo:
|
||||||
|
responses = append(responses, stringConvResponse{
|
||||||
|
style: msgStyle,
|
||||||
|
response: C.GoString(resp.resp),
|
||||||
|
})
|
||||||
|
case BinaryPrompt:
|
||||||
|
// Let's steal the resp ownership here, so that the request
|
||||||
|
// finalizer won't act on it.
|
||||||
|
bcr, _ := request.(BinaryConvRequester)
|
||||||
|
resp := bcr.CreateResponse(BinaryPointer(resp.resp))
|
||||||
|
goResponses[i].resp = nil
|
||||||
|
responses = append(responses, resp)
|
||||||
|
default:
|
||||||
|
return nil,
|
||||||
|
fmt.Errorf("unsupported conversation type %v", msgStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartConvMulti initiates a PAM conversation with multiple ConvRequest's.
|
||||||
|
func (m *moduleTransaction) StartConvMulti(requests []ConvRequest) (
|
||||||
|
[]ConvResponse, error) {
|
||||||
|
return m.startConvMultiImpl(m, requests)
|
||||||
|
}
|
||||||
1111
module-transaction_test.go
Normal file
1111
module-transaction_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
|||||||
#include "_cgo_export.h"
|
|
||||||
#include <security/pam_appl.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#ifdef __sun
|
|
||||||
#define PAM_CONST
|
|
||||||
#else
|
|
||||||
#define PAM_CONST const
|
|
||||||
#endif
|
|
||||||
|
|
||||||
int cb_pam_conv(
|
|
||||||
int num_msg,
|
|
||||||
PAM_CONST struct pam_message **msg,
|
|
||||||
struct pam_response **resp,
|
|
||||||
void *appdata_ptr)
|
|
||||||
{
|
|
||||||
*resp = calloc(num_msg, sizeof **resp);
|
|
||||||
if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) {
|
|
||||||
return PAM_CONV_ERR;
|
|
||||||
}
|
|
||||||
if (!*resp) {
|
|
||||||
return PAM_BUF_ERR;
|
|
||||||
}
|
|
||||||
for (size_t i = 0; i < num_msg; ++i) {
|
|
||||||
struct cbPAMConv_return result = cbPAMConv(
|
|
||||||
msg[i]->msg_style,
|
|
||||||
(char *)msg[i]->msg,
|
|
||||||
(uintptr_t)appdata_ptr);
|
|
||||||
if (result.r1 != PAM_SUCCESS) {
|
|
||||||
goto error;
|
|
||||||
}
|
|
||||||
(*resp)[i].resp = result.r0;
|
|
||||||
}
|
|
||||||
return PAM_SUCCESS;
|
|
||||||
error:
|
|
||||||
for (size_t i = 0; i < num_msg; ++i) {
|
|
||||||
if ((*resp)[i].resp) {
|
|
||||||
memset((*resp)[i].resp, 0, strlen((*resp)[i].resp));
|
|
||||||
free((*resp)[i].resp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
memset(*resp, 0, num_msg * sizeof *resp);
|
|
||||||
free(*resp);
|
|
||||||
*resp = NULL;
|
|
||||||
return PAM_CONV_ERR;
|
|
||||||
}
|
|
||||||
|
|
||||||
void init_pam_conv(struct pam_conv *conv, uintptr_t appdata)
|
|
||||||
{
|
|
||||||
conv->conv = cb_pam_conv;
|
|
||||||
conv->appdata_ptr = (void *)appdata;
|
|
||||||
}
|
|
||||||
|
|
||||||
// pam_start_confdir is a recent PAM api to declare a confdir (mostly for tests)
|
|
||||||
// weaken the linking dependency to detect if it’s present.
|
|
||||||
int pam_start_confdir(const char *service_name, const char *user, const struct pam_conv *pam_conversation, const char *confdir, pam_handle_t **pamh) __attribute__ ((weak));
|
|
||||||
int check_pam_start_confdir(void) {
|
|
||||||
if (pam_start_confdir == NULL)
|
|
||||||
return 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
319
transaction.go
319
transaction.go
@@ -1,32 +1,21 @@
|
|||||||
// Package pam provides a wrapper for the PAM application API.
|
// Package pam provides a wrapper for the PAM application API.
|
||||||
package pam
|
package pam
|
||||||
|
|
||||||
//#include <security/pam_appl.h>
|
|
||||||
//#include <stdlib.h>
|
|
||||||
//#include <stdint.h>
|
|
||||||
//#cgo CFLAGS: -Wall -std=c99
|
//#cgo CFLAGS: -Wall -std=c99
|
||||||
//#cgo LDFLAGS: -lpam
|
//#cgo LDFLAGS: -lpam
|
||||||
//void init_pam_conv(struct pam_conv *conv, uintptr_t);
|
|
||||||
//int pam_start_confdir(const char *service_name, const char *user, const struct pam_conv *pam_conversation, const char *confdir, pam_handle_t **pamh) __attribute__ ((weak));
|
|
||||||
//int check_pam_start_confdir(void);
|
|
||||||
//
|
//
|
||||||
//#ifdef PAM_BINARY_PROMPT
|
//#include "transaction.h"
|
||||||
//#define BINARY_PROMPT_IS_SUPPORTED 1
|
|
||||||
//#else
|
|
||||||
//#include <limits.h>
|
|
||||||
//#define PAM_BINARY_PROMPT INT_MAX
|
|
||||||
//#define BINARY_PROMPT_IS_SUPPORTED 0
|
|
||||||
//#endif
|
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"runtime"
|
|
||||||
"runtime/cgo"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// success indicates a successful function return.
|
||||||
|
const success = C.PAM_SUCCESS
|
||||||
|
|
||||||
// Style is the type of message that the conversation handler should display.
|
// Style is the type of message that the conversation handler should display.
|
||||||
type Style int
|
type Style int
|
||||||
|
|
||||||
@@ -37,155 +26,42 @@ const (
|
|||||||
PromptEchoOff Style = C.PAM_PROMPT_ECHO_OFF
|
PromptEchoOff Style = C.PAM_PROMPT_ECHO_OFF
|
||||||
// PromptEchoOn indicates the conversation handler should obtain a
|
// PromptEchoOn indicates the conversation handler should obtain a
|
||||||
// string while echoing text.
|
// string while echoing text.
|
||||||
PromptEchoOn = C.PAM_PROMPT_ECHO_ON
|
PromptEchoOn Style = C.PAM_PROMPT_ECHO_ON
|
||||||
// ErrorMsg indicates the conversation handler should display an
|
// ErrorMsg indicates the conversation handler should display an
|
||||||
// error message.
|
// error message.
|
||||||
ErrorMsg = C.PAM_ERROR_MSG
|
ErrorMsg Style = C.PAM_ERROR_MSG
|
||||||
// TextInfo indicates the conversation handler should display some
|
// TextInfo indicates the conversation handler should display some
|
||||||
// text.
|
// text.
|
||||||
TextInfo = C.PAM_TEXT_INFO
|
TextInfo Style = C.PAM_TEXT_INFO
|
||||||
|
// BinaryPrompt indicates the conversation handler that should implement
|
||||||
|
// the private binary protocol
|
||||||
|
BinaryPrompt Style = C.PAM_BINARY_PROMPT
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConversationHandler is an interface for objects that can be used as
|
|
||||||
// conversation callbacks during PAM authentication.
|
|
||||||
type ConversationHandler interface {
|
|
||||||
// RespondPAM receives a message style and a message string. If the
|
|
||||||
// message Style is PromptEchoOff or PromptEchoOn then the function
|
|
||||||
// should return a response string.
|
|
||||||
RespondPAM(Style, string) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BinaryPointer exposes the type used for the data in a binary conversation
|
// BinaryPointer exposes the type used for the data in a binary conversation
|
||||||
// it represents a pointer to data that is produced by the module and that
|
// it represents a pointer to data that is produced by the module and that
|
||||||
// must be parsed depending on the protocol in use
|
// must be parsed depending on the protocol in use
|
||||||
type BinaryPointer unsafe.Pointer
|
type BinaryPointer unsafe.Pointer
|
||||||
|
|
||||||
type BinaryConversationHandler interface {
|
// NativeHandle is the type of the native PAM handle for a transaction so that
|
||||||
ConversationHandler
|
// it can be exported
|
||||||
// Respond receives a pointer to the binary message. It's up to the
|
type NativeHandle = *C.pam_handle_t
|
||||||
// receiver to parse it according to the protocol specifications.
|
|
||||||
// The function can return a byte array that will passed as pointer back
|
// transactionBase is a handler for a PAM transaction that can be used to
|
||||||
// to the module.
|
// group the operations that can be performed both by the application and the
|
||||||
RespondPAMBinary(BinaryPointer) ([]byte, error)
|
// module side
|
||||||
|
type transactionBase struct {
|
||||||
|
handle NativeHandle
|
||||||
|
lastStatus atomic.Int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConversationFunc is an adapter to allow the use of ordinary functions as
|
// Allows to call pam functions managing return status
|
||||||
// conversation callbacks.
|
func (t *transactionBase) handlePamStatus(cStatus C.int) error {
|
||||||
type ConversationFunc func(Style, string) (string, error)
|
t.lastStatus.Store(int32(cStatus))
|
||||||
|
if status := Error(cStatus); status != success {
|
||||||
// RespondPAM is a conversation callback adapter.
|
return status
|
||||||
func (f ConversationFunc) RespondPAM(s Style, msg string) (string, error) {
|
|
||||||
return f(s, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cbPAMConv is a wrapper for the conversation callback function.
|
|
||||||
//export cbPAMConv
|
|
||||||
func cbPAMConv(s C.int, msg *C.char, c C.uintptr_t) (*C.char, C.int) {
|
|
||||||
var r string
|
|
||||||
var err error
|
|
||||||
v := cgo.Handle(c).Value()
|
|
||||||
switch cb := v.(type) {
|
|
||||||
case ConversationHandler:
|
|
||||||
if s == C.PAM_BINARY_PROMPT {
|
|
||||||
return nil, C.PAM_AUTHINFO_UNAVAIL
|
|
||||||
}
|
|
||||||
r, err = cb.RespondPAM(Style(s), C.GoString(msg))
|
|
||||||
case BinaryConversationHandler:
|
|
||||||
if s == C.PAM_BINARY_PROMPT {
|
|
||||||
bytes, err := cb.RespondPAMBinary(BinaryPointer(msg))
|
|
||||||
if err != nil {
|
|
||||||
return nil, C.PAM_CONV_ERR
|
|
||||||
}
|
|
||||||
return (*C.char)(C.CBytes(bytes)), C.PAM_SUCCESS
|
|
||||||
} else {
|
|
||||||
r, err = cb.RespondPAM(Style(s), C.GoString(msg))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
return nil
|
||||||
return nil, C.PAM_CONV_ERR
|
|
||||||
}
|
|
||||||
return C.CString(r), C.PAM_SUCCESS
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transaction is the application's handle for a PAM transaction.
|
|
||||||
type Transaction struct {
|
|
||||||
handle *C.pam_handle_t
|
|
||||||
conv *C.struct_pam_conv
|
|
||||||
status C.int
|
|
||||||
c cgo.Handle
|
|
||||||
}
|
|
||||||
|
|
||||||
// transactionFinalizer cleans up the PAM handle and deletes the callback
|
|
||||||
// function.
|
|
||||||
func transactionFinalizer(t *Transaction) {
|
|
||||||
C.pam_end(t.handle, t.status)
|
|
||||||
t.c.Delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start initiates a new PAM transaction. Service is treated identically to
|
|
||||||
// how pam_start treats it internally.
|
|
||||||
//
|
|
||||||
// All application calls to PAM begin with Start*. The returned
|
|
||||||
// transaction provides an interface to the remainder of the API.
|
|
||||||
func Start(service, user string, handler ConversationHandler) (*Transaction, error) {
|
|
||||||
return start(service, user, handler, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartFunc registers the handler func as a conversation handler.
|
|
||||||
func StartFunc(service, user string, handler func(Style, string) (string, error)) (*Transaction, error) {
|
|
||||||
return Start(service, user, ConversationFunc(handler))
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartConfDir initiates a new PAM transaction. Service is treated identically to
|
|
||||||
// how pam_start treats it internally.
|
|
||||||
// confdir allows to define where all pam services are defined. This is used to provide
|
|
||||||
// custom paths for tests.
|
|
||||||
//
|
|
||||||
// All application calls to PAM begin with Start*. The returned
|
|
||||||
// transaction provides an interface to the remainder of the API.
|
|
||||||
func StartConfDir(service, user string, handler ConversationHandler, confDir string) (*Transaction, error) {
|
|
||||||
if !CheckPamHasStartConfdir() {
|
|
||||||
return nil, errors.New("StartConfDir() was used, but the pam version on the system is not recent enough")
|
|
||||||
}
|
|
||||||
|
|
||||||
return start(service, user, handler, confDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func start(service, user string, handler ConversationHandler, confDir string) (*Transaction, error) {
|
|
||||||
switch handler.(type) {
|
|
||||||
case BinaryConversationHandler:
|
|
||||||
if C.BINARY_PROMPT_IS_SUPPORTED == 0 {
|
|
||||||
return nil, errors.New("BinaryConversationHandler() was used, but it is not supported by this platform")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t := &Transaction{
|
|
||||||
conv: &C.struct_pam_conv{},
|
|
||||||
c: cgo.NewHandle(handler),
|
|
||||||
}
|
|
||||||
C.init_pam_conv(t.conv, C.uintptr_t(t.c))
|
|
||||||
runtime.SetFinalizer(t, transactionFinalizer)
|
|
||||||
s := C.CString(service)
|
|
||||||
defer C.free(unsafe.Pointer(s))
|
|
||||||
var u *C.char
|
|
||||||
if len(user) != 0 {
|
|
||||||
u = C.CString(user)
|
|
||||||
defer C.free(unsafe.Pointer(u))
|
|
||||||
}
|
|
||||||
if confDir == "" {
|
|
||||||
t.status = C.pam_start(s, u, t.conv, &t.handle)
|
|
||||||
} else {
|
|
||||||
c := C.CString(confDir)
|
|
||||||
defer C.free(unsafe.Pointer(c))
|
|
||||||
t.status = C.pam_start_confdir(s, u, t.conv, c, &t.handle)
|
|
||||||
}
|
|
||||||
if t.status != C.PAM_SUCCESS {
|
|
||||||
return nil, t
|
|
||||||
}
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transaction) Error() string {
|
|
||||||
return C.GoString(C.pam_strerror(t.handle, C.int(t.status)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Item is a an PAM information type.
|
// Item is a an PAM information type.
|
||||||
@@ -196,38 +72,42 @@ const (
|
|||||||
// Service is the name which identifies the PAM stack.
|
// Service is the name which identifies the PAM stack.
|
||||||
Service Item = C.PAM_SERVICE
|
Service Item = C.PAM_SERVICE
|
||||||
// User identifies the username identity used by a service.
|
// User identifies the username identity used by a service.
|
||||||
User = C.PAM_USER
|
User Item = C.PAM_USER
|
||||||
// Tty is the terminal name.
|
// Tty is the terminal name.
|
||||||
Tty = C.PAM_TTY
|
Tty Item = C.PAM_TTY
|
||||||
// Rhost is the requesting host name.
|
// Rhost is the requesting host name.
|
||||||
Rhost = C.PAM_RHOST
|
Rhost Item = C.PAM_RHOST
|
||||||
// Authtok is the currently active authentication token.
|
// Authtok is the currently active authentication token.
|
||||||
Authtok = C.PAM_AUTHTOK
|
Authtok Item = C.PAM_AUTHTOK
|
||||||
// Oldauthtok is the old authentication token.
|
// Oldauthtok is the old authentication token.
|
||||||
Oldauthtok = C.PAM_OLDAUTHTOK
|
Oldauthtok Item = C.PAM_OLDAUTHTOK
|
||||||
// Ruser is the requesting user name.
|
// Ruser is the requesting user name.
|
||||||
Ruser = C.PAM_RUSER
|
Ruser Item = C.PAM_RUSER
|
||||||
// UserPrompt is the string use to prompt for a username.
|
// UserPrompt is the string use to prompt for a username.
|
||||||
UserPrompt = C.PAM_USER_PROMPT
|
UserPrompt Item = C.PAM_USER_PROMPT
|
||||||
|
// FailDelay is the app supplied function to override failure delays.
|
||||||
|
FailDelay Item = C.PAM_FAIL_DELAY
|
||||||
|
// Xdisplay is the X display name
|
||||||
|
Xdisplay Item = C.PAM_XDISPLAY
|
||||||
|
// Xauthdata is the X server authentication data.
|
||||||
|
Xauthdata Item = C.PAM_XAUTHDATA
|
||||||
|
// AuthtokType is the type for pam_get_authtok
|
||||||
|
AuthtokType Item = C.PAM_AUTHTOK_TYPE
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetItem sets a PAM information item.
|
// SetItem sets a PAM information item.
|
||||||
func (t *Transaction) SetItem(i Item, item string) error {
|
func (t *transactionBase) SetItem(i Item, item string) error {
|
||||||
cs := unsafe.Pointer(C.CString(item))
|
cs := unsafe.Pointer(C.CString(item))
|
||||||
defer C.free(cs)
|
defer C.free(cs)
|
||||||
t.status = C.pam_set_item(t.handle, C.int(i), cs)
|
return t.handlePamStatus(C.pam_set_item(t.handle, C.int(i), cs))
|
||||||
if t.status != C.PAM_SUCCESS {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetItem retrieves a PAM information item.
|
// GetItem retrieves a PAM information item.
|
||||||
func (t *Transaction) GetItem(i Item) (string, error) {
|
func (t *transactionBase) GetItem(i Item) (string, error) {
|
||||||
var s unsafe.Pointer
|
var s unsafe.Pointer
|
||||||
t.status = C.pam_get_item(t.handle, C.int(i), &s)
|
err := t.handlePamStatus(C.pam_get_item(t.handle, C.int(i), &s))
|
||||||
if t.status != C.PAM_SUCCESS {
|
if err != nil {
|
||||||
return "", t
|
return "", err
|
||||||
}
|
}
|
||||||
return C.GoString((*C.char)(s)), nil
|
return C.GoString((*C.char)(s)), nil
|
||||||
}
|
}
|
||||||
@@ -243,107 +123,36 @@ const (
|
|||||||
Silent Flags = C.PAM_SILENT
|
Silent Flags = C.PAM_SILENT
|
||||||
// DisallowNullAuthtok indicates that authorization should fail
|
// DisallowNullAuthtok indicates that authorization should fail
|
||||||
// if the user does not have a registered authentication token.
|
// if the user does not have a registered authentication token.
|
||||||
DisallowNullAuthtok = C.PAM_DISALLOW_NULL_AUTHTOK
|
DisallowNullAuthtok Flags = C.PAM_DISALLOW_NULL_AUTHTOK
|
||||||
// EstablishCred indicates that credentials should be established
|
// EstablishCred indicates that credentials should be established
|
||||||
// for the user.
|
// for the user.
|
||||||
EstablishCred = C.PAM_ESTABLISH_CRED
|
EstablishCred Flags = C.PAM_ESTABLISH_CRED
|
||||||
// DeleteCred inidicates that credentials should be deleted.
|
// DeleteCred indicates that credentials should be deleted.
|
||||||
DeleteCred = C.PAM_DELETE_CRED
|
DeleteCred Flags = C.PAM_DELETE_CRED
|
||||||
// ReinitializeCred indicates that credentials should be fully
|
// ReinitializeCred indicates that credentials should be fully
|
||||||
// reinitialized.
|
// reinitialized.
|
||||||
ReinitializeCred = C.PAM_REINITIALIZE_CRED
|
ReinitializeCred Flags = C.PAM_REINITIALIZE_CRED
|
||||||
// RefreshCred indicates that the lifetime of existing credentials
|
// RefreshCred indicates that the lifetime of existing credentials
|
||||||
// should be extended.
|
// should be extended.
|
||||||
RefreshCred = C.PAM_REFRESH_CRED
|
RefreshCred Flags = C.PAM_REFRESH_CRED
|
||||||
// ChangeExpiredAuthtok indicates that the authentication token
|
// ChangeExpiredAuthtok indicates that the authentication token
|
||||||
// should be changed if it has expired.
|
// should be changed if it has expired.
|
||||||
ChangeExpiredAuthtok = C.PAM_CHANGE_EXPIRED_AUTHTOK
|
ChangeExpiredAuthtok Flags = C.PAM_CHANGE_EXPIRED_AUTHTOK
|
||||||
)
|
)
|
||||||
|
|
||||||
// Authenticate is used to authenticate the user.
|
|
||||||
//
|
|
||||||
// Valid flags: Silent, DisallowNullAuthtok
|
|
||||||
func (t *Transaction) Authenticate(f Flags) error {
|
|
||||||
t.status = C.pam_authenticate(t.handle, C.int(f))
|
|
||||||
if t.status != C.PAM_SUCCESS {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCred is used to establish, maintain and delete the credentials of a
|
|
||||||
// user.
|
|
||||||
//
|
|
||||||
// Valid flags: EstablishCred, DeleteCred, ReinitializeCred, RefreshCred
|
|
||||||
func (t *Transaction) SetCred(f Flags) error {
|
|
||||||
t.status = C.pam_setcred(t.handle, C.int(f))
|
|
||||||
if t.status != C.PAM_SUCCESS {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AcctMgmt is used to determine if the user's account is valid.
|
|
||||||
//
|
|
||||||
// Valid flags: Silent, DisallowNullAuthtok
|
|
||||||
func (t *Transaction) AcctMgmt(f Flags) error {
|
|
||||||
t.status = C.pam_acct_mgmt(t.handle, C.int(f))
|
|
||||||
if t.status != C.PAM_SUCCESS {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChangeAuthTok is used to change the authentication token.
|
|
||||||
//
|
|
||||||
// Valid flags: Silent, ChangeExpiredAuthtok
|
|
||||||
func (t *Transaction) ChangeAuthTok(f Flags) error {
|
|
||||||
t.status = C.pam_chauthtok(t.handle, C.int(f))
|
|
||||||
if t.status != C.PAM_SUCCESS {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenSession sets up a user session for an authenticated user.
|
|
||||||
//
|
|
||||||
// Valid flags: Slient
|
|
||||||
func (t *Transaction) OpenSession(f Flags) error {
|
|
||||||
t.status = C.pam_open_session(t.handle, C.int(f))
|
|
||||||
if t.status != C.PAM_SUCCESS {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseSession closes a previously opened session.
|
|
||||||
//
|
|
||||||
// Valid flags: Silent
|
|
||||||
func (t *Transaction) CloseSession(f Flags) error {
|
|
||||||
t.status = C.pam_close_session(t.handle, C.int(f))
|
|
||||||
if t.status != C.PAM_SUCCESS {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutEnv adds or changes the value of PAM environment variables.
|
// PutEnv adds or changes the value of PAM environment variables.
|
||||||
//
|
//
|
||||||
// NAME=value will set a variable to a value.
|
// NAME=value will set a variable to a value.
|
||||||
// NAME= will set a variable to an empty value.
|
// NAME= will set a variable to an empty value.
|
||||||
// NAME (without an "=") will delete a variable.
|
// NAME (without an "=") will delete a variable.
|
||||||
func (t *Transaction) PutEnv(nameval string) error {
|
func (t *transactionBase) PutEnv(nameval string) error {
|
||||||
cs := C.CString(nameval)
|
cs := C.CString(nameval)
|
||||||
defer C.free(unsafe.Pointer(cs))
|
defer C.free(unsafe.Pointer(cs))
|
||||||
t.status = C.pam_putenv(t.handle, cs)
|
return t.handlePamStatus(C.pam_putenv(t.handle, cs))
|
||||||
if t.status != C.PAM_SUCCESS {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEnv is used to retrieve a PAM environment variable.
|
// GetEnv is used to retrieve a PAM environment variable.
|
||||||
func (t *Transaction) GetEnv(name string) string {
|
func (t *transactionBase) GetEnv(name string) string {
|
||||||
cs := C.CString(name)
|
cs := C.CString(name)
|
||||||
defer C.free(unsafe.Pointer(cs))
|
defer C.free(unsafe.Pointer(cs))
|
||||||
value := C.pam_getenv(t.handle, cs)
|
value := C.pam_getenv(t.handle, cs)
|
||||||
@@ -358,13 +167,14 @@ func next(p **C.char) **C.char {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetEnvList returns a copy of the PAM environment as a map.
|
// GetEnvList returns a copy of the PAM environment as a map.
|
||||||
func (t *Transaction) GetEnvList() (map[string]string, error) {
|
func (t *transactionBase) GetEnvList() (map[string]string, error) {
|
||||||
env := make(map[string]string)
|
env := make(map[string]string)
|
||||||
p := C.pam_getenvlist(t.handle)
|
p := C.pam_getenvlist(t.handle)
|
||||||
if p == nil {
|
if p == nil {
|
||||||
t.status = C.PAM_BUF_ERR
|
t.lastStatus.Store(int32(ErrBuf))
|
||||||
return nil, t
|
return nil, ErrBuf
|
||||||
}
|
}
|
||||||
|
t.lastStatus.Store(success)
|
||||||
for q := p; *q != nil; q = next(q) {
|
for q := p; *q != nil; q = next(q) {
|
||||||
chunks := strings.SplitN(C.GoString(*q), "=", 2)
|
chunks := strings.SplitN(C.GoString(*q), "=", 2)
|
||||||
if len(chunks) == 2 {
|
if len(chunks) == 2 {
|
||||||
@@ -380,3 +190,8 @@ func (t *Transaction) GetEnvList() (map[string]string, error) {
|
|||||||
func CheckPamHasStartConfdir() bool {
|
func CheckPamHasStartConfdir() bool {
|
||||||
return C.check_pam_start_confdir() == 0
|
return C.check_pam_start_confdir() == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckPamHasBinaryProtocol return if pam on system supports PAM_BINARY_PROMPT
|
||||||
|
func CheckPamHasBinaryProtocol() bool {
|
||||||
|
return C.BINARY_PROMPT_IS_SUPPORTED != 0
|
||||||
|
}
|
||||||
|
|||||||
99
transaction.h
Normal file
99
transaction.h
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <security/pam_appl.h>
|
||||||
|
#include <security/pam_modules.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifdef PAM_BINARY_PROMPT
|
||||||
|
#define BINARY_PROMPT_IS_SUPPORTED 1
|
||||||
|
#else
|
||||||
|
#include <limits.h>
|
||||||
|
#define PAM_BINARY_PROMPT INT_MAX
|
||||||
|
#define BINARY_PROMPT_IS_SUPPORTED 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __sun
|
||||||
|
#define PAM_CONST
|
||||||
|
#else
|
||||||
|
#define PAM_CONST const
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern int _go_pam_conv_handler(struct pam_message *, uintptr_t, char **reply);
|
||||||
|
extern void _go_pam_data_cleanup(pam_handle_t *, uintptr_t, int status);
|
||||||
|
|
||||||
|
static inline int cb_pam_conv(int num_msg, PAM_CONST struct pam_message **msg, struct pam_response **resp, void *appdata_ptr)
|
||||||
|
{
|
||||||
|
if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG)
|
||||||
|
return PAM_CONV_ERR;
|
||||||
|
|
||||||
|
*resp = calloc(num_msg, sizeof **resp);
|
||||||
|
if (!*resp)
|
||||||
|
return PAM_BUF_ERR;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < num_msg; ++i) {
|
||||||
|
int result = _go_pam_conv_handler((struct pam_message *)msg[i], (uintptr_t)appdata_ptr, &(*resp)[i].resp);
|
||||||
|
if (result != PAM_SUCCESS)
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PAM_SUCCESS;
|
||||||
|
error:
|
||||||
|
for (size_t i = 0; i < num_msg; ++i) {
|
||||||
|
if ((*resp)[i].resp) {
|
||||||
|
#ifdef PAM_BINARY_PROMPT
|
||||||
|
if (msg[i]->msg_style != PAM_BINARY_PROMPT)
|
||||||
|
#endif
|
||||||
|
memset((*resp)[i].resp, 0, strlen((*resp)[i].resp));
|
||||||
|
free((*resp)[i].resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(*resp, 0, num_msg * sizeof *resp);
|
||||||
|
free(*resp);
|
||||||
|
*resp = NULL;
|
||||||
|
return PAM_CONV_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void init_pam_conv(struct pam_conv *conv, uintptr_t appdata)
|
||||||
|
{
|
||||||
|
conv->conv = cb_pam_conv;
|
||||||
|
conv->appdata_ptr = (void *)appdata;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int start_pam_conv(struct pam_conv *pc, int num_msgs, const struct pam_message **msgs, struct pam_response **out_resp)
|
||||||
|
{
|
||||||
|
return pc->conv(num_msgs, msgs, out_resp, pc->appdata_ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pam_start_confdir is a recent PAM api to declare a confdir (mostly for
|
||||||
|
// tests) weaken the linking dependency to detect if it’s present.
|
||||||
|
int pam_start_confdir(const char *service_name, const char *user, const struct pam_conv *pam_conversation,
|
||||||
|
const char *confdir, pam_handle_t **pamh) __attribute__((weak));
|
||||||
|
|
||||||
|
static inline int check_pam_start_confdir(void)
|
||||||
|
{
|
||||||
|
if (pam_start_confdir == NULL)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void data_cleanup(pam_handle_t *pamh, void *data, int error_status)
|
||||||
|
{
|
||||||
|
_go_pam_data_cleanup(pamh, (uintptr_t)data, error_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int set_data(pam_handle_t *pamh, const char *name, uintptr_t handle)
|
||||||
|
{
|
||||||
|
if (handle)
|
||||||
|
return pam_set_data(pamh, name, (void *)handle, data_cleanup);
|
||||||
|
|
||||||
|
return pam_set_data(pamh, name, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int get_data(pam_handle_t *pamh, const char *name, uintptr_t *out_handle)
|
||||||
|
{
|
||||||
|
return pam_get_data(pamh, name, (const void **)out_handle);
|
||||||
|
}
|
||||||
@@ -2,11 +2,44 @@ package pam
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func maybeEndTransaction(t *testing.T, tx *Transaction) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if tx == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := tx.End()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("end #error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureTransactionEnds(t *testing.T, tx *Transaction) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
runtime.SetFinalizer(tx, func(tx *Transaction) {
|
||||||
|
// #nosec:G103 - the pointer conversion is checked.
|
||||||
|
handle := atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&tx.handle)))
|
||||||
|
if handle == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("transaction has not been finalized")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestPAM_001(t *testing.T) {
|
func TestPAM_001(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
u, _ := user.Current()
|
u, _ := user.Current()
|
||||||
if u.Uid != "0" {
|
if u.Uid != "0" {
|
||||||
t.Skip("run this test as root")
|
t.Skip("run this test as root")
|
||||||
@@ -15,6 +48,8 @@ func TestPAM_001(t *testing.T) {
|
|||||||
tx, err := StartFunc("", "test", func(s Style, msg string) (string, error) {
|
tx, err := StartFunc("", "test", func(s Style, msg string) (string, error) {
|
||||||
return p, nil
|
return p, nil
|
||||||
})
|
})
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("start #error: %v", err)
|
t.Fatalf("start #error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -33,6 +68,7 @@ func TestPAM_001(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPAM_002(t *testing.T) {
|
func TestPAM_002(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
u, _ := user.Current()
|
u, _ := user.Current()
|
||||||
if u.Uid != "0" {
|
if u.Uid != "0" {
|
||||||
t.Skip("run this test as root")
|
t.Skip("run this test as root")
|
||||||
@@ -46,6 +82,8 @@ func TestPAM_002(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return "", errors.New("unexpected")
|
return "", errors.New("unexpected")
|
||||||
})
|
})
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("start #error: %v", err)
|
t.Fatalf("start #error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -71,6 +109,7 @@ func (c Credentials) RespondPAM(s Style, msg string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPAM_003(t *testing.T) {
|
func TestPAM_003(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
u, _ := user.Current()
|
u, _ := user.Current()
|
||||||
if u.Uid != "0" {
|
if u.Uid != "0" {
|
||||||
t.Skip("run this test as root")
|
t.Skip("run this test as root")
|
||||||
@@ -80,6 +119,8 @@ func TestPAM_003(t *testing.T) {
|
|||||||
Password: "secret",
|
Password: "secret",
|
||||||
}
|
}
|
||||||
tx, err := Start("", "", c)
|
tx, err := Start("", "", c)
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("start #error: %v", err)
|
t.Fatalf("start #error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -90,6 +131,7 @@ func TestPAM_003(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPAM_004(t *testing.T) {
|
func TestPAM_004(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
u, _ := user.Current()
|
u, _ := user.Current()
|
||||||
if u.Uid != "0" {
|
if u.Uid != "0" {
|
||||||
t.Skip("run this test as root")
|
t.Skip("run this test as root")
|
||||||
@@ -98,6 +140,8 @@ func TestPAM_004(t *testing.T) {
|
|||||||
Password: "secret",
|
Password: "secret",
|
||||||
}
|
}
|
||||||
tx, err := Start("", "test", c)
|
tx, err := Start("", "test", c)
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("start #error: %v", err)
|
t.Fatalf("start #error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -108,16 +152,29 @@ func TestPAM_004(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPAM_005(t *testing.T) {
|
func TestPAM_005(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
u, _ := user.Current()
|
u, _ := user.Current()
|
||||||
if u.Uid != "0" {
|
if u.Uid != "0" {
|
||||||
t.Skip("run this test as root")
|
t.Skip("run this test as root")
|
||||||
}
|
}
|
||||||
|
if _, found := os.LookupEnv("GO_PAM_TEST_WITH_ASAN"); found {
|
||||||
|
t.Skip("test fails under ASAN")
|
||||||
|
}
|
||||||
tx, err := StartFunc("passwd", "test", func(s Style, msg string) (string, error) {
|
tx, err := StartFunc("passwd", "test", func(s Style, msg string) (string, error) {
|
||||||
return "secret", nil
|
return "secret", nil
|
||||||
})
|
})
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("start #error: %v", err)
|
t.Fatalf("start #error: %v", err)
|
||||||
}
|
}
|
||||||
|
service, err := tx.GetItem(Service)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetItem #error: %v", err)
|
||||||
|
}
|
||||||
|
if service != "passwd" {
|
||||||
|
t.Fatalf("Unexpected service: %v", service)
|
||||||
|
}
|
||||||
err = tx.ChangeAuthTok(Silent)
|
err = tx.ChangeAuthTok(Silent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("chauthtok #error: %v", err)
|
t.Fatalf("chauthtok #error: %v", err)
|
||||||
@@ -125,6 +182,7 @@ func TestPAM_005(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPAM_006(t *testing.T) {
|
func TestPAM_006(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
u, _ := user.Current()
|
u, _ := user.Current()
|
||||||
if u.Uid != "0" {
|
if u.Uid != "0" {
|
||||||
t.Skip("run this test as root")
|
t.Skip("run this test as root")
|
||||||
@@ -132,6 +190,8 @@ func TestPAM_006(t *testing.T) {
|
|||||||
tx, err := StartFunc("passwd", u.Username, func(s Style, msg string) (string, error) {
|
tx, err := StartFunc("passwd", u.Username, func(s Style, msg string) (string, error) {
|
||||||
return "secret", nil
|
return "secret", nil
|
||||||
})
|
})
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("start #error: %v", err)
|
t.Fatalf("start #error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -146,6 +206,7 @@ func TestPAM_006(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPAM_007(t *testing.T) {
|
func TestPAM_007(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
u, _ := user.Current()
|
u, _ := user.Current()
|
||||||
if u.Uid != "0" {
|
if u.Uid != "0" {
|
||||||
t.Skip("run this test as root")
|
t.Skip("run this test as root")
|
||||||
@@ -153,6 +214,8 @@ func TestPAM_007(t *testing.T) {
|
|||||||
tx, err := StartFunc("", "test", func(s Style, msg string) (string, error) {
|
tx, err := StartFunc("", "test", func(s Style, msg string) (string, error) {
|
||||||
return "", errors.New("Sorry, it didn't work")
|
return "", errors.New("Sorry, it didn't work")
|
||||||
})
|
})
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("start #error: %v", err)
|
t.Fatalf("start #error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -164,15 +227,24 @@ func TestPAM_007(t *testing.T) {
|
|||||||
if len(s) == 0 {
|
if len(s) == 0 {
|
||||||
t.Fatalf("error #expected an error message")
|
t.Fatalf("error #expected an error message")
|
||||||
}
|
}
|
||||||
|
if !errors.Is(err, ErrAuth) {
|
||||||
|
t.Fatalf("error #unexpected error %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPAM_ConfDir(t *testing.T) {
|
func TestPAM_ConfDir(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
u, _ := user.Current()
|
u, _ := user.Current()
|
||||||
c := Credentials{
|
c := Credentials{
|
||||||
// the custom service always permits even with wrong password.
|
// the custom service always permits even with wrong password.
|
||||||
Password: "wrongsecret",
|
Password: "wrongsecret",
|
||||||
}
|
}
|
||||||
tx, err := StartConfDir("permit-service", u.Username, c, "test-services")
|
tx, err := StartConfDir("permit-service", u.Username, c, "test-services")
|
||||||
|
defer func() {
|
||||||
|
if tx != nil {
|
||||||
|
_ = tx.End()
|
||||||
|
}
|
||||||
|
}()
|
||||||
if !CheckPamHasStartConfdir() {
|
if !CheckPamHasStartConfdir() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err)
|
t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err)
|
||||||
@@ -180,6 +252,13 @@ func TestPAM_ConfDir(t *testing.T) {
|
|||||||
// nothing else we do, we don't support it.
|
// nothing else we do, we don't support it.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
service, err := tx.GetItem(Service)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetItem #error: %v", err)
|
||||||
|
}
|
||||||
|
if service != "permit-service" {
|
||||||
|
t.Fatalf("Unexpected service: %v", service)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("start #error: %v", err)
|
t.Fatalf("start #error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -190,21 +269,36 @@ func TestPAM_ConfDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPAM_ConfDir_FailNoServiceOrUnsupported(t *testing.T) {
|
func TestPAM_ConfDir_FailNoServiceOrUnsupported(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
|
if !CheckPamHasStartConfdir() {
|
||||||
|
t.Skip("this requires PAM with Conf dir support")
|
||||||
|
}
|
||||||
u, _ := user.Current()
|
u, _ := user.Current()
|
||||||
c := Credentials{
|
c := Credentials{
|
||||||
Password: "secret",
|
Password: "secret",
|
||||||
}
|
}
|
||||||
_, err := StartConfDir("does-not-exists", u.Username, c, ".")
|
tx, err := StartConfDir("does-not-exists", u.Username, c, ".")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("authenticate #expected an error")
|
t.Fatalf("authenticate #expected an error")
|
||||||
}
|
}
|
||||||
|
if tx != nil {
|
||||||
|
t.Fatalf("authenticate #unexpected transaction")
|
||||||
|
}
|
||||||
s := err.Error()
|
s := err.Error()
|
||||||
if len(s) == 0 {
|
if len(s) == 0 {
|
||||||
t.Fatalf("error #expected an error message")
|
t.Fatalf("error #expected an error message")
|
||||||
}
|
}
|
||||||
|
var pamErr Error
|
||||||
|
if !errors.As(err, &pamErr) {
|
||||||
|
t.Fatalf("error #unexpected type: %#v", err)
|
||||||
|
}
|
||||||
|
if pamErr != ErrAbort {
|
||||||
|
t.Fatalf("error #unexpected status: %v", pamErr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPAM_ConfDir_InfoMessage(t *testing.T) {
|
func TestPAM_ConfDir_InfoMessage(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
u, _ := user.Current()
|
u, _ := user.Current()
|
||||||
var infoText string
|
var infoText string
|
||||||
tx, err := StartConfDir("echo-service", u.Username,
|
tx, err := StartConfDir("echo-service", u.Username,
|
||||||
@@ -216,24 +310,46 @@ func TestPAM_ConfDir_InfoMessage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return "", errors.New("unexpected")
|
return "", errors.New("unexpected")
|
||||||
}), "test-services")
|
}), "test-services")
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("start #error: %v", err)
|
t.Fatalf("start #error: %v", err)
|
||||||
}
|
}
|
||||||
|
service, err := tx.GetItem(Service)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetItem #error: %v", err)
|
||||||
|
}
|
||||||
|
if service != "echo-service" {
|
||||||
|
t.Fatalf("Unexpected service: %v", service)
|
||||||
|
}
|
||||||
err = tx.Authenticate(0)
|
err = tx.Authenticate(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("authenticate #error: %v", err)
|
t.Fatalf("authenticate #error: %v", err)
|
||||||
}
|
}
|
||||||
if infoText != "This is an info message for user " + u.Username + " on echo-service" {
|
if infoText != "This is an info message for user "+u.Username+" on echo-service" {
|
||||||
t.Fatalf("Unexpected info message: %v", infoText)
|
t.Fatalf("Unexpected info message: %v", infoText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPAM_ConfDir_Deny(t *testing.T) {
|
func TestPAM_ConfDir_Deny(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
|
if !CheckPamHasStartConfdir() {
|
||||||
|
t.Skip("this requires PAM with Conf dir support")
|
||||||
|
}
|
||||||
u, _ := user.Current()
|
u, _ := user.Current()
|
||||||
tx, err := StartConfDir("deny-service", u.Username, Credentials{}, "test-services")
|
tx, err := StartConfDir("deny-service", u.Username, Credentials{}, "test-services")
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("start #error: %v", err)
|
t.Fatalf("start #error: %v", err)
|
||||||
}
|
}
|
||||||
|
service, err := tx.GetItem(Service)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetItem #error: %v", err)
|
||||||
|
}
|
||||||
|
if service != "deny-service" {
|
||||||
|
t.Fatalf("Unexpected service: %v", service)
|
||||||
|
}
|
||||||
err = tx.Authenticate(0)
|
err = tx.Authenticate(0)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("authenticate #expected an error")
|
t.Fatalf("authenticate #expected an error")
|
||||||
@@ -242,15 +358,21 @@ func TestPAM_ConfDir_Deny(t *testing.T) {
|
|||||||
if len(s) == 0 {
|
if len(s) == 0 {
|
||||||
t.Fatalf("error #expected an error message")
|
t.Fatalf("error #expected an error message")
|
||||||
}
|
}
|
||||||
|
if !errors.Is(err, ErrAuth) {
|
||||||
|
t.Fatalf("error #unexpected error %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPAM_ConfDir_PromptForUserName(t *testing.T) {
|
func TestPAM_ConfDir_PromptForUserName(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
c := Credentials{
|
c := Credentials{
|
||||||
User: "testuser",
|
User: "testuser",
|
||||||
// the custom service only cares about correct user name.
|
// the custom service only cares about correct user name.
|
||||||
Password: "wrongsecret",
|
Password: "wrongsecret",
|
||||||
}
|
}
|
||||||
tx, err := StartConfDir("succeed-if-user-test", "", c, "test-services")
|
tx, err := StartConfDir("succeed-if-user-test", "", c, "test-services")
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
if !CheckPamHasStartConfdir() {
|
if !CheckPamHasStartConfdir() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err)
|
t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err)
|
||||||
@@ -268,11 +390,14 @@ func TestPAM_ConfDir_PromptForUserName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPAM_ConfDir_WrongUserName(t *testing.T) {
|
func TestPAM_ConfDir_WrongUserName(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
c := Credentials{
|
c := Credentials{
|
||||||
User: "wronguser",
|
User: "wronguser",
|
||||||
Password: "wrongsecret",
|
Password: "wrongsecret",
|
||||||
}
|
}
|
||||||
tx, err := StartConfDir("succeed-if-user-test", "", c, "test-services")
|
tx, err := StartConfDir("succeed-if-user-test", "", c, "test-services")
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
if !CheckPamHasStartConfdir() {
|
if !CheckPamHasStartConfdir() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err)
|
t.Fatalf("start should have errored out as pam_start_confdir is not available: %v", err)
|
||||||
@@ -288,12 +413,21 @@ func TestPAM_ConfDir_WrongUserName(t *testing.T) {
|
|||||||
if len(s) == 0 {
|
if len(s) == 0 {
|
||||||
t.Fatalf("error #expected an error message")
|
t.Fatalf("error #expected an error message")
|
||||||
}
|
}
|
||||||
|
if !errors.Is(err, ErrAuth) {
|
||||||
|
t.Fatalf("error #unexpected error %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestItem(t *testing.T) {
|
func TestItem(t *testing.T) {
|
||||||
tx, _ := StartFunc("passwd", "test", func(s Style, msg string) (string, error) {
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
|
tx, err := StartFunc("passwd", "test", func(s Style, msg string) (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
})
|
})
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("start #error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
s, err := tx.GetItem(Service)
|
s, err := tx.GetItem(Service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -325,9 +459,12 @@ func TestItem(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestEnv(t *testing.T) {
|
func TestEnv(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
tx, err := StartFunc("", "", func(s Style, msg string) (string, error) {
|
tx, err := StartFunc("", "", func(s Style, msg string) (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
})
|
})
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("start #error: %v", err)
|
t.Fatalf("start #error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -390,7 +527,143 @@ func TestEnv(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_Error(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
|
if !CheckPamHasStartConfdir() {
|
||||||
|
t.Skip("this requires PAM with Conf dir support")
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses := map[string]error{
|
||||||
|
"success": nil,
|
||||||
|
"open_err": ErrOpen,
|
||||||
|
"symbol_err": ErrSymbol,
|
||||||
|
"service_err": ErrService,
|
||||||
|
"system_err": ErrSystem,
|
||||||
|
"buf_err": ErrBuf,
|
||||||
|
"perm_denied": ErrPermDenied,
|
||||||
|
"auth_err": ErrAuth,
|
||||||
|
"cred_insufficient": ErrCredInsufficient,
|
||||||
|
"authinfo_unavail": ErrAuthinfoUnavail,
|
||||||
|
"user_unknown": ErrUserUnknown,
|
||||||
|
"maxtries": ErrMaxtries,
|
||||||
|
"new_authtok_reqd": ErrNewAuthtokReqd,
|
||||||
|
"acct_expired": ErrAcctExpired,
|
||||||
|
"session_err": ErrSession,
|
||||||
|
"cred_unavail": ErrCredUnavail,
|
||||||
|
"cred_expired": ErrCredExpired,
|
||||||
|
"cred_err": ErrCred,
|
||||||
|
"no_module_data": ErrNoModuleData,
|
||||||
|
"conv_err": ErrConv,
|
||||||
|
"authtok_err": ErrAuthtok,
|
||||||
|
"authtok_recover_err": ErrAuthtokRecovery,
|
||||||
|
"authtok_lock_busy": ErrAuthtokLockBusy,
|
||||||
|
"authtok_disable_aging": ErrAuthtokDisableAging,
|
||||||
|
"try_again": ErrTryAgain,
|
||||||
|
"ignore": nil, /* Ignore can't be returned */
|
||||||
|
"abort": ErrAbort,
|
||||||
|
"authtok_expired": ErrAuthtokExpired,
|
||||||
|
"module_unknown": ErrModuleUnknown,
|
||||||
|
"bad_item": ErrBadItem,
|
||||||
|
"conv_again": ErrConvAgain,
|
||||||
|
"incomplete": ErrIncomplete,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action int
|
||||||
|
const (
|
||||||
|
account Action = iota + 1
|
||||||
|
auth
|
||||||
|
password
|
||||||
|
session
|
||||||
|
)
|
||||||
|
actions := map[string]Action{
|
||||||
|
"account": account,
|
||||||
|
"auth": auth,
|
||||||
|
"password": password,
|
||||||
|
"session": session,
|
||||||
|
}
|
||||||
|
|
||||||
|
c := Credentials{}
|
||||||
|
|
||||||
|
servicePath := t.TempDir()
|
||||||
|
|
||||||
|
for ret, expected := range statuses {
|
||||||
|
ret := ret
|
||||||
|
expected := expected
|
||||||
|
for actionName, action := range actions {
|
||||||
|
actionName := actionName
|
||||||
|
action := action
|
||||||
|
t.Run(fmt.Sprintf("%s %s", ret, actionName), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
serviceName := ret + "-" + actionName
|
||||||
|
serviceFile := filepath.Join(servicePath, serviceName)
|
||||||
|
contents := fmt.Sprintf("%[1]s requisite pam_debug.so "+
|
||||||
|
"auth=%[2]s cred=%[2]s acct=%[2]s prechauthtok=%[2]s "+
|
||||||
|
"chauthtok=%[2]s open_session=%[2]s close_session=%[2]s\n"+
|
||||||
|
"%[1]s requisite pam_permit.so\n", actionName, ret)
|
||||||
|
|
||||||
|
if err := os.WriteFile(serviceFile,
|
||||||
|
[]byte(contents), 0600); err != nil {
|
||||||
|
t.Fatalf("can't create service file %v: %v", serviceFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := StartConfDir(serviceName, "user", c, servicePath)
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("start #error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case account:
|
||||||
|
err = tx.AcctMgmt(0)
|
||||||
|
case auth:
|
||||||
|
err = tx.Authenticate(0)
|
||||||
|
case password:
|
||||||
|
err = tx.ChangeAuthTok(0)
|
||||||
|
case session:
|
||||||
|
err = tx.OpenSession(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, expected) {
|
||||||
|
t.Fatalf("error #unexpected status %#v vs %#v", err,
|
||||||
|
expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
var status Error
|
||||||
|
if !errors.As(err, &status) || err.Error() != status.Error() {
|
||||||
|
t.Fatalf("error #unexpected status %#v vs %#v", err.Error(),
|
||||||
|
status.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Finalizer(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
|
if !CheckPamHasStartConfdir() {
|
||||||
|
t.Skip("this requires PAM with Conf dir support")
|
||||||
|
}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
tx, err := StartConfDir("permit-service", "", nil, "test-services")
|
||||||
|
ensureTransactionEnds(t, tx)
|
||||||
|
defer maybeEndTransaction(t, tx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("start #error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
runtime.GC()
|
||||||
|
// sleep to switch to finalizer goroutine
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFailure_001(t *testing.T) {
|
func TestFailure_001(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
tx := Transaction{}
|
tx := Transaction{}
|
||||||
_, err := tx.GetEnvList()
|
_, err := tx.GetEnvList()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -399,6 +672,7 @@ func TestFailure_001(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFailure_002(t *testing.T) {
|
func TestFailure_002(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
tx := Transaction{}
|
tx := Transaction{}
|
||||||
err := tx.PutEnv("")
|
err := tx.PutEnv("")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -407,6 +681,7 @@ func TestFailure_002(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFailure_003(t *testing.T) {
|
func TestFailure_003(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
tx := Transaction{}
|
tx := Transaction{}
|
||||||
err := tx.CloseSession(0)
|
err := tx.CloseSession(0)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -415,6 +690,7 @@ func TestFailure_003(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFailure_004(t *testing.T) {
|
func TestFailure_004(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
tx := Transaction{}
|
tx := Transaction{}
|
||||||
err := tx.OpenSession(0)
|
err := tx.OpenSession(0)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -423,6 +699,7 @@ func TestFailure_004(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFailure_005(t *testing.T) {
|
func TestFailure_005(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
tx := Transaction{}
|
tx := Transaction{}
|
||||||
err := tx.ChangeAuthTok(0)
|
err := tx.ChangeAuthTok(0)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -431,6 +708,7 @@ func TestFailure_005(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFailure_006(t *testing.T) {
|
func TestFailure_006(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
tx := Transaction{}
|
tx := Transaction{}
|
||||||
err := tx.AcctMgmt(0)
|
err := tx.AcctMgmt(0)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -439,6 +717,7 @@ func TestFailure_006(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFailure_007(t *testing.T) {
|
func TestFailure_007(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
tx := Transaction{}
|
tx := Transaction{}
|
||||||
err := tx.SetCred(0)
|
err := tx.SetCred(0)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -447,6 +726,7 @@ func TestFailure_007(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFailure_008(t *testing.T) {
|
func TestFailure_008(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
tx := Transaction{}
|
tx := Transaction{}
|
||||||
err := tx.SetItem(User, "test")
|
err := tx.SetItem(User, "test")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -455,9 +735,19 @@ func TestFailure_008(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFailure_009(t *testing.T) {
|
func TestFailure_009(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
tx := Transaction{}
|
tx := Transaction{}
|
||||||
_, err := tx.GetItem(User)
|
_, err := tx.GetItem(User)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("getenvlist #expected an error")
|
t.Fatalf("getenvlist #expected an error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFailure_010(t *testing.T) {
|
||||||
|
t.Cleanup(maybeDoLeakCheck)
|
||||||
|
tx := Transaction{}
|
||||||
|
err := tx.End()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("end #unexpected error %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
42
utils.go
Normal file
42
utils.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Package pam provides a wrapper for the PAM application API.
|
||||||
|
package pam
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#ifdef __SANITIZE_ADDRESS__
|
||||||
|
#include <sanitizer/lsan_interface.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
maybe_do_leak_check (void)
|
||||||
|
{
|
||||||
|
#ifdef __SANITIZE_ADDRESS__
|
||||||
|
__lsan_do_leak_check();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func maybeDoLeakCheck() {
|
||||||
|
runtime.GC()
|
||||||
|
time.Sleep(time.Millisecond * 20)
|
||||||
|
if os.Getenv("GO_PAM_SKIP_LEAK_CHECK") == "" {
|
||||||
|
C.maybe_do_leak_check()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func allocateCBytes(bytes []byte) BinaryPointer {
|
||||||
|
return BinaryPointer(C.CBytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
func binaryPointerCBytesFinalizer(ptr BinaryPointer) {
|
||||||
|
C.free(unsafe.Pointer(ptr))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user