diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c678f1d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true + +[*.{go,go.tmpl}] +indent_style = tab +indent_size = 4 + +[*.{yaml,yml}] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..46ee6db --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +*.go text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +*.json text eol=lf +*.sh text eol=lf +*.md text eol=lf + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f076788..e31b828 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,4 @@ name: Continuous Integration - on: push: @@ -13,7 +12,7 @@ jobs: name: Tests strategy: matrix: - os: [ ubuntu-latest ] + os: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - name: Install Go @@ -22,6 +21,7 @@ jobs: go-version: stable - name: Install Ubuntu dependencies + if: ${{ matrix.os == 'ubuntu-latest' }} run: sudo apt install libpam0g-dev - name: Checkout code @@ -40,26 +40,63 @@ jobs: - name: Install goveralls run: go install github.com/mattn/goveralls@latest - - name: Install Syft - uses: anchore/sbom-action/download-syft@v0.17.2 - - name: Test run: | mkdir -p var go test -v -covermode atomic "-coverprofile=var/profile.cov" ./... + - name: Send coverage + if: false + env: + COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + goveralls "-coverprofile=profile.cov" "-service=github" "-parallel" "-flagname=go-${{ matrix.os }}" + package: + name: Package + strategy: + matrix: + os: [ ubuntu-latest ] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Install Ubuntu dependencies + if: ${{ matrix.os == 'ubuntu-latest' }} + run: sudo apt install libpam0g-dev + + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cache + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Install Syft + uses: anchore/sbom-action/download-syft@v0.17.2 + - name: GoReleaser uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: "~> v2" - args: build --snapshot --clean + args: release --snapshot --clean env: BIFROEST_VENDOR: engity - - name: Send coverage - if: false - env: - COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - goveralls "-coverprofile=profile.cov" "-service=github" "-parallel" "-flagname=go-${{ matrix.os }}" + - name: Archive package results + uses: actions/upload-artifact@v4 + with: + retention-days: 1 + name: dist + path: | + var/dist/*.tgz* + var/dist/*.zip* diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..a686a54 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,34 @@ +name: Lint +on: + push: + + pull_request: + types: + - opened + - reopened + +permissions: + contents: read + +jobs: + golangci: + name: "golangci-lint" + strategy: + matrix: + os: [ ubuntu-latest, windows-latest ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Install Ubuntu dependencies + if: ${{ matrix.os == 'ubuntu-latest' }} + run: sudo apt install libpam0g-dev + + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.60 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..b436c0d --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,30 @@ +run: + timeout: 20m + +linters: + enable: + - asasalint + - asciicheck + - bidichk + - misspell + disable: + - ineffassign + +linters-settings: + misspell: + locale: US + ignore-words: + - engity + - bifröst + - bifroest + +issues: + exclude-dirs: + - internal/fmtsort + - internal/text + exclude: + - S1002 + - S1016 + - SA1012 + - S1031 + - SA1019 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index d9f274d..5ea6d93 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -15,6 +15,7 @@ builds: binary: bifroest goos: - linux + - windows env: - CGO_ENABLED=0 goarch: diff --git a/.idea/.gitignore b/.idea/.gitignore index 1659f91..3d2aaca 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,3 +1,6 @@ * !/.gitignore !/icon.svg +!/inspectionProfiles +!/codeStyles +!/watcherTasks.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..2cbddaf --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,28 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..995b277 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml new file mode 100644 index 0000000..5d92fce --- /dev/null +++ b/.idea/watcherTasks.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/cmd/bifroest/dummy-server.go b/cmd/bifroest/dummy-server.go index 045982a..09e0adb 100644 --- a/cmd/bifroest/dummy-server.go +++ b/cmd/bifroest/dummy-server.go @@ -3,16 +3,18 @@ package main import ( "context" "fmt" - "github.com/alecthomas/kingpin" - log "github.com/echocat/slf4g" - "github.com/engity-com/bifroest/pkg/common" - "github.com/engity-com/bifroest/pkg/errors" "net" "net/http" "os" "os/signal" "strings" "syscall" + + "github.com/alecthomas/kingpin" + log "github.com/echocat/slf4g" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/errors" ) var ( @@ -83,7 +85,7 @@ func dummyServerHandleIndex(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodHead { return } - _, err = fmt.Fprintf(w, `Hello from a dummy-server! + _, _ = fmt.Fprintf(w, `Hello from a dummy-server! Called URI: %v Method: %v diff --git a/cmd/bifroest/main.go b/cmd/bifroest/main.go index 4fa89d4..c88832c 100644 --- a/cmd/bifroest/main.go +++ b/cmd/bifroest/main.go @@ -14,10 +14,6 @@ import ( "github.com/echocat/slf4g/native/facade/value" ) -const ( - defaultConfigurationRef = "/etc/engity/bifroest/configuration.yaml" -) - var ( configurationRef configuration.ConfigurationRef registerCommands []func(*kingpin.Application) diff --git a/cmd/bifroest/main_unix.go b/cmd/bifroest/main_unix.go new file mode 100644 index 0000000..e7e4aaa --- /dev/null +++ b/cmd/bifroest/main_unix.go @@ -0,0 +1,7 @@ +//go:build unix + +package main + +const ( + defaultConfigurationRef = "/etc/engity/bifroest/configuration.yaml" +) diff --git a/cmd/bifroest/main_windows.go b/cmd/bifroest/main_windows.go new file mode 100644 index 0000000..be332f7 --- /dev/null +++ b/cmd/bifroest/main_windows.go @@ -0,0 +1,7 @@ +//go:build windows + +package main + +const ( + defaultConfigurationRef = `C:\ProgramData\Engity\Bifroest\configuration.yaml` +) diff --git a/cmd/bifroest/run.go b/cmd/bifroest/run.go index 9d06ce3..50d93c5 100644 --- a/cmd/bifroest/run.go +++ b/cmd/bifroest/run.go @@ -2,12 +2,14 @@ package main import ( "context" - "github.com/alecthomas/kingpin" - log "github.com/echocat/slf4g" - "github.com/engity-com/bifroest/pkg/service" "os" "os/signal" "syscall" + + "github.com/alecthomas/kingpin" + log "github.com/echocat/slf4g" + + "github.com/engity-com/bifroest/pkg/service" ) var _ = registerCommand(func(app *kingpin.Application) { @@ -25,6 +27,7 @@ var _ = registerCommand(func(app *kingpin.Application) { func doRun() error { svc := service.Service{ Configuration: *configurationRef.Get(), + Version: versionV, } fail := func(err error) error { diff --git a/cmd/bifroest/sftp-server.go b/cmd/bifroest/sftp-server.go index 42f8164..a620c6a 100644 --- a/cmd/bifroest/sftp-server.go +++ b/cmd/bifroest/sftp-server.go @@ -1,9 +1,11 @@ package main import ( + "os" + "github.com/alecthomas/kingpin" + "github.com/engity-com/bifroest/pkg/sftp" - "os" ) var ( @@ -53,7 +55,7 @@ func (this *stdpipe) Close() (rErr error) { rErr = err } if err := os.Stdout.Close(); err != nil && rErr == nil { - rErr = err + rErr = err //nolint:golint,staticcheck } return nil } diff --git a/cmd/bifroest/version.go b/cmd/bifroest/version.go index cde5b52..4713a48 100644 --- a/cmd/bifroest/version.go +++ b/cmd/bifroest/version.go @@ -2,12 +2,14 @@ package main import ( "fmt" - "github.com/alecthomas/kingpin" - "github.com/engity-com/bifroest/pkg/common" "os" "runtime" "strings" "time" + + "github.com/alecthomas/kingpin" + + "github.com/engity-com/bifroest/pkg/common" ) var ( @@ -95,11 +97,13 @@ func (this versionT) Vendor() string { } func (this versionT) GoVersion() string { - v := runtime.Version() - strings.TrimPrefix(v, "go") - return v + return strings.TrimPrefix(runtime.Version(), "go") } func (this versionT) Platform() string { return runtime.GOOS + "/" + runtime.GOARCH } + +func (this versionT) Features() common.VersionFeatures { + return featuresV +} diff --git a/cmd/bifroest/version_features.go b/cmd/bifroest/version_features.go new file mode 100644 index 0000000..12a69e5 --- /dev/null +++ b/cmd/bifroest/version_features.go @@ -0,0 +1,41 @@ +package main + +import ( + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/configuration" + "github.com/engity-com/bifroest/pkg/crypto/unix/password" +) + +var ( + featuresV = &features{} +) + +type features struct{} + +func (this *features) ForEach(consumer func(common.VersionFeatureCategory)) { + consumer(&featureCategory{"authorization", configuration.GetSupportedAuthorizationFeatureFlags}) + consumer(&featureCategory{"environment", configuration.GetSupportedEnvironmentFeatureFlags}) + consumer(&featureCategory{"session", configuration.GetSupportedSessionFeatureFlags}) + consumer(&featureCategory{"password-crypt", password.GetSupportedFeatureFlags}) +} + +type featureCategory struct { + name string + getter func() []string +} + +func (this *featureCategory) Name() string { + return this.name +} + +func (this *featureCategory) ForEach(consumer func(common.VersionFeature)) { + for _, v := range this.getter() { + consumer(feature(v)) + } +} + +type feature string + +func (this feature) Name() string { + return string(this) +} diff --git a/doc/configurations/demo.yaml b/doc/configurations/demo.yaml index 7343804..d829c72 100644 --- a/doc/configurations/demo.yaml +++ b/doc/configurations/demo.yaml @@ -237,10 +237,10 @@ flows: ## default always true. ## @template{context{authentication{token{..},idToken{..},userInfo{..}}}} -> bool loginAllowed: | - {{ and - (.authorization.idToken.groups | has "my-great-group-uuid") - (.authorization.idToken.tid | eq "my-great-tenant-uuid") - }} + {{ and + (.authorization.idToken.groups | has "my-great-group-uuid") + (.authorization.idToken.tid | eq "my-great-tenant-uuid") + }} dispose: ## Tell the environment to delete this user afterward, if it's corresponding session will be disposed. @@ -330,17 +330,17 @@ flows: ## default always true. ## @template{context{authentication{user{name,uid,group,..}}}} -> bool loginAllowed: | - {{ or - (.authorization.user.group.name | eq "ssh" ) - (.authorization.user.groups | firstMatching `{{.name | eq "ssh"}}`) - }} - - dispose: {} - ## These properties have no affect, as this user is never managed (authorization: type=local) in this - ## case, although they are all true. - #deleteManagedUser: true - #deleteManagedUserHomeDir: true - #killManagedUserProcesses: true + {{ or + (.authorization.user.group.name | eq "ssh" ) + (.authorization.user.groups | firstMatching `{{.name | eq "ssh"}}`) + }} + + dispose: { } + ## These properties have no affect, as this user is never managed (authorization: type=local) in this + ## case, although they are all true. + #deleteManagedUser: true + #deleteManagedUserHomeDir: true + #killManagedUserProcesses: true ## Banner which will be shown to the user upon successfully start of its environment. ## @default diff --git a/doc/configurations/docker.yaml b/doc/configurations/docker.yaml new file mode 100644 index 0000000..bf87988 --- /dev/null +++ b/doc/configurations/docker.yaml @@ -0,0 +1,27 @@ +## The configuration can be used if you simply want to use Engity's Bifröst as a drop-in-replacement +## for the regular sshd. + +ssh: + addresses: [ ":22" ] + +session: + type: fs + +flows: + - name: local + authorization: + type: simple + entries: + - password: foo:sdfasdfasdf + ## If PAM does not exist or is not supported, please comment the following line. + pamService: "sshd" + + environment: + type: local + name: "{{.authorization.user.name}}" + ## If you only want to allow user with group "ssh" to log in, uncomment the following lines: + #loginAllowed: | + # {{ or + # (.authorization.user.group.name | eq "ssh" ) + # (.authorization.user.groups | firstMatching `{{.name | eq "ssh" }}` ) + # }} diff --git a/go.mod b/go.mod index 47a9494..d3364dd 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/engity-com/bifroest -go 1.22.0 +go 1.23.0 require ( github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 @@ -21,6 +21,7 @@ require ( github.com/pkg/sftp v1.13.6 github.com/shirou/gopsutil v3.21.11+incompatible github.com/stretchr/testify v1.9.0 + github.com/tg123/go-htpasswd v1.2.2 golang.org/x/crypto v0.26.0 golang.org/x/oauth2 v0.22.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 5740dbd..6321c34 100644 --- a/go.sum +++ b/go.sum @@ -99,6 +99,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tg123/go-htpasswd v1.2.2 h1:tmNccDsQ+wYsoRfiONzIhDm5OkVHQzN3w4FOBAlN6BY= +github.com/tg123/go-htpasswd v1.2.2/go.mod h1:FcIrK0J+6zptgVwK1JDlqyajW/1B4PtuJ/FLWl7nx8A= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= diff --git a/internal/text/template/exec_test.go b/internal/text/template/exec_test.go index e607fd3..978f759 100644 --- a/internal/text/template/exec_test.go +++ b/internal/text/template/exec_test.go @@ -820,7 +820,7 @@ func testExecute(execTests []execTest, template *Template, t *testing.T) { fmt.Printf("%s: %s\n\t%s\n", test.name, test.input, err) } } - result := b.String() + result := strings.ReplaceAll(b.String(), "\r\n", "\n") if result != test.output { t.Errorf("%s: expected\n\t%q\ngot\n\t%q", test.name, test.output, result) } diff --git a/pkg/authorization/authorization.go b/pkg/authorization/authorization.go index 348f1d0..4d3217b 100644 --- a/pkg/authorization/authorization.go +++ b/pkg/authorization/authorization.go @@ -2,11 +2,13 @@ package authorization import ( "context" + + "golang.org/x/crypto/ssh" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/configuration" "github.com/engity-com/bifroest/pkg/session" "github.com/engity-com/bifroest/pkg/sys" - "golang.org/x/crypto/ssh" ) type Authorization interface { diff --git a/pkg/authorization/authorizer.go b/pkg/authorization/authorizer.go index 5f3cbd1..ea3a66b 100644 --- a/pkg/authorization/authorizer.go +++ b/pkg/authorization/authorizer.go @@ -3,9 +3,11 @@ package authorization import ( "context" "errors" + "io" + log "github.com/echocat/slf4g" + "github.com/engity-com/bifroest/pkg/session" - "io" ) var ( diff --git a/pkg/authorization/facade-authorizer.go b/pkg/authorization/facade-authorizer.go index 276cf8f..daf9d0f 100644 --- a/pkg/authorization/facade-authorizer.go +++ b/pkg/authorization/facade-authorizer.go @@ -3,11 +3,12 @@ package authorization import ( "context" "fmt" + "reflect" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/configuration" "github.com/engity-com/bifroest/pkg/errors" "github.com/engity-com/bifroest/pkg/session" - "reflect" ) func NewAuthorizerFacade(ctx context.Context, flows *configuration.Flows) (*AuthorizerFacade, error) { @@ -17,7 +18,7 @@ func NewAuthorizerFacade(ctx context.Context, flows *configuration.Flows) (*Auth entries := make([]facaded, len(*flows)) for i, flow := range *flows { - if err := entries[i].setConf(ctx, &flow); err != nil { + if err := entries[i].newFrom(ctx, &flow); err != nil { return nil, err } } @@ -103,35 +104,21 @@ type facaded struct { requirement *configuration.Requirement } -func (this *facaded) setConf(ctx context.Context, flow *configuration.Flow) error { +func (this *facaded) newFrom(ctx context.Context, flow *configuration.Flow) error { fail := func(err error) error { return fmt.Errorf("cannot initizalize authorization for flow %q: %w", flow.Name, err) } - var newV CloseableAuthorizer - switch authConf := flow.Authorization.V.(type) { - case *configuration.AuthorizationOidcDeviceAuth: - v, err := NewOidcDeviceAuth(ctx, flow.Name, authConf) - if err != nil { - return fail(err) - } - newV = v - case *configuration.AuthorizationLocal: - v, err := NewLocal(ctx, flow.Name, authConf) - if err != nil { - return fail(err) - } - newV = v - default: - return fail(fmt.Errorf("cannot handle authorization type %v", reflect.TypeOf(flow.Authorization.V))) + factory, ok := configurationTypeToAuthorizerFactory[reflect.TypeOf(flow.Authorization.V)] + if !ok { + return fail(errors.Config.Newf("cannot handle authorization type %v", reflect.TypeOf(flow.Authorization.V))) } - - if oldV := this.CloseableAuthorizer; oldV != nil { - if err := oldV.Close(); err != nil { - return err - } + m := reflect.ValueOf(factory) + rets := m.Call([]reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(flow.Name), reflect.ValueOf(flow.Authorization.V)}) + if err, ok := rets[1].Interface().(error); ok && err != nil { + return fail(err) } - this.CloseableAuthorizer = newV + this.CloseableAuthorizer = rets[0].Interface().(CloseableAuthorizer) this.requirement = &flow.Requirement this.flow = flow.Name return nil @@ -149,3 +136,15 @@ func (this *facaded) canHandle(req Request) (bool, error) { return true, nil } + +var ( + configurationTypeToAuthorizerFactory = make(map[reflect.Type]any) +) + +type AuthorizerFactory[C any, A CloseableAuthorizer] func(ctx context.Context, flow configuration.FlowName, conf C) (A, error) + +func RegisterAuthorizer[C any, A CloseableAuthorizer](factory AuthorizerFactory[C, A]) AuthorizerFactory[C, A] { + ct := reflect.TypeFor[C]() + configurationTypeToAuthorizerFactory[ct] = factory + return factory +} diff --git a/pkg/authorization/htpasswd-authorizer.go b/pkg/authorization/htpasswd-authorizer.go new file mode 100644 index 0000000..f920873 --- /dev/null +++ b/pkg/authorization/htpasswd-authorizer.go @@ -0,0 +1,248 @@ +package authorization + +import ( + "context" + "encoding/json" + "fmt" + + log "github.com/echocat/slf4g" + + "github.com/engity-com/bifroest/pkg/configuration" + "github.com/engity-com/bifroest/pkg/errors" + "github.com/engity-com/bifroest/pkg/session" +) + +var ( + _ = RegisterAuthorizer(NewHtpasswd) +) + +type HtpasswdAuthorizer struct { + flow configuration.FlowName + conf *configuration.AuthorizationHtpasswd + + Logger log.Logger +} + +func NewHtpasswd(_ context.Context, flow configuration.FlowName, conf *configuration.AuthorizationHtpasswd) (*HtpasswdAuthorizer, error) { + fail := func(err error) (*HtpasswdAuthorizer, error) { + return nil, err + } + failf := func(msg string, args ...any) (*HtpasswdAuthorizer, error) { + return fail(errors.Newf(errors.Config, msg, args...)) + } + + if conf == nil { + return failf("nil configuration") + } + + result := HtpasswdAuthorizer{ + flow: flow, + conf: conf, + } + + return &result, nil +} + +func (this *HtpasswdAuthorizer) AuthorizePublicKey(req PublicKeyRequest) (Authorization, error) { + fail := func(err error) (Authorization, error) { + return nil, fmt.Errorf("cannot authorize htpasswd %q via authorized keys: %w", req.Remote().User(), err) + } + failf := func(message string, args ...any) (Authorization, error) { + return fail(fmt.Errorf(message, args...)) + } + + sess, err := req.Sessions().FindByPublicKey(req.Context(), req.RemotePublicKey(), (&session.FindOpts{}).WithPredicate( + session.IsFlow(this.flow), + session.IsStillValid, + session.IsRemoteName(req.Remote().User()), + )) + if errors.Is(err, session.ErrNoSuchSession) { + return Forbidden(req.Remote()), nil + } else if err != nil { + return failf("cannot find session: %w", err) + } + + auth := &htpasswd{ + remote: req.Remote(), + envVars: nil, + flow: this.flow, + session: sess, + sessionsPublicKey: req.RemotePublicKey(), + } + + if accepted, err := req.Validate(auth); err != nil { + return nil, fmt.Errorf("cannot validate request: %w", err) + } else if !accepted { + return Forbidden(req.Remote()), nil + } + + return auth, nil +} + +func (this *HtpasswdAuthorizer) lookupEntry(req Request, password string) (auth *htpasswd, accepted bool, err error) { + match := false + if f := this.conf.File; !f.IsZero() { + if f.Match(req.Remote().User(), password) { + match = true + } + } + + if f := this.conf.Entries; !match && !f.IsZero() { + if f.Match(req.Remote().User(), password) { + match = true + } + } + + if !match { + return nil, false, nil + } + + auth = &htpasswd{ + req.Remote(), + nil, + this.flow, + nil, + nil, + } + + accepted, err = req.Validate(auth) + if err != nil { + return nil, false, fmt.Errorf("cannot validate request: %w", err) + } + + return auth, accepted, nil +} + +func (this *HtpasswdAuthorizer) ensureSessionFor(req Request) (session.Session, error) { + fail := func(err error) (session.Session, error) { + return nil, err + } + failf := func(msg string, args ...any) (session.Session, error) { + return fail(errors.Newf(errors.System, msg, args...)) + } + + buf := htpasswdToken{ + User: htpasswdTokenUser{ + Name: req.Remote().User(), + }, + EnvVars: nil, + } + at, err := json.Marshal(buf) + if err != nil { + return failf("cannot marshal authorization token: %w", err) + } + + sess, err := req.Sessions().FindByAccessToken(req.Context(), at, (&session.FindOpts{}).WithPredicate( + session.IsFlow(this.flow), + session.IsStillValid, + session.IsRemoteName(req.Remote().User()), + )) + if errors.Is(err, session.ErrNoSuchSession) { + sess, err = req.Sessions().Create(req.Context(), this.flow, req.Remote(), at) + } + if err != nil { + return fail(err) + } + + return sess, nil +} + +func (this *HtpasswdAuthorizer) AuthorizePassword(req PasswordRequest) (Authorization, error) { + fail := func(err error) (Authorization, error) { + return nil, fmt.Errorf("cannot authorize htpasswd %q via password: %w", req.Remote().User(), err) + } + failf := func(message string, args ...any) (Authorization, error) { + return fail(fmt.Errorf(message, args...)) + } + + auth, accepted, err := this.lookupEntry(req, req.RemotePassword()) + if err != nil { + return fail(err) + } + if !accepted { + return Forbidden(req.Remote()), nil + } + + sess, err := this.ensureSessionFor(req) + if err != nil { + return failf("cannot create session: %w", err) + } + auth.session = sess + + return auth, nil +} + +func (this *HtpasswdAuthorizer) AuthorizeInteractive(req InteractiveRequest) (Authorization, error) { + fail := func(err error) (Authorization, error) { + return nil, fmt.Errorf("cannot authorize htpasswd %q via password: %w", req.Remote().User(), err) + } + failf := func(message string, args ...any) (Authorization, error) { + return fail(fmt.Errorf(message, args...)) + } + + pass, err := req.Prompt("Password: ", false) + if err != nil { + return fail(err) + } + + auth, accepted, err := this.lookupEntry(req, pass) + if err != nil { + return fail(err) + } + if !accepted { + return Forbidden(req.Remote()), nil + } + + sess, err := this.ensureSessionFor(req) + if err != nil { + return failf("cannot create session: %w", err) + } + auth.session = sess + + return auth, nil +} + +func (this *HtpasswdAuthorizer) RestoreFromSession(ctx context.Context, sess session.Session, _ *RestoreOpts) (Authorization, error) { + failf := func(t errors.Type, msg string, args ...any) (Authorization, error) { + args = append([]any{sess}, args...) + return nil, errors.Newf(t, "cannot restore authorization from session %v: "+msg, args...) + } + if !sess.Flow().IsEqualTo(this.flow) { + return nil, ErrNoSuchAuthorization + } + + tb, err := sess.AuthorizationToken(ctx) + if err != nil { + return failf(errors.System, "cannot retrieve token: %w", err) + } + + if len(tb) == 0 { + return nil, ErrNoSuchAuthorization + } + + var buf htpasswdToken + if err := json.Unmarshal(tb, &buf); err != nil { + return failf(errors.System, "cannot decode token of: %w", err) + } + + si, err := sess.Info(ctx) + if err != nil { + return failf(errors.System, "cannot retrieve session's info: %w", err) + } + sla, err := si.LastAccessed(ctx) + if err != nil { + return failf(errors.System, "cannot retrieve session's last accessed: %w", err) + } + + return &htpasswd{ + sla.Remote(), + buf.EnvVars.Clone(), + this.flow.Clone(), + sess, + nil, + }, nil +} + +func (this *HtpasswdAuthorizer) Close() error { + return nil +} diff --git a/pkg/authorization/htpasswd.go b/pkg/authorization/htpasswd.go new file mode 100644 index 0000000..607cfb1 --- /dev/null +++ b/pkg/authorization/htpasswd.go @@ -0,0 +1,79 @@ +package authorization + +import ( + "context" + "fmt" + + "golang.org/x/crypto/ssh" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/configuration" + "github.com/engity-com/bifroest/pkg/session" + "github.com/engity-com/bifroest/pkg/sys" +) + +type htpasswd struct { + remote common.Remote + envVars sys.EnvVars + flow configuration.FlowName + session session.Session + sessionsPublicKey ssh.PublicKey +} + +func (this *htpasswd) Remote() common.Remote { + return this.remote +} + +func (this *htpasswd) IsAuthorized() bool { + return true +} + +func (this *htpasswd) EnvVars() sys.EnvVars { + return this.envVars +} + +func (this *htpasswd) Flow() configuration.FlowName { + return this.flow +} + +func (this *htpasswd) FindSession() session.Session { + return this.session +} + +func (this *htpasswd) FindSessionsPublicKey() ssh.PublicKey { + return this.sessionsPublicKey +} + +func (this *htpasswd) GetField(name string, ce ContextEnabled) (any, bool, error) { + return getField(name, ce, this, func() (any, bool, error) { + switch name { + case "user": + return this.Remote(), true, nil + default: + return nil, false, fmt.Errorf("unknown field %q", name) + } + }) +} + +func (this *htpasswd) Dispose(ctx context.Context) (bool, error) { + sess := this.session + if sess == nil { + return false, nil + } + + // Delete myself from my session. + if err := sess.SetAuthorizationToken(ctx, nil); err != nil { + return false, err + } + + return true, nil +} + +type htpasswdToken struct { + User htpasswdTokenUser `json:"user"` + EnvVars sys.EnvVars `json:"envVars,omitempty"` +} + +type htpasswdTokenUser struct { + Name string `json:"name,omitempty"` +} diff --git a/pkg/authorization/local-authorizer.go b/pkg/authorization/local-authorizer.go index 2e3f952..79cb9d8 100644 --- a/pkg/authorization/local-authorizer.go +++ b/pkg/authorization/local-authorizer.go @@ -1,3 +1,5 @@ +//go:build unix + package authorization import ( @@ -5,7 +7,10 @@ import ( "context" "encoding/json" "fmt" + log "github.com/echocat/slf4g" + "golang.org/x/crypto/ssh" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/configuration" "github.com/engity-com/bifroest/pkg/crypto" @@ -14,7 +19,10 @@ import ( "github.com/engity-com/bifroest/pkg/sys" "github.com/engity-com/bifroest/pkg/template" "github.com/engity-com/bifroest/pkg/user" - "golang.org/x/crypto/ssh" +) + +var ( + _ = RegisterAuthorizer(NewLocal) ) type LocalAuthorizer struct { @@ -157,6 +165,20 @@ func (this *LocalAuthorizer) isAuthorizedViaPublicKey(req PublicKeyRequest, u *u return true, nil } +type userEnabledRequest struct { + Request + user *user.User +} + +func (this *userEnabledRequest) GetField(name string) (any, bool) { + switch name { + case "user": + return this.user, true + default: + return nil, false + } +} + func (this *LocalAuthorizer) getAuthorizedKeysFilesOf(req PublicKeyRequest, u *user.User) ([]string, error) { ctx := userEnabledRequest{req, u} return common.MapSliceErr(this.conf.AuthorizedKeys, func(tmpl template.String) (string, error) { diff --git a/pkg/authorization/local-authorizer_with_pam.go b/pkg/authorization/local-authorizer_with_pam.go index b558490..b8db22c 100644 --- a/pkg/authorization/local-authorizer_with_pam.go +++ b/pkg/authorization/local-authorizer_with_pam.go @@ -4,8 +4,10 @@ package authorization import ( "errors" - "github.com/engity-com/bifroest/pkg/sys" + "github.com/msteinert/pam/v2" + + "github.com/engity-com/bifroest/pkg/sys" ) func (this *LocalAuthorizer) checkPassword(req PasswordRequest, requestedUsername string, validatePassword func(string, Request) (bool, error)) (username string, env sys.EnvVars, success bool, rErr error) { diff --git a/pkg/authorization/local-authorizer_without_pam.go b/pkg/authorization/local-authorizer_without_pam.go index 1acf2da..7d72393 100644 --- a/pkg/authorization/local-authorizer_without_pam.go +++ b/pkg/authorization/local-authorizer_without_pam.go @@ -1,9 +1,10 @@ -//go:build !cgo || !linux || without_pam +//go:build (!cgo || without_pam) && linux package authorization import ( "fmt" + "github.com/engity-com/bifroest/pkg/sys" ) diff --git a/pkg/authorization/local.go b/pkg/authorization/local.go index 8b1b944..de957b6 100644 --- a/pkg/authorization/local.go +++ b/pkg/authorization/local.go @@ -1,14 +1,18 @@ +//go:build unix + package authorization import ( "context" "fmt" + + "golang.org/x/crypto/ssh" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/configuration" "github.com/engity-com/bifroest/pkg/session" "github.com/engity-com/bifroest/pkg/sys" "github.com/engity-com/bifroest/pkg/user" - "golang.org/x/crypto/ssh" ) type local struct { diff --git a/pkg/authorization/oidc-device-auth-authorizer.go b/pkg/authorization/oidc-device-auth-authorizer.go index 793ccb0..b0f3e0c 100644 --- a/pkg/authorization/oidc-device-auth-authorizer.go +++ b/pkg/authorization/oidc-device-auth-authorizer.go @@ -4,12 +4,18 @@ import ( "context" "encoding/json" "fmt" + coidc "github.com/coreos/go-oidc/v3/oidc" log "github.com/echocat/slf4g" + "golang.org/x/oauth2" + "github.com/engity-com/bifroest/pkg/configuration" "github.com/engity-com/bifroest/pkg/errors" "github.com/engity-com/bifroest/pkg/session" - "golang.org/x/oauth2" +) + +var ( + _ = RegisterAuthorizer(NewOidcDeviceAuth) ) type OidcDeviceAuthAuthorizer struct { @@ -355,7 +361,7 @@ func (this *OidcDeviceAuthAuthorizer) retrieveDeviceAuthToken(ctx context.Contex return failf(errors.User, "authorize of device timed out") } if errors.Is(err, context.Canceled) { - return failf(errors.User, "authorize cancelled by user") + return failf(errors.User, "authorize canceled by user") } var oaErr *oauth2.RetrieveError if errors.As(err, &oaErr) && oaErr.ErrorCode == "expired_token" { diff --git a/pkg/authorization/oidc.go b/pkg/authorization/oidc.go index e5e0db5..fc7e2b8 100644 --- a/pkg/authorization/oidc.go +++ b/pkg/authorization/oidc.go @@ -3,14 +3,16 @@ package authorization import ( "context" "fmt" + "sync" + coidc "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/crypto/ssh" + "golang.org/x/oauth2" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/configuration" "github.com/engity-com/bifroest/pkg/session" "github.com/engity-com/bifroest/pkg/sys" - "golang.org/x/crypto/ssh" - "golang.org/x/oauth2" - "sync" ) type oidc struct { diff --git a/pkg/authorization/request.go b/pkg/authorization/request.go index 46cef2f..f60ee26 100644 --- a/pkg/authorization/request.go +++ b/pkg/authorization/request.go @@ -2,11 +2,11 @@ package authorization import ( "github.com/echocat/slf4g" - "github.com/engity-com/bifroest/pkg/common" - "github.com/engity-com/bifroest/pkg/session" - "github.com/engity-com/bifroest/pkg/user" "github.com/gliderlabs/ssh" gssh "golang.org/x/crypto/ssh" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/session" ) type Request interface { @@ -33,17 +33,3 @@ type InteractiveRequest interface { SendError(string) error Prompt(msg string, echoOn bool) (string, error) } - -type userEnabledRequest struct { - Request - user *user.User -} - -func (this *userEnabledRequest) GetField(name string) (any, bool) { - switch name { - case "user": - return this.user, true - default: - return nil, false - } -} diff --git a/pkg/authorization/simple-authorizer.go b/pkg/authorization/simple-authorizer.go new file mode 100644 index 0000000..5a4a96b --- /dev/null +++ b/pkg/authorization/simple-authorizer.go @@ -0,0 +1,347 @@ +package authorization + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + + log "github.com/echocat/slf4g" + "golang.org/x/crypto/ssh" + + "github.com/engity-com/bifroest/pkg/configuration" + "github.com/engity-com/bifroest/pkg/crypto" + "github.com/engity-com/bifroest/pkg/errors" + "github.com/engity-com/bifroest/pkg/session" +) + +var ( + _ = RegisterAuthorizer(NewSimple) +) + +type SimpleAuthorizer struct { + flow configuration.FlowName + conf *configuration.AuthorizationSimple + + Logger log.Logger +} + +func NewSimple(_ context.Context, flow configuration.FlowName, conf *configuration.AuthorizationSimple) (*SimpleAuthorizer, error) { + fail := func(err error) (*SimpleAuthorizer, error) { + return nil, err + } + failf := func(msg string, args ...any) (*SimpleAuthorizer, error) { + return fail(errors.Newf(errors.Config, msg, args...)) + } + + if conf == nil { + return failf("nil configuration") + } + + result := SimpleAuthorizer{ + flow: flow, + conf: conf, + } + + return &result, nil +} + +func (this *SimpleAuthorizer) AuthorizePublicKey(req PublicKeyRequest) (Authorization, error) { + fail := func(err error) (Authorization, error) { + return nil, fmt.Errorf("cannot authorize simple %q via authorized keys: %w", req.Remote().User(), err) + } + failf := func(message string, args ...any) (Authorization, error) { + return fail(fmt.Errorf(message, args...)) + } + + entry, auth, accepted, err := this.lookupEntry(req) + if err != nil { + return fail(err) + } + if !accepted { + return Forbidden(req.Remote()), nil + } + + sess, err := req.Sessions().FindByPublicKey(req.Context(), req.RemotePublicKey(), (&session.FindOpts{}).WithPredicate( + session.IsFlow(this.flow), + session.IsStillValid, + session.IsRemoteName(req.Remote().User()), + )) + if errors.Is(err, session.ErrNoSuchSession) { + if ok, err := this.isAuthorizedViaPublicKey(req, entry); err != nil { + return fail(err) + } else if !ok { + return Forbidden(req.Remote()), nil + } + sess, err = this.ensureSessionFor(req, entry) + if err != nil { + return fail(err) + } + + auth.session = sess + } else if err != nil { + return failf("cannot find session: %w", err) + } else { + auth.session = sess + auth.sessionsPublicKey = req.RemotePublicKey() + } + + return auth, nil +} + +func (this *SimpleAuthorizer) lookupEntry(req Request) (entry *configuration.AuthorizationSimpleEntry, auth *simple, accepted bool, err error) { + for _, candidate := range this.conf.Entries { + if !strings.EqualFold(candidate.Name, req.Remote().User()) { + continue + } + entry = &candidate + break + } + + if entry == nil { + return nil, nil, false, nil + } + + auth = &simple{ + entry, + req.Remote(), + nil, + this.flow, + nil, + nil, + } + + accepted, err = req.Validate(auth) + if err != nil { + return nil, nil, false, fmt.Errorf("cannot validate request: %w", err) + } + + return entry, auth, accepted, nil +} + +func (this *SimpleAuthorizer) isAuthorizedViaPublicKey(req PublicKeyRequest, entry *configuration.AuthorizationSimpleEntry) (bool, error) { + fail := func(err error) (bool, error) { + return false, err + } + failf := func(msg string, args ...any) (bool, error) { + return fail(errors.Newf(errors.System, msg, args...)) + } + + foundMatch := false + + if v := entry.AuthorizedKeysFile; !v.IsZero() { + if err := v.ForEach(func(_ int, key ssh.PublicKey, _ string, _ []crypto.AuthorizedKeyOption) (canContinue bool, err error) { + if bytes.Equal(req.RemotePublicKey().Marshal(), key.Marshal()) { + foundMatch = true + return false, nil + } + return true, nil + }); err != nil { + return failf("cannot resolve authorized keys of user %q: %w", entry.Name, err) + } + } + + if !foundMatch { + if v := entry.AuthorizedKeys; !v.IsZero() { + if err := v.ForEach(func(_ int, key ssh.PublicKey, _ string, _ []crypto.AuthorizedKeyOption) (canContinue bool, err error) { + if bytes.Equal(req.RemotePublicKey().Marshal(), key.Marshal()) { + foundMatch = true + return false, nil + } + return true, nil + }); err != nil { + return failf("cannot resolve authorized keys of user %q", entry.Name, err) + } + } + } + + if !foundMatch { + req.Logger().Debug("presented public key does not match any authorized keys of simple user") + return false, nil + } + + return true, nil +} + +func (this *SimpleAuthorizer) ensureSessionFor(req Request, entry *configuration.AuthorizationSimpleEntry) (session.Session, error) { + fail := func(err error) (session.Session, error) { + return nil, err + } + failf := func(msg string, args ...any) (session.Session, error) { + return fail(errors.Newf(errors.System, msg, args...)) + } + + buf := simpleToken{ + User: simpleTokenUser{ + Name: entry.Name, + }, + EnvVars: nil, + } + at, err := json.Marshal(buf) + if err != nil { + return failf("cannot marshal authorization token: %w", err) + } + + sess, err := req.Sessions().FindByAccessToken(req.Context(), at, (&session.FindOpts{}).WithPredicate( + session.IsFlow(this.flow), + session.IsStillValid, + session.IsRemoteName(req.Remote().User()), + )) + if errors.Is(err, session.ErrNoSuchSession) { + sess, err = req.Sessions().Create(req.Context(), this.flow, req.Remote(), at) + } + if err != nil { + return fail(err) + } + + return sess, nil +} + +func (this *SimpleAuthorizer) AuthorizePassword(req PasswordRequest) (Authorization, error) { + fail := func(err error) (Authorization, error) { + return nil, fmt.Errorf("cannot authorize simple %q via password: %w", req.Remote().User(), err) + } + failf := func(message string, args ...any) (Authorization, error) { + return fail(fmt.Errorf(message, args...)) + } + + entry, auth, accepted, err := this.lookupEntry(req) + if err != nil { + return fail(err) + } + if !accepted { + return Forbidden(req.Remote()), nil + } + + if expected := entry.Password; expected.IsZero() { + return Forbidden(req.Remote()), nil + } else if ok, err := expected.Compare([]byte(req.RemotePassword())); err != nil || !ok { + return Forbidden(req.Remote()), nil + } + + sess, err := this.ensureSessionFor(req, entry) + if err != nil { + return failf("cannot create session: %w", err) + } + + auth.session = sess + + return auth, nil +} + +func (this *SimpleAuthorizer) AuthorizeInteractive(req InteractiveRequest) (Authorization, error) { + fail := func(err error) (Authorization, error) { + return nil, fmt.Errorf("cannot authorize simple %q via password: %w", req.Remote().User(), err) + } + failf := func(message string, args ...any) (Authorization, error) { + return fail(fmt.Errorf(message, args...)) + } + + entry, auth, accepted, err := this.lookupEntry(req) + if err != nil { + return fail(err) + } + if !accepted { + return Forbidden(req.Remote()), nil + } + + pass, err := req.Prompt("Password: ", false) + if err != nil { + return fail(err) + } + + if expected := entry.Password; expected.IsZero() { + return Forbidden(req.Remote()), nil + } else if ok, err := expected.Compare([]byte(pass)); err != nil || !ok { + return Forbidden(req.Remote()), nil + } + + sess, err := this.ensureSessionFor(req, entry) + if err != nil { + return failf("cannot create session: %w", err) + } + + auth.session = sess + + return auth, nil +} + +func (this *SimpleAuthorizer) RestoreFromSession(ctx context.Context, sess session.Session, opts *RestoreOpts) (Authorization, error) { + failf := func(t errors.Type, msg string, args ...any) (Authorization, error) { + args = append([]any{sess}, args...) + return nil, errors.Newf(t, "cannot restore authorization from session %v: "+msg, args...) + } + cleanFromSessionOnly := func() (Authorization, error) { + if opts.IsAutoCleanUpAllowed() { + // Clear the stored token. + if err := sess.SetAuthorizationToken(ctx, nil); err != nil { + return failf(errors.System, "cannot clear existing authorization token of session after user wasn't found: %w", err) + } + opts.GetLogger(this.logger). + With("session", sess). + Info("session's user does not longer exist; therefore according authorization token was removed from session") + } + return nil, ErrNoSuchAuthorization + } + + if !sess.Flow().IsEqualTo(this.flow) { + return nil, ErrNoSuchAuthorization + } + + tb, err := sess.AuthorizationToken(ctx) + if err != nil { + return failf(errors.System, "cannot retrieve token: %w", err) + } + + if len(tb) == 0 { + return nil, ErrNoSuchAuthorization + } + + var buf simpleToken + if err := json.Unmarshal(tb, &buf); err != nil { + return failf(errors.System, "cannot decode token of: %w", err) + } + + var entry *configuration.AuthorizationSimpleEntry + for _, candidate := range this.conf.Entries { + if candidate.Name != buf.User.Name { + continue + } + entry = &candidate + break + } + + if entry == nil { + return cleanFromSessionOnly() + } + + si, err := sess.Info(ctx) + if err != nil { + return failf(errors.System, "cannot retrieve session's info: %w", err) + } + sla, err := si.LastAccessed(ctx) + if err != nil { + return failf(errors.System, "cannot retrieve session's last accessed: %w", err) + } + + return &simple{ + entry, + sla.Remote(), + buf.EnvVars.Clone(), + this.flow.Clone(), + sess, + nil, + }, nil +} + +func (this *SimpleAuthorizer) Close() error { + return nil +} + +func (this *SimpleAuthorizer) logger() log.Logger { + if v := this.Logger; v != nil { + return v + } + return log.GetLogger("authorizer") +} diff --git a/pkg/authorization/simple.go b/pkg/authorization/simple.go new file mode 100644 index 0000000..6511432 --- /dev/null +++ b/pkg/authorization/simple.go @@ -0,0 +1,80 @@ +package authorization + +import ( + "context" + "fmt" + + "golang.org/x/crypto/ssh" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/configuration" + "github.com/engity-com/bifroest/pkg/session" + "github.com/engity-com/bifroest/pkg/sys" +) + +type simple struct { + entry *configuration.AuthorizationSimpleEntry + remote common.Remote + envVars sys.EnvVars + flow configuration.FlowName + session session.Session + sessionsPublicKey ssh.PublicKey +} + +func (this *simple) Remote() common.Remote { + return this.remote +} + +func (this *simple) IsAuthorized() bool { + return true +} + +func (this *simple) EnvVars() sys.EnvVars { + return this.envVars +} + +func (this *simple) Flow() configuration.FlowName { + return this.flow +} + +func (this *simple) FindSession() session.Session { + return this.session +} + +func (this *simple) FindSessionsPublicKey() ssh.PublicKey { + return this.sessionsPublicKey +} + +func (this *simple) GetField(name string, ce ContextEnabled) (any, bool, error) { + return getField(name, ce, this, func() (any, bool, error) { + switch name { + case "entry": + return this.entry, true, nil + default: + return nil, false, fmt.Errorf("unknown field %q", name) + } + }) +} + +func (this *simple) Dispose(ctx context.Context) (bool, error) { + sess := this.session + if sess == nil { + return false, nil + } + + // Delete myself from my session. + if err := sess.SetAuthorizationToken(ctx, nil); err != nil { + return false, err + } + + return true, nil +} + +type simpleToken struct { + User simpleTokenUser `json:"user"` + EnvVars sys.EnvVars `json:"envVars,omitempty"` +} + +type simpleTokenUser struct { + Name string `json:"name,omitempty"` +} diff --git a/pkg/common/version.go b/pkg/common/version.go index 6c61388..355f660 100644 --- a/pkg/common/version.go +++ b/pkg/common/version.go @@ -2,6 +2,8 @@ package common import ( "fmt" + "strconv" + "strings" "time" ) @@ -14,24 +16,66 @@ type Version interface { Vendor() string GoVersion() string Platform() string + Features() VersionFeatures } func FormatVersion(v Version, format VersionFormat) string { switch format { case VersionFormatLong: - return v.Title() + ` + result := v.Title() + ` Version: ` + v.Version() + ` Revision: ` + v.Revision() + ` Edition: ` + v.Edition().String() + ` Build: ` + v.BuildAt().Format(time.RFC3339) + ` by ` + v.Vendor() + ` Go: ` + v.GoVersion() + ` -Platform: ` + v.Platform() +Platform: ` + v.Platform() + ` +Features: ` + + csnl := 0 + v.Features().ForEach(func(category VersionFeatureCategory) { + cnl := len(category.Name()) + 1 + if cnl > csnl { + csnl = cnl + } + }) + + v.Features().ForEach(func(category VersionFeatureCategory) { + var fts []string + category.ForEach(func(feature VersionFeature) { + fts = append(fts, feature.Name()) + }) + result += fmt.Sprintf("\n\t%-"+strconv.Itoa(csnl)+"s %s", category.Name()+":", strings.Join(fts, " ")) + }) + + return result default: return v.Title() + ` ` + v.Version() + `-` + v.Revision() + `+` + v.Edition().String() + `@` + v.Platform() + ` ` + v.BuildAt().Format(time.RFC3339) } } +func VersionToMap(v Version) map[string]any { + result := map[string]any{ + "version": v.Version(), + "revision": v.Revision(), + "edition": v.Edition(), + "buildAt": v.BuildAt(), + "vendor": v.Vendor(), + "go": v.GoVersion(), + "platform": v.Platform(), + } + + v.Features().ForEach(func(category VersionFeatureCategory) { + var fts []string + category.ForEach(func(feature VersionFeature) { + fts = append(fts, feature.Name()) + }) + result["features-"+category.Name()] = strings.Join(fts, ",") + }) + + return result +} + type VersionEdition uint8 const ( @@ -75,3 +119,16 @@ const ( VersionFormatShort VersionFormat = iota VersionFormatLong ) + +type VersionFeatures interface { + ForEach(func(VersionFeatureCategory)) +} + +type VersionFeatureCategory interface { + Name() string + ForEach(func(VersionFeature)) +} + +type VersionFeature interface { + Name() string +} diff --git a/pkg/configuration/authorization-htpasswd.go b/pkg/configuration/authorization-htpasswd.go new file mode 100644 index 0000000..1c3d59a --- /dev/null +++ b/pkg/configuration/authorization-htpasswd.go @@ -0,0 +1,88 @@ +package configuration + +import ( + "gopkg.in/yaml.v3" + + "github.com/engity-com/bifroest/pkg/crypto" + "github.com/engity-com/bifroest/pkg/sys" +) + +var ( + DefaultAuthorizationHtpasswdFile = defaultAuthorizationHtpasswdFile + + _ = RegisterAuthorizationV(func() AuthorizationV { + return &AuthorizationHtpasswd{} + }) +) + +type AuthorizationHtpasswd struct { + File crypto.HtpasswdFile `yaml:"file,omitempty"` + Entries crypto.Htpasswd `yaml:"entries,omitempty"` +} + +func (this *AuthorizationHtpasswd) SetDefaults() error { + return setDefaults(this, + func(v *AuthorizationHtpasswd) (string, defaulter) { + return "file", defaulterFunc(func() error { + fn := DefaultAuthorizationHtpasswdFile + var buf crypto.HtpasswdFile + if fn != "" { + if err := buf.Set(fn); err != nil && !sys.IsNotExist(err) { + return err + } + } + v.File = buf + return nil + }) + }, + noopSetDefault[AuthorizationHtpasswd]("entries"), + ) +} + +func (this *AuthorizationHtpasswd) Trim() error { + return trim(this, + noopTrim[AuthorizationHtpasswd]("file"), + noopTrim[AuthorizationHtpasswd]("entries"), + ) +} + +func (this *AuthorizationHtpasswd) Validate() error { + return validate(this, + func(v *AuthorizationHtpasswd) (string, validator) { return "file", &v.File }, + func(v *AuthorizationHtpasswd) (string, validator) { return "entries", &v.Entries }, + ) +} + +func (this *AuthorizationHtpasswd) UnmarshalYAML(node *yaml.Node) error { + return unmarshalYAML(this, node, func(target *AuthorizationHtpasswd, node *yaml.Node) error { + type raw AuthorizationHtpasswd + return node.Decode((*raw)(target)) + }) +} + +func (this AuthorizationHtpasswd) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case AuthorizationHtpasswd: + return this.isEqualTo(&v) + case *AuthorizationHtpasswd: + return this.isEqualTo(v) + default: + return false + } +} + +func (this AuthorizationHtpasswd) isEqualTo(other *AuthorizationHtpasswd) bool { + return isEqual(&this.File, &other.File) && + isEqual(&this.Entries, &other.Entries) +} + +func (this AuthorizationHtpasswd) Types() []string { + return []string{"htpasswd"} +} + +func (this AuthorizationHtpasswd) FeatureFlags() []string { + return []string{"htpasswd"} +} diff --git a/pkg/configuration/authorization-htpasswd_unix.go b/pkg/configuration/authorization-htpasswd_unix.go new file mode 100644 index 0000000..e46b62d --- /dev/null +++ b/pkg/configuration/authorization-htpasswd_unix.go @@ -0,0 +1,7 @@ +//go:build unix + +package configuration + +var ( + defaultAuthorizationHtpasswdFile = `/etc/engity/bifroest/htpasswd` +) diff --git a/pkg/configuration/authorization-htpasswd_windows.go b/pkg/configuration/authorization-htpasswd_windows.go new file mode 100644 index 0000000..60b57cc --- /dev/null +++ b/pkg/configuration/authorization-htpasswd_windows.go @@ -0,0 +1,7 @@ +//go:build windows + +package configuration + +var ( + defaultAuthorizationHtpasswdFile = `C:\ProgramData\Engity\Bifroest\htpasswd` +) diff --git a/pkg/configuration/authorization-local.go b/pkg/configuration/authorization-local.go index ab1d09c..6b6b23d 100644 --- a/pkg/configuration/authorization-local.go +++ b/pkg/configuration/authorization-local.go @@ -1,19 +1,26 @@ +//go:build unix + package configuration import ( - "github.com/engity-com/bifroest/pkg/template" "gopkg.in/yaml.v3" + + "github.com/engity-com/bifroest/pkg/template" ) var ( DefaultAuthorizationLocalPamService = defaultAuthorizationLocalPamService DefaultAuthorizationLocalAuthorizedKeys = []template.String{template.MustNewString("{{.user.homeDir}}/.ssh/authorized_keys")} + + _ = RegisterAuthorizationV(func() AuthorizationV { + return &AuthorizationLocal{} + }) ) type AuthorizationLocal struct { - AuthorizedKeys []template.String `yaml:"authorizedKeys,omitempty"` - Password Password `yaml:"password,omitempty"` - PamService string `yaml:"pamService,omitempty"` + AuthorizedKeys []template.String `yaml:"authorizedKeys,omitempty"` + Password PasswordProperties `yaml:"password,omitempty"` + PamService string `yaml:"pamService,omitempty"` } func (this *AuthorizationLocal) SetDefaults() error { @@ -66,3 +73,7 @@ func (this AuthorizationLocal) isEqualTo(other *AuthorizationLocal) bool { isEqual(&this.Password, &other.Password) && this.PamService == other.PamService } + +func (this AuthorizationLocal) Types() []string { + return []string{"local"} +} diff --git a/pkg/configuration/authorization-local_with_pam.go b/pkg/configuration/authorization-local_with_pam.go index b0c1d36..b82a418 100644 --- a/pkg/configuration/authorization-local_with_pam.go +++ b/pkg/configuration/authorization-local_with_pam.go @@ -1,7 +1,11 @@ -//go:build !cgo || !linux || without_pam +//go:build cgo && linux && !without_pam package configuration var ( - defaultAuthorizationLocalPamService = "" + defaultAuthorizationLocalPamService = "sshd" ) + +func (this AuthorizationLocal) FeatureFlags() []string { + return []string{"local[pam]"} +} diff --git a/pkg/configuration/authorization-local_without_pam.go b/pkg/configuration/authorization-local_without_pam.go index ab537e2..e066802 100644 --- a/pkg/configuration/authorization-local_without_pam.go +++ b/pkg/configuration/authorization-local_without_pam.go @@ -1,7 +1,11 @@ -//go:build cgo && linux && !without_pam +//go:build (!cgo || !linux || without_pam) && unix package configuration var ( - defaultAuthorizationLocalPamService = "sshd" + defaultAuthorizationLocalPamService = "" //nolint:golint,unused ) + +func (this AuthorizationLocal) FeatureFlags() []string { + return []string{"local"} +} diff --git a/pkg/configuration/authorization-oidc-auth.go b/pkg/configuration/authorization-oidc-auth.go index a90ee6a..e3a4c33 100644 --- a/pkg/configuration/authorization-oidc-auth.go +++ b/pkg/configuration/authorization-oidc-auth.go @@ -1,15 +1,20 @@ package configuration import ( + "slices" + "github.com/coreos/go-oidc/v3/oidc" "gopkg.in/yaml.v3" - "slices" ) var ( DefaultAuthorizationOidcScopes = []string{oidc.ScopeOpenID, "profile", "email"} DefaultAuthorizationOidcRetrieveIdToken = true DefaultAuthorizationOidcRetrieveUserInfo = false + + _ = RegisterAuthorizationV(func() AuthorizationV { + return &AuthorizationOidcDeviceAuth{} + }) ) type AuthorizationOidcDeviceAuth struct { @@ -86,3 +91,11 @@ func (this AuthorizationOidcDeviceAuth) isEqualTo(other *AuthorizationOidcDevice this.RetrieveIdToken == other.RetrieveIdToken && this.RetrieveUserInfo == other.RetrieveUserInfo } + +func (this AuthorizationOidcDeviceAuth) Types() []string { + return []string{"oidcDeviceAuth", "oidc-device-auth", "oidc_device_auth"} +} + +func (this AuthorizationOidcDeviceAuth) FeatureFlags() []string { + return []string{"oidcDeviceAuth"} +} diff --git a/pkg/configuration/authorization-simple-entry.go b/pkg/configuration/authorization-simple-entry.go new file mode 100644 index 0000000..3db4e47 --- /dev/null +++ b/pkg/configuration/authorization-simple-entry.go @@ -0,0 +1,120 @@ +package configuration + +import ( + "fmt" + "github.com/engity-com/bifroest/pkg/crypto" + "gopkg.in/yaml.v3" +) + +type AuthorizationSimpleEntry struct { + Name string `yaml:"name"` + AuthorizedKeys crypto.AuthorizedKeys `yaml:"authorizedKeys,omitempty"` + AuthorizedKeysFile crypto.AuthorizedKeysFile `yaml:"authorizedKeysFile,omitempty"` + Password crypto.Password `yaml:"password,omitempty"` +} + +func (this *AuthorizationSimpleEntry) GetField(name string) (any, bool, error) { + switch name { + case "name": + return this.Name, true, nil + default: + return nil, false, fmt.Errorf("unknown field %q", name) + } +} + +func (this *AuthorizationSimpleEntry) SetDefaults() error { + return setDefaults(this, + noopSetDefault[AuthorizationSimpleEntry]("name"), + noopSetDefault[AuthorizationSimpleEntry]("authorizedKeys"), + noopSetDefault[AuthorizationSimpleEntry]("authorizedKeysFile"), + noopSetDefault[AuthorizationSimpleEntry]("password"), + ) +} + +func (this *AuthorizationSimpleEntry) Trim() error { + return trim(this, + func(v *AuthorizationSimpleEntry) (string, trimmer) { return "name", &stringTrimmer{&v.Name} }, + func(v *AuthorizationSimpleEntry) (string, trimmer) { return "authorizedKeys", &v.AuthorizedKeys }, + noopTrim[AuthorizationSimpleEntry]("authorizedKeysFile"), + noopTrim[AuthorizationSimpleEntry]("password"), + ) +} + +func (this *AuthorizationSimpleEntry) Validate() error { + return validate(this, + notEmptyStringValidate[AuthorizationSimpleEntry]("name", func(v *AuthorizationSimpleEntry) *string { return &v.Name }), + func(v *AuthorizationSimpleEntry) (string, validator) { return "authorizedKeys", &v.AuthorizedKeys }, + func(v *AuthorizationSimpleEntry) (string, validator) { + return "authorizedKeysFile", &v.AuthorizedKeysFile + }, + func(v *AuthorizationSimpleEntry) (string, validator) { return "password", &v.Password }, + ) +} + +func (this *AuthorizationSimpleEntry) UnmarshalYAML(node *yaml.Node) error { + return unmarshalYAML(this, node, func(target *AuthorizationSimpleEntry, node *yaml.Node) error { + type raw AuthorizationSimpleEntry + return node.Decode((*raw)(target)) + }) +} + +func (this AuthorizationSimpleEntry) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case AuthorizationSimpleEntry: + return this.isEqualTo(&v) + case *AuthorizationSimpleEntry: + return this.isEqualTo(v) + default: + return false + } +} + +func (this AuthorizationSimpleEntry) isEqualTo(other *AuthorizationSimpleEntry) bool { + return this.Name == other.Name && + isEqual(&this.AuthorizedKeys, &other.AuthorizedKeys) && + isEqual(&this.AuthorizedKeysFile, &other.AuthorizedKeysFile) && + isEqual(&this.Password, &other.Password) +} + +type AuthorizationSimpleEntries []AuthorizationSimpleEntry + +func (this *AuthorizationSimpleEntries) SetDefaults() error { + return setSliceDefaults(this) +} + +func (this *AuthorizationSimpleEntries) Trim() error { + return trimSlice(this) +} + +func (this AuthorizationSimpleEntries) Validate() error { + return validateSlice(this) +} + +func (this AuthorizationSimpleEntries) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case AuthorizationSimpleEntries: + return this.isEqualTo(&v) + case *AuthorizationSimpleEntries: + return this.isEqualTo(v) + default: + return false + } +} + +func (this AuthorizationSimpleEntries) isEqualTo(other *AuthorizationSimpleEntries) bool { + if len(this) != len(*other) { + return false + } + for i, tv := range this { + if !tv.IsEqualTo((*other)[i]) { + return false + } + } + return true +} diff --git a/pkg/configuration/authorization-simple.go b/pkg/configuration/authorization-simple.go new file mode 100644 index 0000000..9a5bc66 --- /dev/null +++ b/pkg/configuration/authorization-simple.go @@ -0,0 +1,66 @@ +package configuration + +import ( + "gopkg.in/yaml.v3" +) + +var ( + _ = RegisterAuthorizationV(func() AuthorizationV { + return &AuthorizationSimple{} + }) +) + +type AuthorizationSimple struct { + Entries AuthorizationSimpleEntries `yaml:"entries,omitempty"` +} + +func (this *AuthorizationSimple) SetDefaults() error { + return setDefaults(this, + func(v *AuthorizationSimple) (string, defaulter) { return "entries", &v.Entries }, + ) +} + +func (this *AuthorizationSimple) Trim() error { + return trim(this, + func(v *AuthorizationSimple) (string, trimmer) { return "entries", &v.Entries }, + ) +} + +func (this *AuthorizationSimple) Validate() error { + return validate(this, + func(v *AuthorizationSimple) (string, validator) { return "entries", &v.Entries }, + ) +} + +func (this *AuthorizationSimple) UnmarshalYAML(node *yaml.Node) error { + return unmarshalYAML(this, node, func(target *AuthorizationSimple, node *yaml.Node) error { + type raw AuthorizationSimple + return node.Decode((*raw)(target)) + }) +} + +func (this AuthorizationSimple) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case AuthorizationSimple: + return this.isEqualTo(&v) + case *AuthorizationSimple: + return this.isEqualTo(v) + default: + return false + } +} + +func (this AuthorizationSimple) isEqualTo(other *AuthorizationSimple) bool { + return isEqual(&this.Entries, &other.Entries) +} + +func (this AuthorizationSimple) Types() []string { + return []string{"simple"} +} + +func (this AuthorizationSimple) FeatureFlags() []string { + return []string{"simple"} +} diff --git a/pkg/configuration/authorization.go b/pkg/configuration/authorization.go index 385e314..0d1905c 100644 --- a/pkg/configuration/authorization.go +++ b/pkg/configuration/authorization.go @@ -2,9 +2,10 @@ package configuration import ( "fmt" - "gopkg.in/yaml.v3" - "reflect" + "sort" "strings" + + "gopkg.in/yaml.v3" ) type Authorization struct { @@ -16,13 +17,32 @@ type AuthorizationV interface { trimmer validator equaler + Types() []string + FeatureFlags() []string } -func (this *Authorization) SetDefaults() error { - *this = Authorization{&AuthorizationLocal{}} - if err := this.V.SetDefaults(); err != nil { - return err +var ( + typeToAuthorizationFactory = make(map[string]AuthorizationVFactory) + authorizationVs []AuthorizationV +) + +type AuthorizationVFactory func() AuthorizationV + +func RegisterAuthorizationV(factory AuthorizationVFactory) AuthorizationVFactory { + pt := factory() + ts := pt.Types() + if len(ts) == 0 { + panic(fmt.Errorf("the instance does not provide any type")) + } + for _, t := range ts { + typeToAuthorizationFactory[strings.ToLower(t)] = factory } + authorizationVs = append(authorizationVs, pt) + return factory +} + +func (this *Authorization) SetDefaults() error { + *this = Authorization{} return nil } @@ -55,17 +75,16 @@ func (this *Authorization) UnmarshalYAML(node *yaml.Node) error { return reportYamlRelatedErr(node, err) } - switch strings.ToLower(typeBuf.Type) { - case "": + if typeBuf.Type == "" { return reportYamlRelatedErrf(node, "[type] required but absent") - case "oidc-device-auth", "oidc_device_auth", "oidcdeviceauth": - this.V = &AuthorizationOidcDeviceAuth{} - case "local": - this.V = &AuthorizationLocal{} - default: + } + + factory, ok := typeToAuthorizationFactory[strings.ToLower(typeBuf.Type)] + if !ok { return reportYamlRelatedErrf(node, "[type] illegal type: %q", typeBuf.Type) } + this.V = factory() if err := node.Decode(this.V); err != nil { return reportYamlRelatedErr(node, err) } @@ -76,18 +95,14 @@ func (this *Authorization) UnmarshalYAML(node *yaml.Node) error { func (this *Authorization) MarshalYAML() (any, error) { typeBuf := struct { AuthorizationV `yaml:",inline"` - Type string `yaml:"type"` + Type string `yaml:"type,omitempty"` }{ AuthorizationV: this.V, } - switch typeBuf.AuthorizationV.(type) { - case *AuthorizationOidcDeviceAuth: - typeBuf.Type = "oidcDeviceAuth" - case *AuthorizationLocal: - typeBuf.Type = "local" - default: - return nil, fmt.Errorf("[type] illegal type: %v", reflect.TypeOf(typeBuf.AuthorizationV)) + if this.V != nil { + typeBuf.Type = this.V.Types()[0] + typeBuf.AuthorizationV = this.V } return typeBuf, nil @@ -113,3 +128,12 @@ func (this Authorization) isEqualTo(other *Authorization) bool { } return this.V.IsEqualTo(other.V) } + +func GetSupportedAuthorizationFeatureFlags() []string { + var result []string + for _, v := range authorizationVs { + result = append(result, v.FeatureFlags()...) + } + sort.Strings(result) + return result +} diff --git a/pkg/configuration/authorization_test.go b/pkg/configuration/authorization_test.go index 18d0472..7cb3bf7 100644 --- a/pkg/configuration/authorization_test.go +++ b/pkg/configuration/authorization_test.go @@ -1,8 +1,9 @@ package configuration import ( - "github.com/echocat/slf4g/sdk/testlog" "testing" + + "github.com/echocat/slf4g/sdk/testlog" ) func TestAuthorization_UnmarshalYAML(t *testing.T) { diff --git a/pkg/configuration/configuration-ref.go b/pkg/configuration/configuration-ref.go index 461cba7..97b959a 100644 --- a/pkg/configuration/configuration-ref.go +++ b/pkg/configuration/configuration-ref.go @@ -1,9 +1,8 @@ package configuration type ConfigurationRef struct { - v Configuration - fn string - loadErr error + v Configuration + fn string } func (this ConfigurationRef) IsZero() bool { diff --git a/pkg/configuration/configuration.go b/pkg/configuration/configuration.go index 01b5787..a2f3ce1 100644 --- a/pkg/configuration/configuration.go +++ b/pkg/configuration/configuration.go @@ -1,12 +1,14 @@ package configuration import ( + "io" + "os" + + "gopkg.in/yaml.v3" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/errors" "github.com/engity-com/bifroest/pkg/sys" - "gopkg.in/yaml.v3" - "io" - "os" ) type Configuration struct { diff --git a/pkg/configuration/configuration_test.go b/pkg/configuration/configuration_unix_test.go similarity index 99% rename from pkg/configuration/configuration_test.go rename to pkg/configuration/configuration_unix_test.go index df12824..52d452a 100644 --- a/pkg/configuration/configuration_test.go +++ b/pkg/configuration/configuration_unix_test.go @@ -1,11 +1,15 @@ +//go:build unix + package configuration import ( + "testing" + "github.com/echocat/slf4g/sdk/testlog" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/crypto" "github.com/engity-com/bifroest/pkg/template" - "testing" ) func TestConfiguration_UnmarshalYAML(t *testing.T) { @@ -31,7 +35,7 @@ func TestConfiguration_UnmarshalYAML(t *testing.T) { name: "required-set", yaml: `flows: - name: foo - authorization: + authorization: type: oidcDeviceAuth issuer: https://foo-bar clientId: anId diff --git a/pkg/configuration/configuration_windows_test.go b/pkg/configuration/configuration_windows_test.go new file mode 100644 index 0000000..0cb0664 --- /dev/null +++ b/pkg/configuration/configuration_windows_test.go @@ -0,0 +1,100 @@ +//go:build windows + +package configuration + +import ( + "testing" + + "github.com/echocat/slf4g/sdk/testlog" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/crypto" +) + +func TestConfiguration_UnmarshalYAML(t *testing.T) { + testlog.Hook(t) + + runUnmarshalYamlTests(t, + unmarshalYamlTestCase[Configuration]{ + name: "empty", + yaml: ``, + expectedError: `EOF`, + }, + unmarshalYamlTestCase[Configuration]{ + name: "flows-missing", + yaml: `{}`, + expectedError: `[flows] required but absent`, + }, + unmarshalYamlTestCase[Configuration]{ + name: "flows-empty", + yaml: `flows: []`, + expectedError: `[flows] required but absent`, + }, + unmarshalYamlTestCase[Configuration]{ + name: "required-set", + yaml: `flows: +- name: foo + authorization: + type: oidcDeviceAuth + issuer: https://foo-bar + clientId: anId + clientSecret: aSecret + environment: + type: local`, + expected: Configuration{ + Ssh: Ssh{ + Addresses: DefaultSshAddresses, + Keys: Keys{ + HostKeys: DefaultHostKeyLocations, + RsaRestriction: crypto.DefaultRsaRestriction, + DsaRestriction: crypto.DefaultDsaRestriction, + EcdsaRestriction: crypto.DefaultEcdsaRestriction, + Ed25519Restriction: crypto.DefaultEd25519Restriction, + RememberMeNotification: DefaultRememberMeNotification, + }, + IdleTimeout: DefaultSshIdleTimeout, + MaxTimeout: DefaultSshMaxTimeout, + MaxAuthTries: DefaultSshMaxAuthTries, + MaxConnections: DefaultSshMaxConnections, + Banner: DefaultSshBanner, + }, + Session: Session{&SessionFs{ + IdleTimeout: DefaultSessionIdleTimeout, + MaxTimeout: DefaultSessionMaxTimeout, + MaxConnections: DefaultSessionMaxConnections, + Storage: DefaultSessionFsStorage, + FileMode: DefaultSessionFsFileMode, + }}, + Flows: []Flow{{ + Name: "foo", + Requirement: Requirement{ + IncludedRequestingName: common.MustNewRegexp(""), + ExcludedRequestingName: common.MustNewRegexp(""), + }, + Authorization: Authorization{&AuthorizationOidcDeviceAuth{ + Issuer: "https://foo-bar", + ClientId: "anId", + ClientSecret: "aSecret", + Scopes: DefaultAuthorizationOidcScopes, + RetrieveIdToken: DefaultAuthorizationOidcRetrieveIdToken, + RetrieveUserInfo: DefaultAuthorizationOidcRetrieveUserInfo, + }}, + Environment: Environment{&EnvironmentLocal{ + LoginAllowed: DefaultEnvironmentLocalLoginAllowed, + Banner: DefaultEnvironmentLocalBanner, + ShellCommand: DefaultEnvironmentLocalShellCommand, + ExecCommandPrefix: DefaultEnvironmentLocalExecCommandPrefix, + Directory: DefaultEnvironmentLocalDirectory, + PortForwardingAllowed: DefaultEnvironmentLocalPortForwardingAllowed, + }}, + }}, + HouseKeeping: HouseKeeping{ + Every: DefaultHouseKeepingEvery, + InitialDelay: DefaultHouseKeepingInitialDelay, + AutoRepair: DefaultHouseKeepingAutoRepair, + KeepExpiredFor: DefaultHouseKeepingKeepExpiredFor, + }, + }, + }, + ) +} diff --git a/pkg/configuration/environment-local-dispose.go b/pkg/configuration/environment-local-dispose.go index f96add6..bb5577b 100644 --- a/pkg/configuration/environment-local-dispose.go +++ b/pkg/configuration/environment-local-dispose.go @@ -1,8 +1,11 @@ +//go:build unix + package configuration import ( - "github.com/engity-com/bifroest/pkg/template" "gopkg.in/yaml.v3" + + "github.com/engity-com/bifroest/pkg/template" ) var ( diff --git a/pkg/configuration/environment-local.go b/pkg/configuration/environment-local.go index 39c8280..e51ecd9 100644 --- a/pkg/configuration/environment-local.go +++ b/pkg/configuration/environment-local.go @@ -1,107 +1,13 @@ package configuration -import ( - "github.com/engity-com/bifroest/pkg/template" - "gopkg.in/yaml.v3" -) +import "github.com/engity-com/bifroest/pkg/template" var ( DefaultEnvironmentLocalLoginAllowed = template.BoolOf(true) - DefaultEnvironmentLocalCreateIfAbsent = template.BoolOf(false) - DefaultEnvironmentLocalUpdateIfDifferent = template.BoolOf(false) DefaultEnvironmentLocalBanner = template.MustNewString("") DefaultEnvironmentLocalPortForwardingAllowed = template.BoolOf(true) -) - -type EnvironmentLocal struct { - User UserRequirementTemplate `yaml:",inline"` - - LoginAllowed template.Bool `yaml:"loginAllowed,omitempty"` - - CreateIfAbsent template.Bool `yaml:"createIfAbsent,omitempty"` - UpdateIfDifferent template.Bool `yaml:"updateIfDifferent,omitempty"` - Dispose EnvironmentLocalDispose `yaml:"dispose"` - - Banner template.String `yaml:"banner,omitempty"` - - PortForwardingAllowed template.Bool `yaml:"portForwardingAllowed,omitempty"` -} - -func (this *EnvironmentLocal) SetDefaults() error { - return setDefaults(this, - func(v *EnvironmentLocal) (string, defaulter) { return "", &v.User }, - - fixedDefault("loginAllowed", func(v *EnvironmentLocal) *template.Bool { return &v.LoginAllowed }, DefaultEnvironmentLocalLoginAllowed), - - fixedDefault("createIfAbsent", func(v *EnvironmentLocal) *template.Bool { return &v.CreateIfAbsent }, DefaultEnvironmentLocalCreateIfAbsent), - fixedDefault("updateIfDifferent", func(v *EnvironmentLocal) *template.Bool { return &v.UpdateIfDifferent }, DefaultEnvironmentLocalUpdateIfDifferent), - func(v *EnvironmentLocal) (string, defaulter) { return "dispose", &v.Dispose }, - - fixedDefault("banner", func(v *EnvironmentLocal) *template.String { return &v.Banner }, DefaultEnvironmentLocalBanner), - - fixedDefault("portForwardingAllowed", func(v *EnvironmentLocal) *template.Bool { return &v.PortForwardingAllowed }, DefaultEnvironmentLocalPortForwardingAllowed), - ) -} - -func (this *EnvironmentLocal) Trim() error { - return trim(this, - func(v *EnvironmentLocal) (string, trimmer) { return "", &v.User }, - noopTrim[EnvironmentLocal]("loginAllowed"), - - noopTrim[EnvironmentLocal]("createIfAbsent"), - noopTrim[EnvironmentLocal]("updateIfDifferent"), - func(v *EnvironmentLocal) (string, trimmer) { return "dispose", &v.Dispose }, - - noopTrim[EnvironmentLocal]("banner"), - - noopTrim[EnvironmentLocal]("portForwardingAllowed"), - ) -} - -func (this *EnvironmentLocal) Validate() error { - return validate(this, - func(v *EnvironmentLocal) (string, validator) { return "", &v.User }, - - noopValidate[EnvironmentLocal]("loginAllowed"), - - noopValidate[EnvironmentLocal]("createIfAbsent"), - noopValidate[EnvironmentLocal]("updateIfDifferent"), - func(v *EnvironmentLocal) (string, validator) { return "dispose", &v.Dispose }, - - noopValidate[EnvironmentLocal]("banner"), - - noopValidate[EnvironmentLocal]("portForwardingAllowed"), - ) -} - -func (this *EnvironmentLocal) UnmarshalYAML(node *yaml.Node) error { - return unmarshalYAML(this, node, func(target *EnvironmentLocal, node *yaml.Node) error { - type raw EnvironmentLocal - return node.Decode((*raw)(target)) + _ = RegisterEnvironmentV(func() EnvironmentV { + return &EnvironmentLocal{} }) -} - -func (this EnvironmentLocal) IsEqualTo(other any) bool { - if other == nil { - return false - } - switch v := other.(type) { - case EnvironmentLocal: - return this.isEqualTo(&v) - case *EnvironmentLocal: - return this.isEqualTo(v) - default: - return false - } -} - -func (this EnvironmentLocal) isEqualTo(other *EnvironmentLocal) bool { - return isEqual(&this.User, &other.User) && - isEqual(&this.LoginAllowed, &other.LoginAllowed) && - isEqual(&this.CreateIfAbsent, &other.CreateIfAbsent) && - isEqual(&this.UpdateIfDifferent, &other.UpdateIfDifferent) && - isEqual(&this.Dispose, &other.Dispose) && - isEqual(&this.Banner, &other.Banner) && - isEqual(&this.PortForwardingAllowed, &other.PortForwardingAllowed) -} +) diff --git a/pkg/configuration/environment-local_unix.go b/pkg/configuration/environment-local_unix.go new file mode 100644 index 0000000..e5c97f7 --- /dev/null +++ b/pkg/configuration/environment-local_unix.go @@ -0,0 +1,115 @@ +//go:build unix + +package configuration + +import ( + "gopkg.in/yaml.v3" + + "github.com/engity-com/bifroest/pkg/template" +) + +var ( + DefaultEnvironmentLocalCreateIfAbsent = template.BoolOf(false) + DefaultEnvironmentLocalUpdateIfDifferent = template.BoolOf(false) +) + +type EnvironmentLocal struct { + User UserRequirementTemplate `yaml:",inline"` + + LoginAllowed template.Bool `yaml:"loginAllowed,omitempty"` + + CreateIfAbsent template.Bool `yaml:"createIfAbsent,omitempty"` + UpdateIfDifferent template.Bool `yaml:"updateIfDifferent,omitempty"` + Dispose EnvironmentLocalDispose `yaml:"dispose"` + + Banner template.String `yaml:"banner,omitempty"` + + PortForwardingAllowed template.Bool `yaml:"portForwardingAllowed,omitempty"` +} + +func (this *EnvironmentLocal) SetDefaults() error { + return setDefaults(this, + func(v *EnvironmentLocal) (string, defaulter) { return "", &v.User }, + + fixedDefault("loginAllowed", func(v *EnvironmentLocal) *template.Bool { return &v.LoginAllowed }, DefaultEnvironmentLocalLoginAllowed), + + fixedDefault("createIfAbsent", func(v *EnvironmentLocal) *template.Bool { return &v.CreateIfAbsent }, DefaultEnvironmentLocalCreateIfAbsent), + fixedDefault("updateIfDifferent", func(v *EnvironmentLocal) *template.Bool { return &v.UpdateIfDifferent }, DefaultEnvironmentLocalUpdateIfDifferent), + func(v *EnvironmentLocal) (string, defaulter) { return "dispose", &v.Dispose }, + + fixedDefault("banner", func(v *EnvironmentLocal) *template.String { return &v.Banner }, DefaultEnvironmentLocalBanner), + + fixedDefault("portForwardingAllowed", func(v *EnvironmentLocal) *template.Bool { return &v.PortForwardingAllowed }, DefaultEnvironmentLocalPortForwardingAllowed), + ) +} + +func (this *EnvironmentLocal) Trim() error { + return trim(this, + func(v *EnvironmentLocal) (string, trimmer) { return "", &v.User }, + + noopTrim[EnvironmentLocal]("loginAllowed"), + + noopTrim[EnvironmentLocal]("createIfAbsent"), + noopTrim[EnvironmentLocal]("updateIfDifferent"), + func(v *EnvironmentLocal) (string, trimmer) { return "dispose", &v.Dispose }, + + noopTrim[EnvironmentLocal]("banner"), + + noopTrim[EnvironmentLocal]("portForwardingAllowed"), + ) +} + +func (this *EnvironmentLocal) Validate() error { + return validate(this, + func(v *EnvironmentLocal) (string, validator) { return "", &v.User }, + + noopValidate[EnvironmentLocal]("loginAllowed"), + + noopValidate[EnvironmentLocal]("createIfAbsent"), + noopValidate[EnvironmentLocal]("updateIfDifferent"), + func(v *EnvironmentLocal) (string, validator) { return "dispose", &v.Dispose }, + + noopValidate[EnvironmentLocal]("banner"), + + noopValidate[EnvironmentLocal]("portForwardingAllowed"), + ) +} + +func (this *EnvironmentLocal) UnmarshalYAML(node *yaml.Node) error { + return unmarshalYAML(this, node, func(target *EnvironmentLocal, node *yaml.Node) error { + type raw EnvironmentLocal + return node.Decode((*raw)(target)) + }) +} + +func (this EnvironmentLocal) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case EnvironmentLocal: + return this.isEqualTo(&v) + case *EnvironmentLocal: + return this.isEqualTo(v) + default: + return false + } +} + +func (this EnvironmentLocal) isEqualTo(other *EnvironmentLocal) bool { + return isEqual(&this.User, &other.User) && + isEqual(&this.LoginAllowed, &other.LoginAllowed) && + isEqual(&this.CreateIfAbsent, &other.CreateIfAbsent) && + isEqual(&this.UpdateIfDifferent, &other.UpdateIfDifferent) && + isEqual(&this.Dispose, &other.Dispose) && + isEqual(&this.Banner, &other.Banner) && + isEqual(&this.PortForwardingAllowed, &other.PortForwardingAllowed) +} + +func (this EnvironmentLocal) Types() []string { + return []string{"local"} +} + +func (this EnvironmentLocal) FeatureFlags() []string { + return []string{"local[pty,impersonate]"} +} diff --git a/pkg/configuration/environment-local_windows.go b/pkg/configuration/environment-local_windows.go new file mode 100644 index 0000000..cf1a607 --- /dev/null +++ b/pkg/configuration/environment-local_windows.go @@ -0,0 +1,114 @@ +//go:build windows + +package configuration + +import ( + "os" + "os/user" + + "gopkg.in/yaml.v3" + + "github.com/engity-com/bifroest/pkg/template" +) + +var ( + DefaultShell = func() string { + v, ok := os.LookupEnv("COMSPEC") + if ok { + return v + } + return `C:\WINDOWS\system32\cmd.exe` + }() + + DefaultEnvironmentLocalShellCommand = template.MustNewStrings(DefaultShell) + DefaultEnvironmentLocalExecCommandPrefix = template.MustNewStrings(DefaultShell, "/C") + DefaultEnvironmentLocalDirectory = template.MustNewString(func() string { + u, err := user.Current() + if err == nil && u.HomeDir != "" { + return u.HomeDir + } + return "" + }()) +) + +type EnvironmentLocal struct { + LoginAllowed template.Bool `yaml:"loginAllowed,omitempty"` + + Banner template.String `yaml:"banner,omitempty"` + + ShellCommand template.Strings `yaml:"shellCommand,omitempty"` + ExecCommandPrefix template.Strings `yaml:"execCommandPrefix,omitempty"` + Directory template.String `yaml:"directory,omitempty"` + PortForwardingAllowed template.Bool `yaml:"portForwardingAllowed,omitempty"` +} + +func (this *EnvironmentLocal) SetDefaults() error { + return setDefaults(this, + fixedDefault("loginAllowed", func(v *EnvironmentLocal) *template.Bool { return &v.LoginAllowed }, DefaultEnvironmentLocalLoginAllowed), + fixedDefault("banner", func(v *EnvironmentLocal) *template.String { return &v.Banner }, DefaultEnvironmentLocalBanner), + fixedDefault("shellCommand", func(v *EnvironmentLocal) *template.Strings { return &v.ShellCommand }, DefaultEnvironmentLocalShellCommand), + fixedDefault("execCommandPrefix", func(v *EnvironmentLocal) *template.Strings { return &v.ExecCommandPrefix }, DefaultEnvironmentLocalExecCommandPrefix), + fixedDefault("directory", func(v *EnvironmentLocal) *template.String { return &v.Directory }, DefaultEnvironmentLocalDirectory), + fixedDefault("portForwardingAllowed", func(v *EnvironmentLocal) *template.Bool { return &v.PortForwardingAllowed }, DefaultEnvironmentLocalPortForwardingAllowed), + ) +} + +func (this *EnvironmentLocal) Trim() error { + return trim(this, + noopTrim[EnvironmentLocal]("loginAllowed"), + noopTrim[EnvironmentLocal]("banner"), + noopTrim[EnvironmentLocal]("shellCommand"), + noopTrim[EnvironmentLocal]("execCommandPrefix"), + noopTrim[EnvironmentLocal]("directory"), + noopTrim[EnvironmentLocal]("portForwardingAllowed"), + ) +} + +func (this *EnvironmentLocal) Validate() error { + return validate(this, + noopValidate[EnvironmentLocal]("loginAllowed"), + noopValidate[EnvironmentLocal]("banner"), + noopValidate[EnvironmentLocal]("shellCommand"), + noopValidate[EnvironmentLocal]("execCommandPrefix"), + noopValidate[EnvironmentLocal]("directory"), + noopValidate[EnvironmentLocal]("portForwardingAllowed"), + ) +} + +func (this *EnvironmentLocal) UnmarshalYAML(node *yaml.Node) error { + return unmarshalYAML(this, node, func(target *EnvironmentLocal, node *yaml.Node) error { + type raw EnvironmentLocal + return node.Decode((*raw)(target)) + }) +} + +func (this EnvironmentLocal) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case EnvironmentLocal: + return this.isEqualTo(&v) + case *EnvironmentLocal: + return this.isEqualTo(v) + default: + return false + } +} + +func (this EnvironmentLocal) isEqualTo(other *EnvironmentLocal) bool { + return isEqual(&this.LoginAllowed, &other.LoginAllowed) && + isEqual(&this.Banner, &other.Banner) && + isEqual(&this.ShellCommand, &other.ShellCommand) && + isEqual(&this.ExecCommandPrefix, &other.ExecCommandPrefix) && + isEqual(&this.Directory, &other.Directory) && + isEqual(&this.PortForwardingAllowed, &other.PortForwardingAllowed) +} + +func (this EnvironmentLocal) Types() []string { + return []string{"local"} +} + +func (this EnvironmentLocal) FeatureFlags() []string { + return []string{"local"} +} diff --git a/pkg/configuration/environment.go b/pkg/configuration/environment.go index f2250df..bb1ab6c 100644 --- a/pkg/configuration/environment.go +++ b/pkg/configuration/environment.go @@ -2,8 +2,10 @@ package configuration import ( "fmt" + "sort" + "strings" + "gopkg.in/yaml.v3" - "reflect" ) type Environment struct { @@ -15,13 +17,32 @@ type EnvironmentV interface { trimmer validator equaler + Types() []string + FeatureFlags() []string } -func (this *Environment) SetDefaults() error { - *this = Environment{&EnvironmentLocal{}} - if err := this.V.SetDefaults(); err != nil { - return err +var ( + typeToEnvironmentFactory = make(map[string]EnvironmentVFactory) + environmentVs []EnvironmentV +) + +type EnvironmentVFactory func() EnvironmentV + +func RegisterEnvironmentV(factory EnvironmentVFactory) EnvironmentVFactory { + pt := factory() + ts := pt.Types() + if len(ts) == 0 { + panic(fmt.Errorf("the instance does not provide any type")) } + for _, t := range ts { + typeToEnvironmentFactory[strings.ToLower(t)] = factory + } + environmentVs = append(environmentVs, pt) + return factory +} + +func (this *Environment) SetDefaults() error { + *this = Environment{} return nil } @@ -54,15 +75,16 @@ func (this *Environment) UnmarshalYAML(node *yaml.Node) error { return reportYamlRelatedErr(node, err) } - switch typeBuf.Type { - case "": + if typeBuf.Type == "" { return reportYamlRelatedErrf(node, "[type] required but absent") - case "local": - this.V = &EnvironmentLocal{} - default: + } + + factory, ok := typeToEnvironmentFactory[strings.ToLower(typeBuf.Type)] + if !ok { return reportYamlRelatedErrf(node, "[type] illegal type: %q", typeBuf.Type) } + this.V = factory() if err := node.Decode(this.V); err != nil { return reportYamlRelatedErr(node, err) } @@ -73,16 +95,12 @@ func (this *Environment) UnmarshalYAML(node *yaml.Node) error { func (this *Environment) MarshalYAML() (any, error) { typeBuf := struct { EnvironmentV `yaml:",inline"` - Type string `yaml:"type"` - }{ - EnvironmentV: this.V, - } + Type string `yaml:"type,omitempty"` + }{} - switch typeBuf.EnvironmentV.(type) { - case *EnvironmentLocal: - typeBuf.Type = "local" - default: - return nil, fmt.Errorf("[type] illegal type: %v", reflect.TypeOf(typeBuf.EnvironmentV)) + if this.V != nil { + typeBuf.Type = this.V.Types()[0] + typeBuf.EnvironmentV = this.V } return typeBuf, nil @@ -108,3 +126,12 @@ func (this Environment) isEqualTo(other *Environment) bool { } return this.V.IsEqualTo(other.V) } + +func GetSupportedEnvironmentFeatureFlags() []string { + var result []string + for _, v := range environmentVs { + result = append(result, v.FeatureFlags()...) + } + sort.Strings(result) + return result +} diff --git a/pkg/configuration/group-requirement-template_linux.go b/pkg/configuration/group-requirement-template.go similarity index 99% rename from pkg/configuration/group-requirement-template_linux.go rename to pkg/configuration/group-requirement-template.go index 604288b..8de77cd 100644 --- a/pkg/configuration/group-requirement-template_linux.go +++ b/pkg/configuration/group-requirement-template.go @@ -1,13 +1,15 @@ -//go:build linux +//go:build unix package configuration import ( "fmt" + + "gopkg.in/yaml.v3" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/template" "github.com/engity-com/bifroest/pkg/user" - "gopkg.in/yaml.v3" ) var ( diff --git a/pkg/configuration/housekeeping.go b/pkg/configuration/housekeeping.go index fafc81d..9fe9651 100644 --- a/pkg/configuration/housekeeping.go +++ b/pkg/configuration/housekeeping.go @@ -1,9 +1,11 @@ package configuration import ( - "github.com/engity-com/bifroest/pkg/common" - "gopkg.in/yaml.v3" "time" + + "gopkg.in/yaml.v3" + + "github.com/engity-com/bifroest/pkg/common" ) var ( diff --git a/pkg/configuration/keys.go b/pkg/configuration/keys.go index 2fe2f39..7918e86 100644 --- a/pkg/configuration/keys.go +++ b/pkg/configuration/keys.go @@ -1,10 +1,12 @@ package configuration import ( + "slices" + + "gopkg.in/yaml.v3" + "github.com/engity-com/bifroest/pkg/crypto" "github.com/engity-com/bifroest/pkg/template" - "gopkg.in/yaml.v3" - "slices" ) var ( diff --git a/pkg/configuration/ssh_linux.go b/pkg/configuration/keys_unix.go similarity index 83% rename from pkg/configuration/ssh_linux.go rename to pkg/configuration/keys_unix.go index 51fb60b..cdfbc01 100644 --- a/pkg/configuration/ssh_linux.go +++ b/pkg/configuration/keys_unix.go @@ -1,4 +1,4 @@ -//go:build linux +//go:build unix package configuration diff --git a/pkg/configuration/keys_windows.go b/pkg/configuration/keys_windows.go new file mode 100644 index 0000000..442a15c --- /dev/null +++ b/pkg/configuration/keys_windows.go @@ -0,0 +1,7 @@ +//go:build windows + +package configuration + +const ( + DefaultHostKeyLocation = `C:\ProgramData\Engity\Bifroest\key` +) diff --git a/pkg/configuration/password-properties.go b/pkg/configuration/password-properties.go new file mode 100644 index 0000000..a71f18e --- /dev/null +++ b/pkg/configuration/password-properties.go @@ -0,0 +1,70 @@ +package configuration + +import ( + "gopkg.in/yaml.v3" + + "github.com/engity-com/bifroest/pkg/template" +) + +var ( + DefaultPasswordAllowed = template.BoolOf(true) + DefaultPasswordInteractiveAllowed = template.BoolOf(true) + DefaultPasswordEmptyAllowed = template.BoolOf(false) +) + +type PasswordProperties struct { + Allowed template.Bool `yaml:"allowed"` + InteractiveAllowed template.Bool `yaml:"interactiveAllowed"` + EmptyAllowed template.Bool `yaml:"emptyAllowed"` +} + +func (this *PasswordProperties) SetDefaults() error { + return setDefaults(this, + fixedDefault("allowed", func(v *PasswordProperties) *template.Bool { return &v.Allowed }, DefaultPasswordAllowed), + fixedDefault("interactiveAllowed", func(v *PasswordProperties) *template.Bool { return &v.InteractiveAllowed }, DefaultPasswordInteractiveAllowed), + fixedDefault("emptyAllowed", func(v *PasswordProperties) *template.Bool { return &v.EmptyAllowed }, DefaultPasswordEmptyAllowed), + ) +} + +func (this *PasswordProperties) Trim() error { + return trim(this, + noopTrim[PasswordProperties]("allowed"), + noopTrim[PasswordProperties]("interactiveAllowed"), + noopTrim[PasswordProperties]("emptyAllowed"), + ) +} + +func (this *PasswordProperties) Validate() error { + return validate(this, + noopValidate[PasswordProperties]("allowed"), + noopValidate[PasswordProperties]("interactiveAllowed"), + noopValidate[PasswordProperties]("emptyAllowed"), + ) +} + +func (this *PasswordProperties) UnmarshalYAML(node *yaml.Node) error { + return unmarshalYAML(this, node, func(target *PasswordProperties, node *yaml.Node) error { + type raw PasswordProperties + return node.Decode((*raw)(target)) + }) +} + +func (this PasswordProperties) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case PasswordProperties: + return this.isEqualTo(&v) + case *PasswordProperties: + return this.isEqualTo(v) + default: + return false + } +} + +func (this PasswordProperties) isEqualTo(other *PasswordProperties) bool { + return isEqual(&this.Allowed, &other.Allowed) && + isEqual(&this.InteractiveAllowed, &other.InteractiveAllowed) && + isEqual(&this.EmptyAllowed, &other.EmptyAllowed) +} diff --git a/pkg/configuration/password.go b/pkg/configuration/password.go deleted file mode 100644 index f2bfcb8..0000000 --- a/pkg/configuration/password.go +++ /dev/null @@ -1,69 +0,0 @@ -package configuration - -import ( - "github.com/engity-com/bifroest/pkg/template" - "gopkg.in/yaml.v3" -) - -var ( - DefaultPasswordAllowed = template.BoolOf(true) - DefaultPasswordInteractiveAllowed = template.BoolOf(true) - DefaultPasswordEmptyAllowed = template.BoolOf(false) -) - -type Password struct { - Allowed template.Bool `yaml:"allowed"` - InteractiveAllowed template.Bool `yaml:"interactiveAllowed"` - EmptyAllowed template.Bool `yaml:"emptyAllowed"` -} - -func (this *Password) SetDefaults() error { - return setDefaults(this, - fixedDefault("allowed", func(v *Password) *template.Bool { return &v.Allowed }, DefaultPasswordAllowed), - fixedDefault("interactiveAllowed", func(v *Password) *template.Bool { return &v.InteractiveAllowed }, DefaultPasswordInteractiveAllowed), - fixedDefault("emptyAllowed", func(v *Password) *template.Bool { return &v.EmptyAllowed }, DefaultPasswordEmptyAllowed), - ) -} - -func (this *Password) Trim() error { - return trim(this, - noopTrim[Password]("allowed"), - noopTrim[Password]("interactiveAllowed"), - noopTrim[Password]("emptyAllowed"), - ) -} - -func (this *Password) Validate() error { - return validate(this, - noopValidate[Password]("allowed"), - noopValidate[Password]("interactiveAllowed"), - noopValidate[Password]("emptyAllowed"), - ) -} - -func (this *Password) UnmarshalYAML(node *yaml.Node) error { - return unmarshalYAML(this, node, func(target *Password, node *yaml.Node) error { - type raw Password - return node.Decode((*raw)(target)) - }) -} - -func (this Password) IsEqualTo(other any) bool { - if other == nil { - return false - } - switch v := other.(type) { - case Password: - return this.isEqualTo(&v) - case *Password: - return this.isEqualTo(v) - default: - return false - } -} - -func (this Password) isEqualTo(other *Password) bool { - return isEqual(&this.Allowed, &other.Allowed) && - isEqual(&this.InteractiveAllowed, &other.InteractiveAllowed) && - isEqual(&this.EmptyAllowed, &other.EmptyAllowed) -} diff --git a/pkg/configuration/requirement.go b/pkg/configuration/requirement.go index 16752de..6f1742a 100644 --- a/pkg/configuration/requirement.go +++ b/pkg/configuration/requirement.go @@ -1,8 +1,9 @@ package configuration import ( - "github.com/engity-com/bifroest/pkg/common" "gopkg.in/yaml.v3" + + "github.com/engity-com/bifroest/pkg/common" ) var ( diff --git a/pkg/configuration/session-fs.go b/pkg/configuration/session-fs.go index 6134fe5..51328d3 100644 --- a/pkg/configuration/session-fs.go +++ b/pkg/configuration/session-fs.go @@ -1,9 +1,10 @@ package configuration import ( + "gopkg.in/yaml.v3" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/sys" - "gopkg.in/yaml.v3" ) var ( @@ -11,6 +12,10 @@ var ( DefaultSessionFsStorage = defaultSessionFsStorage // DefaultSessionFsFileMode is the default setting for SessionFs.FileMode. DefaultSessionFsFileMode = sys.FileMode(0600) + + _ = RegisterSessionV(func() SessionV { + return &SessionFs{} + }) ) // SessionFs defines an implementation of Session on file system base. @@ -95,3 +100,11 @@ func (this SessionFs) isEqualTo(other *SessionFs) bool { this.Storage == other.Storage && this.FileMode == other.FileMode } + +func (this SessionFs) Types() []string { + return []string{"fs", "file-system"} +} + +func (this SessionFs) FeatureFlags() []string { + return []string{"fs"} +} diff --git a/pkg/configuration/session-fs_linux.go b/pkg/configuration/session-fs_unix.go similarity index 84% rename from pkg/configuration/session-fs_linux.go rename to pkg/configuration/session-fs_unix.go index 5363712..2dafa3e 100644 --- a/pkg/configuration/session-fs_linux.go +++ b/pkg/configuration/session-fs_unix.go @@ -1,4 +1,4 @@ -//go:build linux +//go:build unix package configuration diff --git a/pkg/configuration/session-fs_windows.go b/pkg/configuration/session-fs_windows.go new file mode 100644 index 0000000..f581913 --- /dev/null +++ b/pkg/configuration/session-fs_windows.go @@ -0,0 +1,7 @@ +//go:build windows + +package configuration + +var ( + defaultSessionFsStorage = `C:\ProgramData\Engity\Bifroest\sessions` +) diff --git a/pkg/configuration/session.go b/pkg/configuration/session.go index e27950f..c01f5ec 100644 --- a/pkg/configuration/session.go +++ b/pkg/configuration/session.go @@ -2,11 +2,13 @@ package configuration import ( "fmt" - "github.com/engity-com/bifroest/pkg/common" - "gopkg.in/yaml.v3" - "reflect" + "sort" "strings" "time" + + "gopkg.in/yaml.v3" + + "github.com/engity-com/bifroest/pkg/common" ) var ( @@ -29,6 +31,28 @@ type SessionV interface { trimmer validator equaler + Types() []string + FeatureFlags() []string +} + +var ( + typeToSessionFactory = make(map[string]SessionVFactory) + sessionVs []SessionV +) + +type SessionVFactory func() SessionV + +func RegisterSessionV(factory SessionVFactory) SessionVFactory { + pt := factory() + ts := pt.Types() + if len(ts) == 0 { + panic(fmt.Errorf("the instance does not provide any type")) + } + for _, t := range ts { + typeToSessionFactory[strings.ToLower(t)] = factory + } + sessionVs = append(sessionVs, pt) + return factory } func (this *Session) SetDefaults() error { @@ -68,13 +92,16 @@ func (this *Session) UnmarshalYAML(node *yaml.Node) error { return reportYamlRelatedErr(node, err) } - switch strings.ToLower(typeBuf.Type) { - case "fs", "file_system": - this.V = &SessionFs{} - default: + if typeBuf.Type == "" { + return reportYamlRelatedErrf(node, "[type] required but absent") + } + + factory, ok := typeToSessionFactory[strings.ToLower(typeBuf.Type)] + if !ok { return reportYamlRelatedErrf(node, "[type] illegal type: %q", typeBuf.Type) } + this.V = factory() if err := node.Decode(this.V); err != nil { return reportYamlRelatedErr(node, err) } @@ -86,15 +113,11 @@ func (this *Session) MarshalYAML() (any, error) { typeBuf := struct { SessionV `yaml:",inline"` Type string `yaml:"type"` - }{ - SessionV: this.V, - } + }{} - switch typeBuf.SessionV.(type) { - case *SessionFs: - typeBuf.Type = "fs" - default: - return nil, fmt.Errorf("[type] illegal type: %v", reflect.TypeOf(typeBuf.SessionV)) + if this.V != nil { + typeBuf.Type = this.V.Types()[0] + typeBuf.SessionV = this.V } return typeBuf, nil @@ -120,3 +143,12 @@ func (this Session) isEqualTo(other *Session) bool { } return this.V.IsEqualTo(other.V) } + +func GetSupportedSessionFeatureFlags() []string { + var result []string + for _, v := range sessionVs { + result = append(result, v.FeatureFlags()...) + } + sort.Strings(result) + return result +} diff --git a/pkg/configuration/ssh.go b/pkg/configuration/ssh.go index 57041a1..4ee1ae3 100644 --- a/pkg/configuration/ssh.go +++ b/pkg/configuration/ssh.go @@ -1,11 +1,13 @@ package configuration import ( + "time" + + "gopkg.in/yaml.v3" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/net" "github.com/engity-com/bifroest/pkg/template" - "gopkg.in/yaml.v3" - "time" ) var ( diff --git a/pkg/configuration/support.go b/pkg/configuration/support.go index f3a389d..6c40c66 100644 --- a/pkg/configuration/support.go +++ b/pkg/configuration/support.go @@ -3,15 +3,22 @@ package configuration import ( "errors" "fmt" - "gopkg.in/yaml.v3" "slices" "strings" + + "gopkg.in/yaml.v3" ) type defaulter interface { SetDefaults() error } +type defaulterFunc func() error + +func (f defaulterFunc) SetDefaults() error { + return f() +} + func setDefaults[T any](target *T, fs ...func(*T) (string, defaulter)) error { for _, f := range fs { n, d := f(target) @@ -235,20 +242,6 @@ func reportYamlRelatedErrf(node *yaml.Node, message string, args ...any) error { return reportYamlRelatedErr(node, fmt.Errorf(message, args...)) } -func enrichWithFile(err error, file string) error { - var ole *LocationError - if errors.As(err, &ole) { - if ole.File == "" { - ole.File = file - } - return err - } - return &LocationError{ - File: file, - Cause: err, - } -} - type LocationError struct { File string Line int @@ -327,6 +320,7 @@ func isEqual[T equaler](left, right *T) bool { return (*left).IsEqualTo(*right) } +//nolint:golint,unused func isEqualSlice[T equaler](left, right *[]T) bool { if left == nil && right == nil { return true diff --git a/pkg/configuration/support_test.go b/pkg/configuration/support_test.go index 313b66b..1940f26 100644 --- a/pkg/configuration/support_test.go +++ b/pkg/configuration/support_test.go @@ -2,10 +2,11 @@ package configuration import ( "fmt" - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" "strings" "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) type unmarshalYamlTestCase[T equaler] struct { diff --git a/pkg/configuration/user-requirement-template_unix.go b/pkg/configuration/user-requirement-template.go similarity index 99% rename from pkg/configuration/user-requirement-template_unix.go rename to pkg/configuration/user-requirement-template.go index 3f2a50c..48ccdde 100644 --- a/pkg/configuration/user-requirement-template_unix.go +++ b/pkg/configuration/user-requirement-template.go @@ -4,10 +4,12 @@ package configuration import ( "fmt" + + "gopkg.in/yaml.v3" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/template" "github.com/engity-com/bifroest/pkg/user" - "gopkg.in/yaml.v3" ) type UserRequirementTemplate struct { diff --git a/pkg/crypto/authorized-keys-file.go b/pkg/crypto/authorized-keys-file.go new file mode 100644 index 0000000..e0fce38 --- /dev/null +++ b/pkg/crypto/authorized-keys-file.go @@ -0,0 +1,55 @@ +package crypto + +import ( + "os" + + "golang.org/x/crypto/ssh" + + "github.com/engity-com/bifroest/pkg/common" +) + +type AuthorizedKeysFile string + +func (this AuthorizedKeysFile) ForEach(consumer func(i int, key ssh.PublicKey, comment string, opts []AuthorizedKeyOption) (canContinue bool, err error)) error { + if len(this) == 0 { + return nil + } + + f, err := os.Open(string(this)) + if err != nil { + return err + } + defer common.IgnoreCloseError(f) + + return parseAuthorizedKeys(f, consumer) +} + +func (this AuthorizedKeysFile) Get() ([]AuthorizedKeyWithOptions, error) { + return getAuthorizedKeysOf(this) +} + +func (this AuthorizedKeysFile) Validate() error { + return validateAuthorizedKeysOf(this) +} + +func (this AuthorizedKeysFile) IsZero() bool { + return len(this) == 0 +} + +func (this AuthorizedKeysFile) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case AuthorizedKeysFile: + return this.isEqualTo(&v) + case *AuthorizedKeysFile: + return this.isEqualTo(v) + default: + return false + } +} + +func (this AuthorizedKeysFile) isEqualTo(other *AuthorizedKeysFile) bool { + return this == *other +} diff --git a/pkg/crypto/authorized-keys-file_test.go b/pkg/crypto/authorized-keys-file_test.go new file mode 100644 index 0000000..001d131 --- /dev/null +++ b/pkg/crypto/authorized-keys-file_test.go @@ -0,0 +1,41 @@ +package crypto + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAuthorizedKeysFile_Get(t *testing.T) { + of := func(arg string) AuthorizedKeysFile { + return AuthorizedKeysFile(arg) + } + + cases := []struct { + given AuthorizedKeysFile + expected []AuthorizedKeyWithOptions + expectedErr string + }{{ + given: of(ed255191Fn), + expected: []AuthorizedKeyWithOptions{{ed255191Pub, nil}}, + }, { + given: of("not-existing"), + expectedErr: "open not-existing", + }, { + given: of(""), + }} + + for i, c := range cases { + t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) { + actual, actualErr := c.given.Get() + if c.expectedErr != "" { + assert.ErrorContains(t, actualErr, c.expectedErr) + } else { + require.NoError(t, actualErr) + } + assert.Equal(t, c.expected, actual) + }) + } +} diff --git a/pkg/crypto/authorized-keys-option-type.go b/pkg/crypto/authorized-keys-option-type.go new file mode 100644 index 0000000..27f1e53 --- /dev/null +++ b/pkg/crypto/authorized-keys-option-type.go @@ -0,0 +1,150 @@ +package crypto + +import ( + "errors" + "fmt" +) + +type AuthorizedKeyOptionType uint8 + +var ( + ErrIllegalAuthorizedKeyOptionType = errors.New("illegal authorized key option type") +) + +const ( + AuthorizedKeyOptionEmpty AuthorizedKeyOptionType = iota + AuthorizedKeyAgentForwarding + AuthorizedKeyNoAgentForwarding + AuthorizedKeyCertAuthority + AuthorizedKeyCommand + AuthorizedKeyEnvironment + AuthorizedKeyExpiryTime + AuthorizedKeyFrom + AuthorizedKeyPermitListen + AuthorizedKeyPermitOpen + AuthorizedKeyPrincipals + AuthorizedKeyPortForwarding + AuthorizedKeyNoPortForwarding + AuthorizedKeyPty + AuthorizedKeyNoPty + AuthorizedKeyNoTouchRequired + AuthorizedKeyVerifyRequired + AuthorizedKeyRestrict + AuthorizedKeyTunnel + AuthorizedKeyUserRc + AuthorizedKeyNoUserRc + AuthorizedKeyX11Forwarding + AuthorizedKeyNoX11Forwarding +) + +func (this AuthorizedKeyOptionType) hasValue() bool { + switch this { + case AuthorizedKeyCommand, + AuthorizedKeyEnvironment, + AuthorizedKeyExpiryTime, + AuthorizedKeyFrom, + AuthorizedKeyPermitListen, + AuthorizedKeyPermitOpen, + AuthorizedKeyPrincipals, + AuthorizedKeyTunnel: + return true + default: + return false + } +} + +func (this AuthorizedKeyOptionType) MarshalText() ([]byte, error) { + v, ok := authorizedKeyOptionTypeToName[this] + if !ok { + return nil, fmt.Errorf("%w: %d", ErrIllegalAuthorizedKeyOptionType, this) + } + return []byte(v), nil +} + +func (this *AuthorizedKeyOptionType) UnmarshalText(text []byte) error { + v, ok := nameToAuthorizedKeyOptionType[string(text)] + if !ok { + return fmt.Errorf("%w: %q", ErrIllegalAuthorizedKeyOptionType, string(text)) + } + *this = v + return nil + +} + +func (this AuthorizedKeyOptionType) String() string { + v, ok := authorizedKeyOptionTypeToName[this] + if !ok { + return fmt.Sprintf("illegal-athorized-key-option-%d", this) + } + return v +} + +func (this AuthorizedKeyOptionType) IsZero() bool { + return this == AuthorizedKeyOptionEmpty +} + +func (this *AuthorizedKeyOptionType) Set(text string) error { + return this.UnmarshalText([]byte(text)) +} + +func (this AuthorizedKeyOptionType) Validate() error { + _, ok := authorizedKeyOptionTypeToName[this] + if !ok { + return fmt.Errorf("%w: %d", ErrIllegalAuthorizedKeyOptionType, this) + } + return nil +} + +func (this AuthorizedKeyOptionType) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case AuthorizedKeyOptionType: + return this.isEqualTo(&v) + case *AuthorizedKeyOptionType: + return this.isEqualTo(v) + default: + return false + } +} + +func (this AuthorizedKeyOptionType) isEqualTo(other *AuthorizedKeyOptionType) bool { + return this == *other +} + +var ( + authorizedKeyOptionTypeToName = map[AuthorizedKeyOptionType]string{ + AuthorizedKeyOptionEmpty: "", + AuthorizedKeyAgentForwarding: "agent-forwarding", + AuthorizedKeyNoAgentForwarding: "no-agent-forwarding", + AuthorizedKeyCertAuthority: "cert-authority", + AuthorizedKeyCommand: "command", + AuthorizedKeyEnvironment: "environment", + AuthorizedKeyExpiryTime: "expiry-time", + AuthorizedKeyFrom: "from", + AuthorizedKeyPermitListen: "permitlisten", + AuthorizedKeyPermitOpen: "permitopen", + AuthorizedKeyPortForwarding: "port-forwarding", + AuthorizedKeyPrincipals: "principals", + AuthorizedKeyNoPortForwarding: "no-port-forwarding", + AuthorizedKeyPty: "pty", + AuthorizedKeyNoPty: "no-pty", + AuthorizedKeyNoTouchRequired: "no-touch-required", + AuthorizedKeyVerifyRequired: "verify-required", + AuthorizedKeyRestrict: "restrict", + AuthorizedKeyTunnel: "tunnel", + AuthorizedKeyUserRc: "user-rc", + AuthorizedKeyNoUserRc: "no-user-rc", + AuthorizedKeyX11Forwarding: "x11-forwarding", + AuthorizedKeyNoX11Forwarding: "no-x11-forwarding", + } + + nameToAuthorizedKeyOptionType = func(in map[AuthorizedKeyOptionType]string) map[string]AuthorizedKeyOptionType { + result := make(map[string]AuthorizedKeyOptionType, len(in)) + for k, v := range in { + result[v] = k + } + return result + }(authorizedKeyOptionTypeToName) +) diff --git a/pkg/crypto/authorized-keys-option.go b/pkg/crypto/authorized-keys-option.go new file mode 100644 index 0000000..69fc155 --- /dev/null +++ b/pkg/crypto/authorized-keys-option.go @@ -0,0 +1,113 @@ +package crypto + +import ( + "bytes" + "errors" + "fmt" + "strconv" + + "golang.org/x/crypto/ssh" +) + +var ( + ErrIllegalAuthorizedKeyOption = errors.New("illegal authorized key option") +) + +type AuthorizedKeyOption struct { + Type AuthorizedKeyOptionType + Value string +} + +func (this AuthorizedKeyOption) MarshalText() ([]byte, error) { + t := this.Type + if t.IsZero() { + return nil, nil + } + + mt, err := t.MarshalText() + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrIllegalAuthorizedKeyOption, err) + } + + if t.hasValue() { + v := this.Value + if v == "" { + return nil, fmt.Errorf("%w: option type %v requires a value, but is absent", ErrIllegalAuthorizedKeyOption, t) + } + + strconv.Quote(this.Value) + + return bytes.Join([][]byte{ + mt, + {'='}, + []byte(strconv.Quote(this.Value)), + }, nil), nil + } + return mt, nil +} + +func (this *AuthorizedKeyOption) UnmarshalText(text []byte) error { + parts := bytes.SplitN(text, []byte("="), 2) + + var buf AuthorizedKeyOption + if err := buf.Type.UnmarshalText(parts[0]); err != nil { + return fmt.Errorf("%w: %v", ErrIllegalAuthorizedKeyOption, err) + } + + if buf.Type.hasValue() { + if len(parts) != 2 { + return fmt.Errorf("%w: option type %v requires a value, but is absent", ErrIllegalAuthorizedKeyOption, buf.Type) + } + var err error + buf.Value, err = strconv.Unquote(string(parts[1])) + if err != nil { + return fmt.Errorf("%w: option's value is not correctly quoted: %s", ErrIllegalAuthorizedKeyOption, string(parts[1])) + } + } else if len(parts) > 1 { + return fmt.Errorf("%w: option type %v does not allow any value, but there was one provided: %q", ErrIllegalAuthorizedKeyOption, buf.Type, string(parts[1])) + } + *this = buf + return nil + +} + +func (this AuthorizedKeyOption) String() string { + v, err := this.MarshalText() + if err != nil { + return "ERR: " + err.Error() + } + return string(v) +} + +func (this *AuthorizedKeyOption) Set(text string) error { + return this.UnmarshalText([]byte(text)) +} + +func (this AuthorizedKeyOption) Validate() error { + _, err := this.MarshalText() + return err +} + +func (this AuthorizedKeyOption) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case AuthorizedKeyOption: + return this.isEqualTo(&v) + case *AuthorizedKeyOption: + return this.isEqualTo(v) + default: + return false + } +} + +func (this AuthorizedKeyOption) isEqualTo(other *AuthorizedKeyOption) bool { + return this.Type.isEqualTo(&other.Type) && + this.Value == other.Value +} + +type AuthorizedKeyWithOptions struct { + ssh.PublicKey + Options []AuthorizedKeyOption +} diff --git a/pkg/crypto/authorized-keys.go b/pkg/crypto/authorized-keys.go new file mode 100644 index 0000000..658bec3 --- /dev/null +++ b/pkg/crypto/authorized-keys.go @@ -0,0 +1,220 @@ +package crypto + +import ( + "bufio" + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + "strings" + + "golang.org/x/crypto/ssh" +) + +var ( + ErrIllegalSshKey = errors.New("illegal ssh key found") + ErrIllegalAuthorizedKeysFormat = errors.New("illegal authorized keys format") +) + +const ( + maxAuthorizedKeysLineSize = 10 * 1024 +) + +type AuthorizedKeys string + +func (this AuthorizedKeys) ForEach(consumer func(i int, key ssh.PublicKey, comment string, opts []AuthorizedKeyOption) (canContinue bool, err error)) error { + if len(this) == 0 { + return nil + } + + return parseAuthorizedKeys(bytes.NewReader([]byte(this)), consumer) +} + +func (this AuthorizedKeys) Get() ([]AuthorizedKeyWithOptions, error) { + return getAuthorizedKeysOf(this) +} + +func (this AuthorizedKeys) Validate() error { + return validateAuthorizedKeysOf(this) +} + +func (this AuthorizedKeys) IsZero() bool { + return strings.TrimSpace(string(this)) == "" +} + +func (this *AuthorizedKeys) Trim() error { + *this = AuthorizedKeys(strings.TrimSpace(string(*this))) + return nil +} + +func (this AuthorizedKeys) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case AuthorizedKeys: + return this.isEqualTo(&v) + case *AuthorizedKeys: + return this.isEqualTo(v) + default: + return false + } +} + +func (this AuthorizedKeys) isEqualTo(other *AuthorizedKeys) bool { + return this == *other +} + +type getAuthorizedKeysOfSource interface { + IsZero() bool + ForEach(func(int, ssh.PublicKey, string, []AuthorizedKeyOption) (bool, error)) error +} + +func getAuthorizedKeysOf(source getAuthorizedKeysOfSource) ([]AuthorizedKeyWithOptions, error) { + var result []AuthorizedKeyWithOptions + return result, source.ForEach(func(i int, key ssh.PublicKey, comment string, opts []AuthorizedKeyOption) (bool, error) { + result = append(result, AuthorizedKeyWithOptions{key, opts}) + return true, nil + }) +} + +func validateAuthorizedKeysOf(source getAuthorizedKeysOfSource) error { + if source.IsZero() { + return nil + } + + atLeastOne := false + if err := source.ForEach(func(int, ssh.PublicKey, string, []AuthorizedKeyOption) (canContinue bool, err error) { + atLeastOne = true + return false, nil + }); err != nil { + return err + } + if !atLeastOne { + return fmt.Errorf("illegal or non-existent authorized keys: %v", source) + } + return nil +} + +func parseAuthorizedKeys(r io.Reader, consumer func(i int, key ssh.PublicKey, comment string, options []AuthorizedKeyOption) (canContinue bool, err error)) error { + scanner := bufio.NewScanner(r) + scanner.Split(bufio.ScanLines) + scanner.Buffer(make([]byte, maxAuthorizedKeysLineSize), maxAuthorizedKeysLineSize) + + var i int + for scanner.Scan() { + line := scanner.Bytes() + pub, comment, options, err := parseAuthorizedKey(line) + if err != nil { + return err + } + if pub == nil { + continue + } + canContinue, err := consumer(i, pub, comment, options) + if err != nil || !canContinue { + return err + } + i++ + } + return scanner.Err() +} + +func parseAuthorizedKey(line []byte) (out ssh.PublicKey, comment string, options []AuthorizedKeyOption, err error) { + line = bytes.TrimSpace(line) + if len(line) == 0 || line[0] == '#' { + return nil, "", nil, nil + } + + var algo string + algo, line = cutOffSshKeyAlgo(line) + if algo == "" { + // No key type recognized. Maybe there's an options field at the beginning. + var b byte + inQuote := false + optionStart := 0 + var i int + for i, b = range line { + isEnd := !inQuote && (b == ' ' || b == '\t') + if (b == ',' && !inQuote) || isEnd { + if i-optionStart > 0 { + var option AuthorizedKeyOption + if err := option.UnmarshalText(line[optionStart:i]); err != nil { + return nil, "", nil, fmt.Errorf("%w: %v", ErrIllegalAuthorizedKeysFormat, err) + } + options = append(options, option) + } + optionStart = i + 1 + } + if isEnd { + break + } + if b == '"' && (i == 0 || (i > 0 && line[i-1] != '\\')) { + inQuote = !inQuote + } + } + for i < len(line) && (line[i] == ' ' || line[i] == '\t') { + i++ + } + if i == len(line) { + return nil, "", nil, ErrIllegalAuthorizedKeysFormat + } + algo, line = cutOffSshKeyAlgo(line[i:]) + } + if algo == "" { + return nil, "", nil, ErrIllegalAuthorizedKeysFormat + } + + i := bytes.IndexAny(line, " \t") + var base64Key []byte + if i == -1 { + base64Key = bytes.TrimSpace(line) + } else { + base64Key, comment = bytes.TrimSpace(line[:i]), strings.TrimSpace(string(line[i+1:])) + } + + key := make([]byte, base64.StdEncoding.DecodedLen(len(base64Key))) + n, err := base64.StdEncoding.Decode(key, base64Key) + if err != nil { + return nil, "", nil, fmt.Errorf("%w: %v", ErrIllegalSshKey, err) + } + key = key[:n] + out, err = ssh.ParsePublicKey(key) + if err != nil { + return nil, "", nil, fmt.Errorf("%w: %v", ErrIllegalSshKey, err) + } + + return out, comment, options, nil +} + +func getSshKeyAlgo(in []byte) string { + switch string(in) { + case ssh.KeyAlgoRSA: + return string(in) + case ssh.KeyAlgoDSA: + return string(in) + case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521: + return string(in) + case ssh.KeyAlgoSKECDSA256: + return string(in) + case ssh.KeyAlgoED25519: + return string(in) + case ssh.KeyAlgoSKED25519: + return string(in) + default: + return "" + } +} + +func cutOffSshKeyAlgo(in []byte) (string, []byte) { + i := bytes.IndexAny(in, " \t") + if i == -1 { + return "", in + } + algo := getSshKeyAlgo(in[:i]) + if algo == "" { + return "", in + } + return algo, in[i+1:] +} diff --git a/pkg/crypto/authorized-keys_test.go b/pkg/crypto/authorized-keys_test.go new file mode 100644 index 0000000..5c78a16 --- /dev/null +++ b/pkg/crypto/authorized-keys_test.go @@ -0,0 +1,115 @@ +package crypto + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" + + "github.com/engity-com/bifroest/pkg/common" +) + +//nolint:golint,unused +var ( + dsa1Pub, dsa1Fn = mustSshPublicKey("dsa-1") + ecdsa1Pub, ecdsa1Fn = mustSshPublicKey("ecdsa-1") + ecdsaSk1Pub, ecdsaSk1Fn = mustSshPublicKey("ecdsa-sk-1") + ed255191Pub, ed255191Fn = mustSshPublicKey("ed25519-1") + ed255192Pub, ed255192Fn = mustSshPublicKey("ed25519-2") + ed255193Pub, ed255193Fn = mustSshPublicKey("ed25519-3") + ed255194Pub, ed255194Fn = mustSshPublicKey("ed25519-4") + ed25519Sk1Pub, ed25519Sk1Fn = mustSshPublicKey("ed25519-sk-1") + rsa1Pub, rsa1Fn = mustSshPublicKey("rsa-1") +) + +func TestAuthorizedKeys_Get(t *testing.T) { + of := func(args ...any) AuthorizedKeys { + strArgs := make([]string, len(args)) + for i, plainArg := range args { + switch arg := plainArg.(type) { + case ssh.PublicKey: + strArgs[i] = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(arg))) + case int32: + strArgs[i] = string([]byte{byte(arg)}) + case []byte: + strArgs[i] = string(arg) + case string: + strArgs[i] = arg + case AuthorizedKeyOption: + strArgs[i] = arg.String() + case AuthorizedKeyOptionType: + strArgs[i] = arg.String() + default: + panic(fmt.Errorf("unknown arg type: %T", plainArg)) + } + } + return AuthorizedKeys(strings.Join(strArgs, "")) + } + + cases := []struct { + given AuthorizedKeys + expected []AuthorizedKeyWithOptions + expectedErr string + }{{ + given: of(ed255191Pub), + expected: []AuthorizedKeyWithOptions{{ed255191Pub, nil}}, + }, { + given: of(ed255191Pub, '\n', ed255192Pub), + expected: []AuthorizedKeyWithOptions{{ed255191Pub, nil}, {ed255192Pub, nil}}, + }, { + given: of("\n foo bar"), + expectedErr: ErrIllegalAuthorizedKeysFormat.Error(), + }, { + given: of(ed255191Pub, "\n foo bar"), + expected: []AuthorizedKeyWithOptions{{ed255191Pub, nil}}, + expectedErr: ErrIllegalAuthorizedKeysFormat.Error(), + }, { + given: of(ed255191Pub, "\n# foo bar"), + expected: []AuthorizedKeyWithOptions{{ed255191Pub, nil}}, + }, { + given: of(""), + expected: nil, + }, { + given: of("#only a comment"), + expected: nil, + }, { + given: of(AuthorizedKeyOption{AuthorizedKeyAgentForwarding, ""}, " ", ed255191Pub), + expected: []AuthorizedKeyWithOptions{{ed255191Pub, []AuthorizedKeyOption{{AuthorizedKeyAgentForwarding, ""}}}}, + }, { + given: of(AuthorizedKeyOption{AuthorizedKeyCommand, "abc def"}, " ", ed255191Pub), + expected: []AuthorizedKeyWithOptions{{ed255191Pub, []AuthorizedKeyOption{{AuthorizedKeyCommand, "abc def"}}}}, + }, { + given: of(AuthorizedKeyCommand, "=abc ", ed255191Pub), + expectedErr: ErrIllegalAuthorizedKeysFormat.Error(), + }, { + given: of(AuthorizedKeyCommand, " ", ed255191Pub), + expectedErr: ErrIllegalAuthorizedKeysFormat.Error(), + }} + + for i, c := range cases { + t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) { + actual, actualErr := c.given.Get() + if c.expectedErr != "" { + assert.ErrorContains(t, actualErr, c.expectedErr) + } else { + require.NoError(t, actualErr) + } + assert.Equal(t, c.expected, actual) + }) + } +} + +func mustSshPublicKey(name string) (ssh.PublicKey, string) { + fn := filepath.Join("testdata", name+".pub") + b, err := os.ReadFile(fn) + common.Must(err, "public key file %q must exist and be readable", fn) + result, _, _, _, err := ssh.ParseAuthorizedKey(b) + common.Must(err, "public key file %q must contain a valid public key", fn) + common.MustNotNil(result, "public key file %q must contain a valid public key", fn) + return result, fn +} diff --git a/pkg/crypto/dsa-restriction.go b/pkg/crypto/dsa-restriction.go index b46f0a1..9e2d817 100644 --- a/pkg/crypto/dsa-restriction.go +++ b/pkg/crypto/dsa-restriction.go @@ -3,8 +3,9 @@ package crypto import ( "crypto/dsa" "fmt" - "golang.org/x/crypto/ssh" "strings" + + "golang.org/x/crypto/ssh" ) type DsaRestriction uint8 diff --git a/pkg/crypto/ecdsa-restriction.go b/pkg/crypto/ecdsa-restriction.go index 85dbfe1..bf5d92b 100644 --- a/pkg/crypto/ecdsa-restriction.go +++ b/pkg/crypto/ecdsa-restriction.go @@ -3,8 +3,9 @@ package crypto import ( "crypto/ecdsa" "fmt" - "golang.org/x/crypto/ssh" "strings" + + "golang.org/x/crypto/ssh" ) type EcdsaRestriction uint8 diff --git a/pkg/crypto/ed25519-restriction.go b/pkg/crypto/ed25519-restriction.go index fa3f0bf..53c3ad9 100644 --- a/pkg/crypto/ed25519-restriction.go +++ b/pkg/crypto/ed25519-restriction.go @@ -3,8 +3,9 @@ package crypto import ( "crypto/ed25519" "fmt" - "golang.org/x/crypto/ssh" "strings" + + "golang.org/x/crypto/ssh" ) type Ed25519Restriction uint8 diff --git a/pkg/crypto/htpasswd-file.go b/pkg/crypto/htpasswd-file.go new file mode 100644 index 0000000..2c915d2 --- /dev/null +++ b/pkg/crypto/htpasswd-file.go @@ -0,0 +1,69 @@ +package crypto + +import ( + "github.com/tg123/go-htpasswd" +) + +type HtpasswdFile struct { + file *htpasswd.File + fn string +} + +func (this HtpasswdFile) Match(username, password string) bool { + if v := this.file; v != nil { + return v.Match(username, password) + } + return false +} + +func (this HtpasswdFile) MarshalText() (text []byte, err error) { + return []byte(this.String()), nil +} + +func (this HtpasswdFile) String() string { + return this.fn +} + +func (this *HtpasswdFile) UnmarshalText(text []byte) error { + if len(text) == 0 { + *this = HtpasswdFile{} + return nil + } + + f, err := htpasswd.New(string(text), htpasswd.DefaultSystems, nil) + if err != nil { + return err + } + *this = HtpasswdFile{f, string(text)} + return nil +} + +func (this *HtpasswdFile) Set(text string) error { + return this.UnmarshalText([]byte(text)) +} + +func (this HtpasswdFile) Validate() error { + return nil +} + +func (this HtpasswdFile) IsZero() bool { + return this.file == nil +} + +func (this HtpasswdFile) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case HtpasswdFile: + return this.isEqualTo(&v) + case *HtpasswdFile: + return this.isEqualTo(v) + default: + return false + } +} + +func (this HtpasswdFile) isEqualTo(other *HtpasswdFile) bool { + return this.fn == other.fn +} diff --git a/pkg/crypto/htpasswd.go b/pkg/crypto/htpasswd.go new file mode 100644 index 0000000..3a137bb --- /dev/null +++ b/pkg/crypto/htpasswd.go @@ -0,0 +1,72 @@ +package crypto + +import ( + "bytes" + + "github.com/tg123/go-htpasswd" +) + +type Htpasswd struct { + file *htpasswd.File + plain string +} + +func (this Htpasswd) Match(username, password string) bool { + if v := this.file; v != nil { + return v.Match(username, password) + } + return false +} + +func (this Htpasswd) MarshalText() (text []byte, err error) { + return []byte(this.String()), nil +} + +func (this Htpasswd) String() string { + return this.plain +} + +func (this *Htpasswd) UnmarshalText(text []byte) error { + text = bytes.TrimSpace(text) + if len(text) == 0 { + *this = Htpasswd{} + return nil + } + + f, err := htpasswd.NewFromReader(bytes.NewBuffer(text), htpasswd.DefaultSystems, nil) + if err != nil { + return err + } + *this = Htpasswd{f, string(text)} + return nil +} + +func (this *Htpasswd) Set(text string) error { + return this.UnmarshalText([]byte(text)) +} + +func (this Htpasswd) Validate() error { + return nil +} + +func (this Htpasswd) IsZero() bool { + return this.file == nil +} + +func (this Htpasswd) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case Htpasswd: + return this.isEqualTo(&v) + case *Htpasswd: + return this.isEqualTo(v) + default: + return false + } +} + +func (this Htpasswd) isEqualTo(other *Htpasswd) bool { + return this.plain == other.plain +} diff --git a/pkg/crypto/key-requirement.go b/pkg/crypto/key-requirement.go index faf0871..f79be3c 100644 --- a/pkg/crypto/key-requirement.go +++ b/pkg/crypto/key-requirement.go @@ -8,10 +8,11 @@ import ( crand "crypto/rand" "crypto/rsa" "fmt" - "github.com/engity-com/bifroest/pkg/common" "io" "os" "path/filepath" + + "github.com/engity-com/bifroest/pkg/common" ) const ( @@ -41,6 +42,9 @@ func (this KeyRequirement) CreateFile(rand io.Reader, fn string) (crypto.Signer, _ = os.MkdirAll(filepath.Dir(fn), 0700) f, err := os.OpenFile(fn, os.O_CREATE|os.O_WRONLY, 0400) + if err != nil { + return nil, err + } defer common.IgnoreCloseError(f) if err := WriteSshPrivateKey(pk, f); err != nil { diff --git a/pkg/crypto/key.go b/pkg/crypto/key.go index 404512a..b93a867 100644 --- a/pkg/crypto/key.go +++ b/pkg/crypto/key.go @@ -5,12 +5,14 @@ import ( "crypto/dsa" "encoding/pem" "fmt" - "github.com/engity-com/bifroest/pkg/sys" + "io" + "os" + "github.com/mikesmitty/edkey" "golang.org/x/crypto/ed25519" "golang.org/x/crypto/ssh" - "io" - "os" + + "github.com/engity-com/bifroest/pkg/sys" ) func EnsureKeyFile(fn string, reqOnAbsence *KeyRequirement, rand io.Reader) (crypto.Signer, error) { diff --git a/pkg/crypto/password-bcrypt.go b/pkg/crypto/password-bcrypt.go new file mode 100644 index 0000000..1476347 --- /dev/null +++ b/pkg/crypto/password-bcrypt.go @@ -0,0 +1,21 @@ +package crypto + +import ( + "golang.org/x/crypto/bcrypt" + + "github.com/engity-com/bifroest/pkg/errors" +) + +func (this PasswordType) encodeBcrypt(password []byte) ([]byte, error) { + return bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) +} + +func (this PasswordType) compareBcrypt(encoded, password []byte) (bool, error) { + err := bcrypt.CompareHashAndPassword(encoded, password) + if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) || errors.Is(err, bcrypt.ErrHashTooShort) { + return false, nil + } else if err != nil { + return false, err + } + return true, nil +} diff --git a/pkg/crypto/password-type.go b/pkg/crypto/password-type.go new file mode 100644 index 0000000..626fcda --- /dev/null +++ b/pkg/crypto/password-type.go @@ -0,0 +1,99 @@ +package crypto + +import ( + "bytes" + "errors" + "fmt" +) + +var ( + ErrIllegalPasswordType = errors.New("illegal password type") +) + +type PasswordType uint8 + +const ( + PasswordTypePlain PasswordType = iota + PasswordTypeBcrypt +) + +func (this PasswordType) String() string { + v, err := this.MarshalText() + if err != nil { + return fmt.Sprintf("illegal-password-type-%d", this) + } + return string(v) +} + +func (this PasswordType) MarshalText() ([]byte, error) { + switch this { + case PasswordTypePlain: + return []byte("plain"), nil + case PasswordTypeBcrypt: + return []byte("bcrypt"), nil + default: + return nil, fmt.Errorf("%w: %d", ErrIllegalPasswordType, this) + } +} + +func (this *PasswordType) Set(plain string) error { + switch plain { + case "plain": + *this = PasswordTypePlain + return nil + case "bcrypt": + *this = PasswordTypeBcrypt + return nil + default: + return fmt.Errorf("%w: %q", ErrIllegalPasswordType, plain) + } +} + +func (this PasswordType) Encode(password []byte) ([]byte, error) { + switch this { + case PasswordTypePlain: + return password, nil + case PasswordTypeBcrypt: + return this.encodeBcrypt(password) + default: + return nil, fmt.Errorf("%w: %d", ErrIllegalPasswordType, this) + } +} + +func (this PasswordType) Compare(encoded, password []byte) (bool, error) { + switch this { + case PasswordTypePlain: + return bytes.Equal(encoded, password), nil + case PasswordTypeBcrypt: + return this.compareBcrypt(encoded, password) + default: + return false, fmt.Errorf("%w: %d", ErrIllegalPasswordType, this) + } +} + +func (this *PasswordType) UnmarshalText(b []byte) error { + return this.Set(string(b)) +} + +func (this PasswordType) Validate() error { + _, err := this.MarshalText() + return err +} + +func (this PasswordType) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case PasswordType: + return this.isEqualTo(&v) + case *PasswordType: + return this.isEqualTo(v) + default: + return false + } +} + +func (this PasswordType) isEqualTo(other *PasswordType) bool { + return this == *other +} diff --git a/pkg/crypto/password.go b/pkg/crypto/password.go new file mode 100644 index 0000000..a3624e5 --- /dev/null +++ b/pkg/crypto/password.go @@ -0,0 +1,97 @@ +package crypto + +import ( + "bytes" + "errors" + "fmt" + "strings" +) + +var ( + ErrIllegalPassword = errors.New("illegal password") +) + +type Password []byte + +func (this Password) String() string { + return string(this) +} + +func (this Password) MarshalText() ([]byte, error) { + if err := this.Validate(); err != nil { + return nil, err + } + return bytes.Clone(this), nil +} + +func (this *Password) Set(plain string) error { + buf := Password(plain) + if err := buf.Validate(); err != nil { + return err + } + *this = buf + return nil +} + +func (this *Password) UnmarshalText(b []byte) error { + return this.Set(string(b)) +} + +func (this Password) Compare(withPassword []byte) (bool, error) { + i := bytes.Index(this, []byte{':'}) + if i < 0 || len(this) < i+1 { + return false, fmt.Errorf("%w: %v", ErrIllegalPassword, string(this)) + } + + var t PasswordType + if err := t.UnmarshalText(this[:i]); err != nil { + return false, fmt.Errorf("%w: %v: %v", ErrIllegalPassword, string(this), err) + } + + return t.Compare(this[i+1:], withPassword) +} + +func (this *Password) SetPassword(t PasswordType, password []byte) error { + bt, err := t.MarshalText() + if err != nil { + return err + } + *this = bytes.Join([][]byte{bt, password}, []byte{}) + return nil +} + +func (this Password) Validate() error { + parts := strings.SplitN(string(this), ":", 2) + if len(parts) != 2 { + return fmt.Errorf("%w: %v", ErrIllegalPassword, string(this)) + } + + var t PasswordType + if err := t.Set(parts[0]); err != nil { + return fmt.Errorf("%w: %v: %v", ErrIllegalPassword, string(this), err) + } + + return nil +} + +func (this Password) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case Password: + return this.isEqualTo(&v) + case *Password: + return this.isEqualTo(v) + default: + return false + } +} + +func (this Password) IsZero() bool { + return len(this) == 0 +} + +func (this Password) isEqualTo(other *Password) bool { + return bytes.Equal(this, *other) +} diff --git a/pkg/crypto/rsa-restriction.go b/pkg/crypto/rsa-restriction.go index 5f41834..224d620 100644 --- a/pkg/crypto/rsa-restriction.go +++ b/pkg/crypto/rsa-restriction.go @@ -3,8 +3,9 @@ package crypto import ( "crypto/rsa" "fmt" - "golang.org/x/crypto/ssh" "strings" + + "golang.org/x/crypto/ssh" ) type RsaRestriction uint8 diff --git a/pkg/crypto/ssh.go b/pkg/crypto/ssh.go index 4b13ecb..233da7f 100644 --- a/pkg/crypto/ssh.go +++ b/pkg/crypto/ssh.go @@ -2,13 +2,14 @@ package crypto import ( "fmt" - "github.com/engity-com/bifroest/pkg/sys" - "github.com/gliderlabs/ssh" - gssh "golang.org/x/crypto/ssh" "os" + + "golang.org/x/crypto/ssh" + + "github.com/engity-com/bifroest/pkg/sys" ) -func DoWithEachAuthorizedKey[R any](requireExistence bool, callback func(gssh.PublicKey) (result R, canContinue bool, err error), files ...string) (result R, err error) { +func DoWithEachAuthorizedKey[R any](requireExistence bool, callback func(ssh.PublicKey) (result R, canContinue bool, err error), files ...string) (result R, err error) { fail := func(err error) (R, error) { var empty R return empty, err @@ -27,7 +28,7 @@ func DoWithEachAuthorizedKey[R any](requireExistence bool, callback func(gssh.Pu } var entry int for len(rest) > 0 { - var pub gssh.PublicKey + var pub ssh.PublicKey pub, _, _, rest, err = ssh.ParseAuthorizedKey(rest) if err != nil { return failf("failed to parse entry #%d of authorized keys file %q: %v", entry, file, err) diff --git a/pkg/crypto/testdata/dsa-1.pub b/pkg/crypto/testdata/dsa-1.pub new file mode 100644 index 0000000..8be1683 --- /dev/null +++ b/pkg/crypto/testdata/dsa-1.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBANAxSuwj8P+siHjIpjGC7I2X14VEZWu5WMJGxz82qeba1m+NLqeprGVr97RyHiFBo0WtQ9LPh6K3kfpFFTMCDGZyEgb+F1CQcL6aMZH2BqOGo7yJxHabfDVBfRrsJUsOWdbgaenM2dPpU6OMxlvyLUMYPNOgh7ERtUu5jNWDahBRAAAAFQC/sVownBc4TrAhyJ3KcYJmUzNH0wAAAIA8rC8CIN9fUZLdr7DcP8JTuyGPC/+VYvaVCkf0K32BlP3jUVxyRNFn+sDJa/0i3/OuYwlOFjfsWNnpwebKeiVBCfX/9oC2bnrBGNC5CMR46N7n3oFip2FqwdC1aBw2yGQiLBA7DScYxPf4dI/SoSoD5Cis6rXmVZXS9pMyM/WfqAAAAIEAnS1x3b5J0PuclrdX6cabSuhb3YuJe0CKCVfbxlRrRVgQxvXTXavJfAsHcpW4G3WbxcJD8i8Lam7L/4F8PQU6TttohF1p/qQSNaBh9Zr2sNSOxiHiDC6xOolyZyAp4ZUfi9gvs0Bw+kuzkS8wJt+Hcp8vvHolSVZ0EVfJWqJ5s7E= dsa-1 diff --git a/pkg/crypto/testdata/ecdsa-1.pub b/pkg/crypto/testdata/ecdsa-1.pub new file mode 100644 index 0000000..0de7ab8 --- /dev/null +++ b/pkg/crypto/testdata/ecdsa-1.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBF/3nsZl1yUMbWubEgGVHYn/BDyKAfln2iqKLKccbGvOWYutNYRdeJhP2CRnqkQSQh3LO0aj+d5xC0I7h6Cax3M= ecdsa-1 diff --git a/pkg/crypto/testdata/ecdsa-sk-1.pub b/pkg/crypto/testdata/ecdsa-sk-1.pub new file mode 100644 index 0000000..1bd39e0 --- /dev/null +++ b/pkg/crypto/testdata/ecdsa-sk-1.pub @@ -0,0 +1 @@ +sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBFFTbB+0w4jrGIWhC2Y1UlIwCurRYJspY016WKzRW6qdItkJZfWU4SSBsWS34wJc4P7vE2QTPE52pT+uZtJjBbkAAAAEc3NoOg== ecdsa-sk-1 diff --git a/pkg/crypto/testdata/ed25519-1.pub b/pkg/crypto/testdata/ed25519-1.pub new file mode 100644 index 0000000..a797a27 --- /dev/null +++ b/pkg/crypto/testdata/ed25519-1.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC80lm5FQbbyRUut6RwZJRbxTLO3W4f08ITDi9fA3+jx ed25519-1 diff --git a/pkg/crypto/testdata/ed25519-2.pub b/pkg/crypto/testdata/ed25519-2.pub new file mode 100644 index 0000000..ccf8c69 --- /dev/null +++ b/pkg/crypto/testdata/ed25519-2.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN6RppxO6UWSTO4R0p2tBGXoIzPMLU+Wz4Z8rW7qGECI ed25519-2 diff --git a/pkg/crypto/testdata/ed25519-3.pub b/pkg/crypto/testdata/ed25519-3.pub new file mode 100644 index 0000000..310eec3 --- /dev/null +++ b/pkg/crypto/testdata/ed25519-3.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB8DaV5irXqJnACmCF29ANjCLAx45gZX+q8I4DWHmqqv ed25519-3 diff --git a/pkg/crypto/testdata/ed25519-4.pub b/pkg/crypto/testdata/ed25519-4.pub new file mode 100644 index 0000000..ff32a5a --- /dev/null +++ b/pkg/crypto/testdata/ed25519-4.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOXszjDoQILzQh5m4ebSSlcV69U0oML01wFCfOrzRmom ed25519-4 diff --git a/pkg/crypto/testdata/ed25519-sk-1.pub b/pkg/crypto/testdata/ed25519-sk-1.pub new file mode 100644 index 0000000..910506d --- /dev/null +++ b/pkg/crypto/testdata/ed25519-sk-1.pub @@ -0,0 +1 @@ +sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIFqRbsoEkZPdr0A8q2eOI2uiPTB2HOagW86gKug+a9MAAAAABHNzaDo= ed25519-sk-1 diff --git a/pkg/crypto/testdata/rsa-1.pub b/pkg/crypto/testdata/rsa-1.pub new file mode 100644 index 0000000..d824310 --- /dev/null +++ b/pkg/crypto/testdata/rsa-1.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDIGYzqJpPf3shQVGo98xMdl5S4LJmWme3i+sPcYseRrKziAWWGc8xzLUGnRwVe5X5v7J+IaHZ0dpnelylbnDwEvQTX+8gZybcL8RpS6u5dKqmKTv12SqcucpGStQ3O0Ec3MnRKEeMoJXIdqIVxuXxC8863H42KzkBvDjZn4qasF8IOVpGSC4+i93bNKScN6epQYzKcPCmZSSAnZJPgih0y1Z6+yNOJd+6PAFXmhBOh7yU0Ypne9szj/6o3YrPuNUj762CZyjg7ivQI/DvxwnUA2X8dnb2pyD4CGrr6YduWMl2xqUEDerNVaPc+I63QR8gIUYYmAs5uQwrDI4U0aWpC7erLMsNRa8C+YUdX+rV2+lJWSH8/k2NGrT1FoG5PWHmZTIe4juKIlAArzDAE6shauM3j4b4YLhly6mySXxT9m+EPtcrZjdEg76/0FylFUH70dx0Wf7lt50cLQIoXCJVovp/w95J6FVMYACl1y/sbDzmirg2PQkqkrr3MZnNY88jI/OuyZYAHNIjMkbriaFIkFBK4epGhsIIpsArPS8ZGZTQNBrrYWF+pf8JvJ1NaoLP+JKUP/A7l1KsqCKK3sWIRY7u8n8McK0VQMig4duHHtZ+aUGhZd/+m19UG1gg7QPUffZQM0RIPWWcsklrmlvzBqVcxgHXkZOoFqzc9WyewWQ== rsa-1 diff --git a/pkg/crypto/unix/password/apr1.go b/pkg/crypto/unix/password/apr1.go index 56b4b6a..2d3edd0 100644 --- a/pkg/crypto/unix/password/apr1.go +++ b/pkg/crypto/unix/password/apr1.go @@ -3,6 +3,7 @@ package password import ( "github.com/GehirnInc/crypt" "github.com/GehirnInc/crypt/apr1_crypt" + "github.com/engity-com/bifroest/pkg/errors" ) @@ -23,3 +24,7 @@ func (p *Apr1) Validate(password string, hash []byte) (bool, error) { return true, nil } } + +func (p *Apr1) Name() string { + return "apr1" +} diff --git a/pkg/crypto/unix/password/crypt.go b/pkg/crypto/unix/password/crypt.go index 33ca997..2ede7e6 100644 --- a/pkg/crypto/unix/password/crypt.go +++ b/pkg/crypto/unix/password/crypt.go @@ -2,6 +2,8 @@ package password import ( "bytes" + "sort" + "github.com/engity-com/bifroest/pkg/errors" ) @@ -12,6 +14,7 @@ var ( type Crypt interface { Validate(password string, hash []byte) (bool, error) + Name() string } func Validate(password string, hash []byte) (bool, error) { @@ -22,3 +25,14 @@ func Validate(password string, hash []byte) (bool, error) { } return false, ErrNoSuchCrypt } + +func GetSupportedFeatureFlags() []string { + result := make([]string, len(Instances)) + var i int + for _, v := range Instances { + result[i] = v.Name() + i++ + } + sort.Strings(result) + return result +} diff --git a/pkg/crypto/unix/password/md5.go b/pkg/crypto/unix/password/md5.go index 4394fd8..1f9cf35 100644 --- a/pkg/crypto/unix/password/md5.go +++ b/pkg/crypto/unix/password/md5.go @@ -3,6 +3,7 @@ package password import ( "github.com/GehirnInc/crypt" "github.com/GehirnInc/crypt/md5_crypt" + "github.com/engity-com/bifroest/pkg/errors" ) @@ -23,3 +24,7 @@ func (p *Md5) Validate(password string, hash []byte) (bool, error) { return true, nil } } + +func (p *Md5) Name() string { + return "md5" +} diff --git a/pkg/crypto/unix/password/sha256.go b/pkg/crypto/unix/password/sha256.go index 773954d..b6a72ad 100644 --- a/pkg/crypto/unix/password/sha256.go +++ b/pkg/crypto/unix/password/sha256.go @@ -3,6 +3,7 @@ package password import ( "github.com/GehirnInc/crypt" "github.com/GehirnInc/crypt/sha256_crypt" + "github.com/engity-com/bifroest/pkg/errors" ) @@ -23,3 +24,7 @@ func (p *Sha256) Validate(password string, hash []byte) (bool, error) { return true, nil } } + +func (p *Sha256) Name() string { + return "sha256" +} diff --git a/pkg/crypto/unix/password/sha512.go b/pkg/crypto/unix/password/sha512.go index ee175ec..cf8790d 100644 --- a/pkg/crypto/unix/password/sha512.go +++ b/pkg/crypto/unix/password/sha512.go @@ -3,6 +3,7 @@ package password import ( "github.com/GehirnInc/crypt" "github.com/GehirnInc/crypt/sha512_crypt" + "github.com/engity-com/bifroest/pkg/errors" ) @@ -23,3 +24,7 @@ func (p *Sha512) Validate(password string, hash []byte) (bool, error) { return true, nil } } + +func (p *Sha512) Name() string { + return "sha512" +} diff --git a/pkg/crypto/unix/password/yescrypt.go b/pkg/crypto/unix/password/yescrypt.go index 8fd3442..cb9b697 100644 --- a/pkg/crypto/unix/password/yescrypt.go +++ b/pkg/crypto/unix/password/yescrypt.go @@ -27,3 +27,7 @@ func (p *Yescrypt) Validate(password string, hash []byte) (bool, error) { return C.GoString(out) == string(hash), nil } + +func (p *Yescrypt) Name() string { + return "yescrypt" +} diff --git a/pkg/environment/environment.go b/pkg/environment/environment.go index b2b764f..223a82c 100644 --- a/pkg/environment/environment.go +++ b/pkg/environment/environment.go @@ -2,8 +2,9 @@ package environment import ( "context" - "github.com/engity-com/bifroest/pkg/session" "io" + + "github.com/engity-com/bifroest/pkg/session" ) type Environment interface { diff --git a/pkg/environment/facade.go b/pkg/environment/facade-repository.go similarity index 56% rename from pkg/environment/facade.go rename to pkg/environment/facade-repository.go index bba49a6..dda9081 100644 --- a/pkg/environment/facade.go +++ b/pkg/environment/facade-repository.go @@ -3,10 +3,14 @@ package environment import ( "context" "fmt" + "reflect" + + "github.com/gliderlabs/ssh" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/configuration" + "github.com/engity-com/bifroest/pkg/errors" "github.com/engity-com/bifroest/pkg/session" - "reflect" ) func NewRepositoryFacade(ctx context.Context, flows *configuration.Flows) (*RepositoryFacade, error) { @@ -39,6 +43,15 @@ func (this *RepositoryFacade) WillBeAccepted(req Request) (bool, error) { return candidate.WillBeAccepted(req) } +func (this *RepositoryFacade) DoesSupportPty(req Request, pty ssh.Pty) (bool, error) { + flow := req.Authorization().Flow() + candidate, ok := this.entries[flow] + if !ok { + return false, fmt.Errorf("does not find valid environment for flow %v", flow) + } + return candidate.DoesSupportPty(req, pty) +} + func (this *RepositoryFacade) Ensure(req Request) (Environment, error) { flow := req.Authorization().Flow() candidate, ok := this.entries[flow] @@ -66,18 +79,33 @@ func (this *RepositoryFacade) Close() (rErr error) { func newInstance(ctx context.Context, flow *configuration.Flow) (env CloseableRepository, err error) { fail := func(err error) (CloseableRepository, error) { - return nil, fmt.Errorf("cannot initizalize environment for flow %q: %w", flow.Name, err) + return nil, errors.System.Newf("cannot initizalize environment for flow %q: %w", flow.Name, err) } - switch envConf := flow.Environment.V.(type) { - case *configuration.EnvironmentLocal: - env, err = NewLocalRepository(ctx, flow.Name, envConf) - default: - return fail(fmt.Errorf("cannot handle environment type %v", reflect.TypeOf(flow.Authorization.V))) + if flow.Environment.V == nil { + return fail(errors.Config.Newf("no environment configured")) } - if err != nil { - return fail(fmt.Errorf("cannot initizalize environment for flow %q: %w", flow.Name, err)) + factory, ok := configurationTypeToRepositoryFactory[reflect.TypeOf(flow.Environment.V)] + if !ok { + return fail(errors.Config.Newf("cannot handle environment type %v", reflect.TypeOf(flow.Environment.V))) } - return env, nil + m := reflect.ValueOf(factory) + rets := m.Call([]reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(flow.Name), reflect.ValueOf(flow.Environment.V)}) + if err, ok := rets[1].Interface().(error); ok && err != nil { + return fail(err) + } + return rets[0].Interface().(CloseableRepository), nil +} + +var ( + configurationTypeToRepositoryFactory = make(map[reflect.Type]any) +) + +type RepositoryFactory[C any, R CloseableRepository] func(ctx context.Context, flow configuration.FlowName, conf C) (R, error) + +func RegisterRepository[C any, R CloseableRepository](factory RepositoryFactory[C, R]) RepositoryFactory[C, R] { + ct := reflect.TypeFor[C]() + configurationTypeToRepositoryFactory[ct] = factory + return factory } diff --git a/pkg/environment/local-repository.go b/pkg/environment/local-repository.go index 3f51316..1e13f8a 100644 --- a/pkg/environment/local-repository.go +++ b/pkg/environment/local-repository.go @@ -1,52 +1,12 @@ package environment import ( - "context" - "encoding/json" "fmt" - log "github.com/echocat/slf4g" - "github.com/engity-com/bifroest/pkg/common" - "github.com/engity-com/bifroest/pkg/configuration" - "github.com/engity-com/bifroest/pkg/errors" - "github.com/engity-com/bifroest/pkg/session" - "github.com/engity-com/bifroest/pkg/template" - "github.com/engity-com/bifroest/pkg/user" ) -type LocalRepository struct { - flow configuration.FlowName - conf *configuration.EnvironmentLocal - - Logger log.Logger - - userRepository user.CloseableRepository -} - -func NewLocalRepository(ctx context.Context, flow configuration.FlowName, conf *configuration.EnvironmentLocal) (*LocalRepository, error) { - fail := func(err error) (*LocalRepository, error) { - return nil, err - } - failf := func(msg string, args ...any) (*LocalRepository, error) { - return fail(fmt.Errorf(msg, args...)) - } - - if conf == nil { - return failf("nil configuration") - } - - userRepository, err := user.DefaultRepositoryProvider.Create(ctx) - if err != nil { - return nil, err - } - - result := LocalRepository{ - flow: flow, - conf: conf, - userRepository: userRepository, - } - - return &result, nil -} +var ( + _ = RegisterRepository(NewLocalRepository) +) func (this *LocalRepository) WillBeAccepted(req Request) (ok bool, err error) { fail := func(err error) (bool, error) { @@ -59,280 +19,3 @@ func (this *LocalRepository) WillBeAccepted(req Request) (ok bool, err error) { return ok, nil } - -func (this *LocalRepository) Ensure(req Request) (Environment, error) { - fail := func(err error) (Environment, error) { - return nil, err - } - failf := func(t errors.Type, msg string, args ...any) (Environment, error) { - return fail(errors.Newf(t, msg, args...)) - } - - if ok, err := this.WillBeAccepted(req); err != nil { - return fail(err) - } else if !ok { - return fail(ErrNotAcceptable) - } - - sess := req.Authorization().FindSession() - if sess == nil { - return failf(errors.System, "authorization without session") - } - - if existing, err := this.FindBySession(req.Context(), sess, nil); err != nil { - if !errors.Is(err, ErrNoSuchEnvironment) { - req.Logger(). - WithError(err). - Warn("cannot restore environment from existing session; will create a new one") - } - } else { - return existing, nil - } - - ensureOpts, err := this.getEnsureOptsOf(req) - if err != nil { - return fail(err) - } - - var u *user.User - var userIsManaged bool - if !ensureOpts.canCreateOrUpdate() { - if u, err = this.lookupUserBy(req); err != nil { - return fail(err) - } - userIsManaged = false - } else { - if u, _, err = this.ensureUserByTask(req, &ensureOpts); err != nil { - return fail(err) - } - userIsManaged = true - } - - portForwardingAllowed, err := this.conf.PortForwardingAllowed.Render(req) - if err != nil { - return fail(err) - } - - deleteOnDispose, err := this.conf.Dispose.DeleteManagedUser.Render(req) - if err != nil { - return fail(err) - } - deleteHomeDirOnDispose, err := this.conf.Dispose.DeleteManagedUserHomeDir.Render(req) - if err != nil { - return fail(err) - } - killProcessesOnDispose, err := this.conf.Dispose.KillManagedUserProcesses.Render(req) - if err != nil { - return fail(err) - } - - lt := localToken{ - localTokenUser{ - u.Name, - common.P(u.Uid), - userIsManaged, - deleteOnDispose && userIsManaged, - deleteHomeDirOnDispose && deleteOnDispose && userIsManaged, - killProcessesOnDispose && userIsManaged, - }, - portForwardingAllowed, - } - if ltb, err := json.Marshal(lt); err != nil { - return failf(errors.System, "cannot marshal environment token: %w", err) - } else if err := sess.SetEnvironmentToken(req.Context(), ltb); err != nil { - return failf(errors.System, "cannot store environment token at session: %w", err) - } - - return &local{ - this, - sess, - u, - portForwardingAllowed, - lt.User.DeleteOnDispose, - lt.User.DeleteHomeDirOnDispose, - lt.User.KillProcessesOnDispose, - }, nil -} - -func (this *LocalRepository) FindBySession(ctx context.Context, sess session.Session, opts *FindOpts) (Environment, error) { - fail := func(err error) (Environment, error) { - return nil, err - } - failf := func(t errors.Type, msg string, args ...any) (Environment, error) { - return fail(errors.Newf(t, msg, args...)) - } - userNotFound := func(userRef any) (Environment, error) { - if !opts.IsAutoCleanUpAllowed() { - return failf(errors.Expired, "user %q of session cannot longer be found; treat as expired", userRef) - } - // Clear the stored token. - if err := sess.SetEnvironmentToken(ctx, nil); err != nil { - return failf(errors.System, "cannot clear existing environment token of session after its user (%v) does not seem to exist any longer: %w", userRef, err) - } - opts.GetLogger(this.logger). - With("session", sess). - With("user", userRef). - Debug("session's user does not longer seem to exist; treat environment as expired; therefore according environment token was removed from session") - return nil, ErrNoSuchEnvironment - } - - ltb, err := sess.EnvironmentToken(ctx) - if err != nil { - return failf(errors.System, "cannot get environment token: %w", err) - } - if len(ltb) == 0 { - return fail(ErrNoSuchEnvironment) - } - var tb localToken - if err := json.Unmarshal(ltb, &tb); err != nil { - return failf(errors.System, "cannot decode environment token: %w", err) - } - - var u *user.User - if v := tb.User.Name; len(v) != 0 { - if u, err = this.userRepository.LookupByName(ctx, v); errors.Is(err, user.ErrNoSuchUser) { - return userNotFound(v) - } else if err != nil { - return failf(errors.System, "cannot lookup environment's user by name %q: %w", v, err) - } - } else if v := tb.User.Uid; v != nil { - if u, err = this.userRepository.LookupById(ctx, *v); errors.Is(err, user.ErrNoSuchUser) { - return userNotFound(v) - } else if err != nil { - return failf(errors.System, "cannot lookup environment's user by id %v: %w", *v, err) - } - } else { - return failf(errors.System, "environment token does not contain valid user information: %w", err) - } - - return &local{ - this, - sess, - u, - tb.PortForwardingAllowed, - tb.User.DeleteOnDispose, - tb.User.DeleteHomeDirOnDispose, - tb.User.KillProcessesOnDispose, - }, nil -} - -type localEnsureOpts struct { - createIfAbsent bool - updateIfDifferent bool -} - -func (this localEnsureOpts) canCreateOrUpdate() bool { - return this.createIfAbsent || this.updateIfDifferent -} - -func (this *LocalRepository) getEnsureOptsOf(r Request) (result localEnsureOpts, err error) { - fail := func(err error) (localEnsureOpts, error) { - return localEnsureOpts{}, err - } - failf := func(msg string, args ...any) (localEnsureOpts, error) { - return fail(fmt.Errorf(msg, args...)) - } - - if result.createIfAbsent, err = this.conf.CreateIfAbsent.Render(r); err != nil { - return failf("cannot render createIfAbsent: %w", err) - } - - if result.updateIfDifferent, err = this.conf.UpdateIfDifferent.Render(r); err != nil { - return failf("cannot render updateIfDifferent: %w", err) - } - - return result, nil -} - -func (this *LocalRepository) lookupUserBy(req Request) (u *user.User, err error) { - fail := func(err error) (*user.User, error) { - return nil, err - } - failf := func(msg string, args ...any) (*user.User, error) { - return fail(errors.Newf(errors.System, msg, args...)) - } - - if v := this.conf.User.Name; !v.IsZero() { - if u, err = this.lookupByName(req, v); err != nil { - return fail(err) - } - } else if v := this.conf.User.Uid; v != nil { - if u, err = this.lookupByUid(req, *v); err != nil { - return fail(err) - } - } else { - return failf("the system isn't allowed to update nor create users and there is neither a user name nor user id configured") - } - - return u, nil -} - -func (this *LocalRepository) ensureUserByTask(r Request, opts *localEnsureOpts) (*user.User, user.EnsureResult, error) { - fail := func(err error) (*user.User, user.EnsureResult, error) { - return nil, 0, err - } - failf := func(msg string, args ...any) (*user.User, user.EnsureResult, error) { - return fail(fmt.Errorf(msg, args...)) - } - - req, err := this.conf.User.Render(nil, r) - if err != nil { - return failf("cannot render user requirement: %w", err) - } - - return this.ensureUser(r.Context(), req, opts) -} - -func (this *LocalRepository) lookupByUid(r Request, tmpl template.TextMarshaller[user.Id, *user.Id]) (*user.User, error) { - fail := func(err error) (*user.User, error) { - return nil, err - } - failf := func(msg string, args ...any) (*user.User, error) { - return fail(fmt.Errorf(msg, args...)) - } - - uid, err := tmpl.Render(r) - if err != nil { - return failf("cannot render UID: %w", err) - } - - return this.userRepository.LookupById(r.Context(), uid) -} - -func (this *LocalRepository) lookupByName(r Request, tmpl template.String) (*user.User, error) { - fail := func(err error) (*user.User, error) { - return nil, err - } - failf := func(msg string, args ...any) (*user.User, error) { - return fail(fmt.Errorf(msg, args...)) - } - - name, err := tmpl.Render(r) - if err != nil { - return failf("cannot render user name: %w", err) - } - - return this.userRepository.LookupByName(r.Context(), name) -} - -func (this *LocalRepository) ensureUser(ctx context.Context, req *user.Requirement, opts *localEnsureOpts) (u *user.User, er user.EnsureResult, err error) { - u, er, err = this.userRepository.Ensure(ctx, req, &user.EnsureOpts{ - CreateAllowed: &opts.createIfAbsent, - ModifyAllowed: &opts.updateIfDifferent, - }) - if err != nil { - return nil, 0, fmt.Errorf("cannot ensure user: %w", err) - } - return u, er, nil -} - -func (this *LocalRepository) Close() error { - return this.userRepository.Close() -} - -func (this *LocalRepository) logger() log.Logger { - if v := this.Logger; v != nil { - return v - } - return log.GetLogger("authorizer") -} diff --git a/pkg/environment/local-repository_unix.go b/pkg/environment/local-repository_unix.go new file mode 100644 index 0000000..0f28611 --- /dev/null +++ b/pkg/environment/local-repository_unix.go @@ -0,0 +1,301 @@ +//go:build unix + +package environment + +import ( + "context" + "encoding/json" + "fmt" + + log "github.com/echocat/slf4g" + "github.com/gliderlabs/ssh" + + "github.com/engity-com/bifroest/pkg/configuration" + "github.com/engity-com/bifroest/pkg/errors" + "github.com/engity-com/bifroest/pkg/session" + "github.com/engity-com/bifroest/pkg/template" + "github.com/engity-com/bifroest/pkg/user" +) + +var ( + _ = RegisterRepository(NewLocalRepository) +) + +type LocalRepository struct { + flow configuration.FlowName + conf *configuration.EnvironmentLocal + + Logger log.Logger + + userRepository user.CloseableRepository +} + +func NewLocalRepository(ctx context.Context, flow configuration.FlowName, conf *configuration.EnvironmentLocal) (*LocalRepository, error) { + fail := func(err error) (*LocalRepository, error) { + return nil, err + } + failf := func(msg string, args ...any) (*LocalRepository, error) { + return fail(fmt.Errorf(msg, args...)) + } + + if conf == nil { + return failf("nil configuration") + } + + userRepository, err := user.DefaultRepositoryProvider.Create(ctx) + if err != nil { + return nil, err + } + + result := LocalRepository{ + flow: flow, + conf: conf, + userRepository: userRepository, + } + + return &result, nil +} + +func (this *LocalRepository) DoesSupportPty(Request, ssh.Pty) (bool, error) { + return true, nil +} + +func (this *LocalRepository) Ensure(req Request) (Environment, error) { + fail := func(err error) (Environment, error) { + return nil, err + } + failf := func(t errors.Type, msg string, args ...any) (Environment, error) { + return fail(errors.Newf(t, msg, args...)) + } + + if ok, err := this.WillBeAccepted(req); err != nil { + return fail(err) + } else if !ok { + return fail(ErrNotAcceptable) + } + + sess := req.Authorization().FindSession() + if sess == nil { + return failf(errors.System, "authorization without session") + } + + if existing, err := this.FindBySession(req.Context(), sess, nil); err != nil { + if !errors.Is(err, ErrNoSuchEnvironment) { + req.Logger(). + WithError(err). + Warn("cannot restore environment from existing session; will create a new one") + } + } else { + return existing, nil + } + + ensureOpts, err := this.getEnsureOptsOf(req) + if err != nil { + return fail(err) + } + + var u *user.User + var userIsManaged bool + if !ensureOpts.canCreateOrUpdate() { + if u, err = this.lookupUserBy(req); err != nil { + return fail(err) + } + userIsManaged = false + } else { + if u, _, err = this.ensureUserByTask(req, &ensureOpts); err != nil { + return fail(err) + } + userIsManaged = true + } + + lt, err := this.newLocalToken(u, req, userIsManaged) + if err != nil { + return fail(err) + } + portForwardingAllowed, err := this.conf.PortForwardingAllowed.Render(req) + if err != nil { + return fail(err) + } + if ltb, err := json.Marshal(lt); err != nil { + return failf(errors.System, "cannot marshal environment token: %w", err) + } else if err := sess.SetEnvironmentToken(req.Context(), ltb); err != nil { + return failf(errors.System, "cannot store environment token at session: %w", err) + } + + return this.new(u, sess, portForwardingAllowed, lt), nil +} + +func (this *LocalRepository) FindBySession(ctx context.Context, sess session.Session, opts *FindOpts) (Environment, error) { + fail := func(err error) (Environment, error) { + return nil, err + } + failf := func(t errors.Type, msg string, args ...any) (Environment, error) { + return fail(errors.Newf(t, msg, args...)) + } + userNotFound := func(userRef any) (Environment, error) { + if !opts.IsAutoCleanUpAllowed() { + return failf(errors.Expired, "user %q of session cannot longer be found; treat as expired", userRef) + } + // Clear the stored token. + if err := sess.SetEnvironmentToken(ctx, nil); err != nil { + return failf(errors.System, "cannot clear existing environment token of session after its user (%v) does not seem to exist any longer: %w", userRef, err) + } + opts.GetLogger(this.logger). + With("session", sess). + With("user", userRef). + Debug("session's user does not longer seem to exist; treat environment as expired; therefore according environment token was removed from session") + return nil, ErrNoSuchEnvironment + } + + ltb, err := sess.EnvironmentToken(ctx) + if err != nil { + return failf(errors.System, "cannot get environment token: %w", err) + } + if len(ltb) == 0 { + return fail(ErrNoSuchEnvironment) + } + var lt localToken + if err := json.Unmarshal(ltb, <); err != nil { + return failf(errors.System, "cannot decode environment token: %w", err) + } + + var u *user.User + if v := lt.User.Name; len(v) != 0 { + if u, err = this.userRepository.LookupByName(ctx, v); errors.Is(err, user.ErrNoSuchUser) { + return userNotFound(v) + } else if err != nil { + return failf(errors.System, "cannot lookup environment's user by name %q: %w", v, err) + } + } else if v := lt.User.Uid; v != nil { + if u, err = this.userRepository.LookupById(ctx, *v); errors.Is(err, user.ErrNoSuchUser) { + return userNotFound(v) + } else if err != nil { + return failf(errors.System, "cannot lookup environment's user by id %v: %w", *v, err) + } + } else { + return failf(errors.System, "environment token does not contain valid user information: %w", err) + } + + return this.new(u, sess, lt.PortForwardingAllowed, <), nil +} + +type localEnsureOpts struct { + createIfAbsent bool + updateIfDifferent bool +} + +func (this localEnsureOpts) canCreateOrUpdate() bool { + return this.createIfAbsent || this.updateIfDifferent +} + +func (this *LocalRepository) getEnsureOptsOf(r Request) (result localEnsureOpts, err error) { + fail := func(err error) (localEnsureOpts, error) { + return localEnsureOpts{}, err + } + failf := func(msg string, args ...any) (localEnsureOpts, error) { + return fail(fmt.Errorf(msg, args...)) + } + + if result.createIfAbsent, err = this.conf.CreateIfAbsent.Render(r); err != nil { + return failf("cannot render createIfAbsent: %w", err) + } + + if result.updateIfDifferent, err = this.conf.UpdateIfDifferent.Render(r); err != nil { + return failf("cannot render updateIfDifferent: %w", err) + } + + return result, nil +} + +func (this *LocalRepository) lookupUserBy(req Request) (u *user.User, err error) { + fail := func(err error) (*user.User, error) { + return nil, err + } + failf := func(msg string, args ...any) (*user.User, error) { + return fail(errors.Newf(errors.System, msg, args...)) + } + + if v := this.conf.User.Name; !v.IsZero() { + if u, err = this.lookupByName(req, v); err != nil { + return fail(err) + } + } else if v := this.conf.User.Uid; v != nil { + if u, err = this.lookupByUid(req, *v); err != nil { + return fail(err) + } + } else { + return failf("the system isn't allowed to update nor create users and there is neither a user name nor user id configured") + } + + return u, nil +} + +func (this *LocalRepository) ensureUserByTask(r Request, opts *localEnsureOpts) (*user.User, user.EnsureResult, error) { + fail := func(err error) (*user.User, user.EnsureResult, error) { + return nil, 0, err + } + failf := func(msg string, args ...any) (*user.User, user.EnsureResult, error) { + return fail(fmt.Errorf(msg, args...)) + } + + req, err := this.conf.User.Render(nil, r) + if err != nil { + return failf("cannot render user requirement: %w", err) + } + + return this.ensureUser(r.Context(), req, opts) +} + +func (this *LocalRepository) lookupByUid(r Request, tmpl template.TextMarshaller[user.Id, *user.Id]) (*user.User, error) { + fail := func(err error) (*user.User, error) { + return nil, err + } + failf := func(msg string, args ...any) (*user.User, error) { + return fail(fmt.Errorf(msg, args...)) + } + + uid, err := tmpl.Render(r) + if err != nil { + return failf("cannot render UID: %w", err) + } + + return this.userRepository.LookupById(r.Context(), uid) +} + +func (this *LocalRepository) lookupByName(r Request, tmpl template.String) (*user.User, error) { + fail := func(err error) (*user.User, error) { + return nil, err + } + failf := func(msg string, args ...any) (*user.User, error) { + return fail(fmt.Errorf(msg, args...)) + } + + name, err := tmpl.Render(r) + if err != nil { + return failf("cannot render user name: %w", err) + } + + return this.userRepository.LookupByName(r.Context(), name) +} + +func (this *LocalRepository) ensureUser(ctx context.Context, req *user.Requirement, opts *localEnsureOpts) (u *user.User, er user.EnsureResult, err error) { + u, er, err = this.userRepository.Ensure(ctx, req, &user.EnsureOpts{ + CreateAllowed: &opts.createIfAbsent, + ModifyAllowed: &opts.updateIfDifferent, + }) + if err != nil { + return nil, 0, fmt.Errorf("cannot ensure user: %w", err) + } + return u, er, nil +} + +func (this *LocalRepository) Close() error { + return this.userRepository.Close() +} + +func (this *LocalRepository) logger() log.Logger { + if v := this.Logger; v != nil { + return v + } + return log.GetLogger("authorizer") +} diff --git a/pkg/environment/local-repository_windows.go b/pkg/environment/local-repository_windows.go new file mode 100644 index 0000000..c42faba --- /dev/null +++ b/pkg/environment/local-repository_windows.go @@ -0,0 +1,120 @@ +//go:build windows + +package environment + +import ( + "context" + "encoding/json" + "fmt" + + log "github.com/echocat/slf4g" + "github.com/gliderlabs/ssh" + + "github.com/engity-com/bifroest/pkg/configuration" + "github.com/engity-com/bifroest/pkg/errors" + "github.com/engity-com/bifroest/pkg/session" +) + +type LocalRepository struct { + flow configuration.FlowName + conf *configuration.EnvironmentLocal + + Logger log.Logger +} + +func NewLocalRepository(_ context.Context, flow configuration.FlowName, conf *configuration.EnvironmentLocal) (*LocalRepository, error) { + fail := func(err error) (*LocalRepository, error) { + return nil, err + } + failf := func(msg string, args ...any) (*LocalRepository, error) { + return fail(fmt.Errorf(msg, args...)) + } + + if conf == nil { + return failf("nil configuration") + } + + result := LocalRepository{ + flow: flow, + conf: conf, + } + + return &result, nil +} + +func (this *LocalRepository) DoesSupportPty(Request, ssh.Pty) (bool, error) { + return false, nil +} + +func (this *LocalRepository) Ensure(req Request) (Environment, error) { + fail := func(err error) (Environment, error) { + return nil, err + } + failf := func(t errors.Type, msg string, args ...any) (Environment, error) { + return fail(errors.Newf(t, msg, args...)) + } + + if ok, err := this.WillBeAccepted(req); err != nil { + return fail(err) + } else if !ok { + return fail(ErrNotAcceptable) + } + + sess := req.Authorization().FindSession() + if sess == nil { + return failf(errors.System, "authorization without session") + } + + if existing, err := this.FindBySession(req.Context(), sess, nil); err != nil { + if !errors.Is(err, ErrNoSuchEnvironment) { + req.Logger(). + WithError(err). + Warn("cannot restore environment from existing session; will create a new one") + } + } else { + return existing, nil + } + + lt, err := this.newLocalToken(req) + if err != nil { + return fail(err) + } + portForwardingAllowed, err := this.conf.PortForwardingAllowed.Render(req) + if err != nil { + return fail(err) + } + if ltb, err := json.Marshal(lt); err != nil { + return failf(errors.System, "cannot marshal environment token: %w", err) + } else if err := sess.SetEnvironmentToken(req.Context(), ltb); err != nil { + return failf(errors.System, "cannot store environment token at session: %w", err) + } + + return this.new(sess, portForwardingAllowed), nil +} + +func (this *LocalRepository) FindBySession(ctx context.Context, sess session.Session, _ *FindOpts) (Environment, error) { + fail := func(err error) (Environment, error) { + return nil, err + } + failf := func(t errors.Type, msg string, args ...any) (Environment, error) { + return fail(errors.Newf(t, msg, args...)) + } + + ltb, err := sess.EnvironmentToken(ctx) + if err != nil { + return failf(errors.System, "cannot get environment token: %w", err) + } + if len(ltb) == 0 { + return fail(ErrNoSuchEnvironment) + } + var lt localToken + if err := json.Unmarshal(ltb, <); err != nil { + return failf(errors.System, "cannot decode environment token: %w", err) + } + + return this.new(sess, lt.PortForwardingAllowed), nil +} + +func (this *LocalRepository) Close() error { + return nil +} diff --git a/pkg/environment/local-token.go b/pkg/environment/local-token.go deleted file mode 100644 index 1e256e8..0000000 --- a/pkg/environment/local-token.go +++ /dev/null @@ -1,17 +0,0 @@ -package environment - -import "github.com/engity-com/bifroest/pkg/user" - -type localToken struct { - User localTokenUser `json:"user"` - PortForwardingAllowed bool `json:"portForwardingAllowed"` -} - -type localTokenUser struct { - Name string `json:"name,omitempty"` - Uid *user.Id `json:"uid,omitempty"` - Managed bool `json:"managed,omitempty"` - DeleteOnDispose bool `json:"deleteOnDispose,omitempty"` - DeleteHomeDirOnDispose bool `json:"deleteHomeDirOnDispose,omitempty"` - KillProcessesOnDispose bool `json:"killProcessesOnDispose,omitempty"` -} diff --git a/pkg/environment/local-token_unix.go b/pkg/environment/local-token_unix.go new file mode 100644 index 0000000..4ab9824 --- /dev/null +++ b/pkg/environment/local-token_unix.go @@ -0,0 +1,58 @@ +//go:build unix + +package environment + +import ( + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/user" +) + +type localToken struct { + User localTokenUser `json:"user"` + PortForwardingAllowed bool `json:"portForwardingAllowed"` +} + +type localTokenUser struct { + Name string `json:"name,omitempty"` + Uid *user.Id `json:"uid,omitempty"` + Managed bool `json:"managed,omitempty"` + DeleteOnDispose bool `json:"deleteOnDispose,omitempty"` + DeleteHomeDirOnDispose bool `json:"deleteHomeDirOnDispose,omitempty"` + KillProcessesOnDispose bool `json:"killProcessesOnDispose,omitempty"` +} + +func (this *LocalRepository) newLocalToken(u *user.User, req Request, userIsManaged bool) (*localToken, error) { + fail := func(err error) (*localToken, error) { + return nil, err + } + + portForwardingAllowed, err := this.conf.PortForwardingAllowed.Render(req) + if err != nil { + return fail(err) + } + + deleteOnDispose, err := this.conf.Dispose.DeleteManagedUser.Render(req) + if err != nil { + return fail(err) + } + deleteHomeDirOnDispose, err := this.conf.Dispose.DeleteManagedUserHomeDir.Render(req) + if err != nil { + return fail(err) + } + killProcessesOnDispose, err := this.conf.Dispose.KillManagedUserProcesses.Render(req) + if err != nil { + return fail(err) + } + + return &localToken{ + localTokenUser{ + u.Name, + common.P(u.Uid), + userIsManaged, + deleteOnDispose && userIsManaged, + deleteHomeDirOnDispose && deleteOnDispose && userIsManaged, + killProcessesOnDispose && userIsManaged, + }, + portForwardingAllowed, + }, nil +} diff --git a/pkg/environment/local-token_windows.go b/pkg/environment/local-token_windows.go new file mode 100644 index 0000000..12eb5fb --- /dev/null +++ b/pkg/environment/local-token_windows.go @@ -0,0 +1,22 @@ +//go:build windows + +package environment + +type localToken struct { + PortForwardingAllowed bool `json:"portForwardingAllowed"` +} + +func (this *LocalRepository) newLocalToken(req Request) (*localToken, error) { + fail := func(err error) (*localToken, error) { + return nil, err + } + + portForwardingAllowed, err := this.conf.PortForwardingAllowed.Render(req) + if err != nil { + return fail(err) + } + + return &localToken{ + portForwardingAllowed, + }, nil +} diff --git a/pkg/environment/local.go b/pkg/environment/local.go index a233dae..bfedbef 100644 --- a/pkg/environment/local.go +++ b/pkg/environment/local.go @@ -3,36 +3,25 @@ package environment import ( "context" "fmt" - "github.com/creack/pty" - log "github.com/echocat/slf4g" - "github.com/echocat/slf4g/level" - "github.com/engity-com/bifroest/pkg/common" - "github.com/engity-com/bifroest/pkg/errors" - "github.com/engity-com/bifroest/pkg/session" - "github.com/engity-com/bifroest/pkg/sys" - "github.com/engity-com/bifroest/pkg/user" - "github.com/gliderlabs/ssh" - "github.com/kardianos/osext" "io" "net" "os" "os/exec" - "path/filepath" "strconv" "strings" "sync" "syscall" -) -type local struct { - repository *LocalRepository - session session.Session - user *user.User - portForwardingAllowed bool - deleteUserOnDispose bool - deleteUserHomeDirOnDispose bool - killUserProcessesOnDispose bool -} + "github.com/creack/pty" + log "github.com/echocat/slf4g" + "github.com/echocat/slf4g/level" + "github.com/gliderlabs/ssh" + "github.com/kardianos/osext" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/errors" + "github.com/engity-com/bifroest/pkg/session" +) func (this *local) Session() session.Session { return this.session @@ -49,27 +38,17 @@ func (this *local) Banner(req Request) (io.ReadCloser, error) { func (this *local) Run(t Task) (exitCode int, rErr error) { l := t.Logger() - creds := this.user.ToCredentials() sshSess := t.SshSession() - cmd := exec.Cmd{ - Dir: this.user.HomeDir, - SysProcAttr: &syscall.SysProcAttr{ - Credential: &creds, - }, - } - - ev := sys.EnvVars{ - "PATH": this.getPathEnv(), + cmd, ev, err := this.createCmdAndEnv(t) + if err != nil { + return -1, err } switch t.TaskType() { case TaskTypeShell: - cmd.Path = this.user.Shell - if rc := t.SshSession().RawCommand(); len(rc) > 0 { - cmd.Args = []string{filepath.Base(this.user.Shell), "-c", rc} - } else { - cmd.Args = []string{"-" + filepath.Base(this.user.Shell)} + if err := this.configureShellCmd(t, cmd); err != nil { + return -1, err } case TaskTypeSftp: efn, err := osext.Executable() @@ -82,18 +61,6 @@ func (this *local) Run(t Task) (exitCode int, rErr error) { return -1, fmt.Errorf("illegal task type: %v", t.TaskType()) } - if v, ok := os.LookupEnv("TZ"); ok { - ev.Set("TZ", v) - } - ev.AddAllOf(t.Authorization().EnvVars()) - ev.Add(sshSess.Environ()...) - ev.Set( - "HOME", this.user.HomeDir, - "USER", this.user.Name, - "LOGNAME", this.user.Name, - "SHELL", this.user.Shell, - ) - if ssh.AgentRequested(sshSess) { l, err := ssh.NewAgentListener() if err != nil { @@ -104,9 +71,6 @@ func (this *local) Run(t Task) (exitCode int, rErr error) { cmd.Env = append(cmd.Env, "SSH_AUTH_SOCK"+l.Addr().String()) } - // TODO! Global configuration with environment - // tODO! If not exist ~/.hushlogin display /etc/motd - cmd.Stdin = sshSess cmd.Stdout = sshSess if t.TaskType() == TaskTypeSftp { @@ -117,11 +81,9 @@ func (this *local) Run(t Task) (exitCode int, rErr error) { } else { cmd.Stderr = sshSess.Stderr() } - this.configureCmd(&cmd) var fPty, fTty *os.File if ptyReq, winCh, isPty := sshSess.Pty(); isPty { - ev.Set("TERM", ptyReq.Term) var err error fPty, fTty, err = pty.Open() if err != nil { @@ -129,7 +91,8 @@ func (this *local) Run(t Task) (exitCode int, rErr error) { } defer common.IgnoreCloseError(fPty) defer common.IgnoreCloseError(fTty) - if err := this.configureCmdForPty(&cmd, fPty, fTty); err != nil { + ev.Set("TERM", ptyReq.Term) + if err := this.configureCmdForPty(cmd, fPty, fTty); err != nil { return -1, fmt.Errorf("cannot configure cmd for pty: %w", err) } cmd.Stderr = fTty @@ -142,7 +105,7 @@ func (this *local) Run(t Task) (exitCode int, rErr error) { if !ok { return } - size := pty.Winsize{uint16(win.Height), uint16(win.Width), 0, 0} + size := pty.Winsize{Rows: uint16(win.Height), Cols: uint16(win.Width)} if err := pty.Setsize(fPty, &size); err != nil { l.WithError(err).Warn("cannot set winsize; ignoring") } @@ -202,12 +165,12 @@ func (this *local) Run(t Task) (exitCode int, rErr error) { }() sshSess.Signals(signals) - defer this.kill(&cmd, l) + defer this.kill(cmd, l) for { select { case s, ok := <-signals: if ok { - this.signal(&cmd, l, s) + this.signal(cmd, l, s) } case <-t.Context().Done(): if err := t.Context().Err(); err != nil && rErr == nil { @@ -235,18 +198,9 @@ func (this *local) Dispose(ctx context.Context) (bool, error) { return false, errors.Newf(errors.System, "cannot dispose environment: %w", err) } - disposed := false - if this.deleteUserOnDispose { - if err := this.repository.userRepository.DeleteById(ctx, this.user.Uid, &user.DeleteOpts{ - HomeDir: common.P(this.deleteUserHomeDirOnDispose), - KillProcesses: common.P(this.killUserProcessesOnDispose), - }); errors.Is(err, user.ErrNoSuchUser) { - // Ok, continue.... - } else if err != nil { - return fail(err) - } else { - disposed = true - } + disposed, err := this.dispose(ctx) + if err != nil { + return fail(err) } sess := this.session @@ -265,7 +219,7 @@ func (this *local) isRelevantError(err error) bool { func (this *local) kill(cmd *exec.Cmd, logger log.Logger) { // TODO! We should consider the whole tree... - if err := cmd.Process.Kill(); errors.Is(err, os.ErrProcessDone) { + if err := cmd.Process.Kill(); errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.EINVAL) { // Ok, great. } else if err != nil { logger.WithError(err). diff --git a/pkg/environment/local_linux.go b/pkg/environment/local_linux.go deleted file mode 100644 index dab2a28..0000000 --- a/pkg/environment/local_linux.go +++ /dev/null @@ -1,49 +0,0 @@ -package environment - -import ( - log "github.com/echocat/slf4g" - "github.com/engity-com/bifroest/pkg/errors" - "github.com/engity-com/bifroest/pkg/sys" - "github.com/gliderlabs/ssh" - "os" - "os/exec" - "syscall" -) - -func (this *local) configureCmd(_ *exec.Cmd) {} - -func (this *local) configureCmdForPty(cmd *exec.Cmd, pty, tty *os.File) error { - cmd.SysProcAttr.Setsid = true - cmd.SysProcAttr.Setctty = true - - if err := syscall.SetNonblock(int(pty.Fd()), true); err != nil { - return err - } - if err := syscall.SetNonblock(int(tty.Fd()), true); err != nil { - return err - } - return nil -} - -func (this *local) getPathEnv() string { - if v := os.Getenv("PATH"); v != "" { - return v - } - return "/bin;/usr/bin" -} - -func (this *local) signal(cmd *exec.Cmd, logger log.Logger, signal ssh.Signal) { - var sig sys.Signal - if err := sig.Set(string(signal)); err != nil { - sig = sys.SIGKILL - } - - if err := cmd.Process.Signal(sig.Native()); errors.Is(err, os.ErrProcessDone) { - // Ignored. - } else if err != nil { - logger.WithError(err). - With("pid", cmd.Process.Pid). - With("signal", sig). - Warn("cannot send signal to process") - } -} diff --git a/pkg/environment/local_unix.go b/pkg/environment/local_unix.go new file mode 100644 index 0000000..77d48ff --- /dev/null +++ b/pkg/environment/local_unix.go @@ -0,0 +1,140 @@ +//go:build unix + +package environment + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "syscall" + + log "github.com/echocat/slf4g" + "github.com/gliderlabs/ssh" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/errors" + "github.com/engity-com/bifroest/pkg/session" + "github.com/engity-com/bifroest/pkg/sys" + "github.com/engity-com/bifroest/pkg/user" +) + +type local struct { + repository *LocalRepository + session session.Session + user *user.User + portForwardingAllowed bool + deleteUserOnDispose bool + deleteUserHomeDirOnDispose bool + killUserProcessesOnDispose bool +} + +func (this *LocalRepository) new(u *user.User, sess session.Session, portForwardingAllowed bool, lt *localToken) *local { + return &local{ + this, + sess, + u, + portForwardingAllowed, + lt.User.DeleteOnDispose, + lt.User.DeleteHomeDirOnDispose, + lt.User.KillProcessesOnDispose, + } +} + +func (this *local) configureShellCmd(t Task, cmd *exec.Cmd) error { + cmd.Path = this.user.Shell + if rc := t.SshSession().RawCommand(); len(rc) > 0 { + cmd.Args = []string{filepath.Base(this.user.Shell), "-c", rc} + } else { + cmd.Args = []string{"-" + filepath.Base(this.user.Shell)} + } + return nil +} + +func (this *local) createCmdAndEnv(t Task) (*exec.Cmd, *sys.EnvVars, error) { + creds := this.user.ToCredentials() + cmd := exec.Cmd{ + Dir: this.user.HomeDir, + SysProcAttr: &syscall.SysProcAttr{ + Credential: &creds, + }, + } + + ev := sys.EnvVars{ + "PATH": this.getPathEnv(), + } + if v, ok := os.LookupEnv("TZ"); ok { + ev.Set("TZ", v) + } + ev.AddAllOf(t.Authorization().EnvVars()) + ev.Add(t.SshSession().Environ()...) + ev.Set( + "HOME", this.user.HomeDir, + "USER", this.user.Name, + "LOGNAME", this.user.Name, + "SHELL", this.user.Shell, + ) + + // TODO! Global configuration with environment + // tODO! If not exist ~/.hushlogin display /etc/motd + + return &cmd, &ev, nil +} + +func (this *local) configureCmdForPty(cmd *exec.Cmd, pty, tty *os.File) error { + cmd.SysProcAttr.Setsid = true + cmd.SysProcAttr.Setctty = true + + if err := syscall.SetNonblock(int(pty.Fd()), true); err != nil { + return err + } + if err := syscall.SetNonblock(int(tty.Fd()), true); err != nil { + return err + } + return nil +} + +func (this *local) getPathEnv() string { + if v := os.Getenv("PATH"); v != "" { + return v + } + return "/bin:/usr/bin" +} + +func (this *local) signal(cmd *exec.Cmd, logger log.Logger, signal ssh.Signal) { + var sig sys.Signal + if err := sig.Set(string(signal)); err != nil { + sig = sys.SIGKILL + } + + if err := cmd.Process.Signal(sig.Native()); errors.Is(err, os.ErrProcessDone) { + // Ignored. + } else if err != nil { + logger.WithError(err). + With("pid", cmd.Process.Pid). + With("signal", sig). + Warn("cannot send signal to process") + } +} + +func (this *local) dispose(ctx context.Context) (bool, error) { + fail := func(err error) (bool, error) { + return false, err + } + + disposed := false + if this.deleteUserOnDispose { + if err := this.repository.userRepository.DeleteById(ctx, this.user.Uid, &user.DeleteOpts{ + HomeDir: common.P(this.deleteUserHomeDirOnDispose), + KillProcesses: common.P(this.killUserProcessesOnDispose), + }); errors.Is(err, user.ErrNoSuchUser) { + // Ok, continue.... + } else if err != nil { + return fail(err) + } else { + disposed = true + } + } + + return disposed, nil +} diff --git a/pkg/environment/local_windows.go b/pkg/environment/local_windows.go new file mode 100644 index 0000000..dc11bd0 --- /dev/null +++ b/pkg/environment/local_windows.go @@ -0,0 +1,141 @@ +//go:build windows + +package environment + +import ( + "context" + "os" + "os/exec" + "syscall" + + log "github.com/echocat/slf4g" + "github.com/gliderlabs/ssh" + + "github.com/engity-com/bifroest/pkg/configuration" + "github.com/engity-com/bifroest/pkg/errors" + "github.com/engity-com/bifroest/pkg/session" + "github.com/engity-com/bifroest/pkg/sys" + "github.com/engity-com/bifroest/pkg/template" +) + +type local struct { + repository *LocalRepository + session session.Session + portForwardingAllowed bool +} + +func (this *LocalRepository) new(sess session.Session, portForwardingAllowed bool) *local { + return &local{ + this, + sess, + portForwardingAllowed, + } +} + +func (this *local) createCmdAndEnv(t Task) (*exec.Cmd, *sys.EnvVars, error) { + dir, err := this.repository.conf.Directory.Render(t) + if err != nil { + return nil, nil, errors.Config.Newf("cannot evaluate environment's directory: %w", err) + } + fi, err := os.Stat(dir) + if err != nil { + return nil, nil, errors.Config.Newf("cannot evaluate environment's directory (%q): %w", dir, err) + } + if !fi.IsDir() { + return nil, nil, errors.Config.Newf("environment's directory (%q) isn't a directory", dir) + } + + cmd := exec.Cmd{ + Dir: dir, + SysProcAttr: &syscall.SysProcAttr{}, + } + + ev := sys.EnvVars{ + "PATH": this.getPathEnv(), + } + if v, ok := os.LookupEnv("TZ"); ok { + ev.Set("TZ", v) + } + ev.AddAllOf(t.Authorization().EnvVars()) + ev.Add(t.SshSession().Environ()...) + + return &cmd, &ev, nil +} + +func (this *local) configureShellCmd(t Task, cmd *exec.Cmd) error { + var argSource *template.Strings + var argName string + + rc := t.SshSession().RawCommand() + if len(rc) > 0 { + argSource = &this.repository.conf.ExecCommandPrefix + argName = "execCommandPrefix" + } else { + argSource = &this.repository.conf.ShellCommand + argName = "shellCommand" + } + + args, err := this.evaluateCommand(t, argName, argSource) + if err != nil { + return err + } + + cmd.Path = args[0] + cmd.Args = append(args, rc) + + return nil +} + +func (this *local) evaluateCommand(t Task, name string, tmpl *template.Strings) ([]string, error) { + args, err := tmpl.Render(t) + if err != nil { + return nil, errors.Config.Newf("cannot evaluate environment's %s: %w", name, err) + } + if len(args) < 1 { + args = []string{configuration.DefaultShell} + } + + args[0], err = exec.LookPath(args[0]) + if err != nil { + return nil, errors.Config.Newf("cannot evaluate environment's %s executable (%q): %w", name, args[0], err) + } + + return args, nil +} + +func (this *local) configureCmdForPty(_ *exec.Cmd, pty, tty *os.File) error { + if err := syscall.SetNonblock(syscall.Handle(int(pty.Fd())), true); err != nil { + return err + } + if err := syscall.SetNonblock(syscall.Handle(int(tty.Fd())), true); err != nil { + return err + } + return nil +} + +func (this *local) getPathEnv() string { + if v := os.Getenv("PATH"); v != "" { + return v + } + return `C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem` +} + +func (this *local) signal(cmd *exec.Cmd, logger log.Logger, signal ssh.Signal) { + var sig sys.Signal + if err := sig.Set(string(signal)); err != nil { + sig = sys.SIGKILL + } + + if err := cmd.Process.Signal(sig.Native()); errors.Is(err, os.ErrProcessDone) { + // Ignored. + } else if err != nil { + logger.WithError(err). + With("pid", cmd.Process.Pid). + With("signal", sig). + Warn("cannot send signal to process") + } +} + +func (this *local) dispose(_ context.Context) (bool, error) { + return true, nil +} diff --git a/pkg/environment/repository.go b/pkg/environment/repository.go index c7ebf7c..9ff5a1b 100644 --- a/pkg/environment/repository.go +++ b/pkg/environment/repository.go @@ -3,9 +3,12 @@ package environment import ( "context" "errors" + "io" + log "github.com/echocat/slf4g" + "github.com/gliderlabs/ssh" + "github.com/engity-com/bifroest/pkg/session" - "io" ) var ( @@ -18,6 +21,10 @@ type Repository interface { // provided Request. WillBeAccepted(Request) (bool, error) + // DoesSupportPty will return true if the resulting Environment will support + // an PTY. + DoesSupportPty(Request, ssh.Pty) (bool, error) + // Ensure will create or return an environment that matches the given Request. // If it is not acceptable to do this action with the provided Request // ErrNotAcceptable is returned; you can call WillBeAccepted to prevent such diff --git a/pkg/environment/request.go b/pkg/environment/request.go index 0863baf..a353d06 100644 --- a/pkg/environment/request.go +++ b/pkg/environment/request.go @@ -2,9 +2,10 @@ package environment import ( log "github.com/echocat/slf4g" + "github.com/gliderlabs/ssh" + "github.com/engity-com/bifroest/pkg/authorization" "github.com/engity-com/bifroest/pkg/common" - "github.com/gliderlabs/ssh" ) type Request interface { diff --git a/pkg/environment/task.go b/pkg/environment/task.go index d0055b3..f2b3de1 100644 --- a/pkg/environment/task.go +++ b/pkg/environment/task.go @@ -2,6 +2,7 @@ package environment import ( "fmt" + "github.com/gliderlabs/ssh" ) diff --git a/pkg/service/connection.go b/pkg/service/connection.go index f444881..ec78201 100644 --- a/pkg/service/connection.go +++ b/pkg/service/connection.go @@ -2,17 +2,19 @@ package service import ( "fmt" - log "github.com/echocat/slf4g" - "github.com/echocat/slf4g/fields" - "github.com/engity-com/bifroest/pkg/authorization" - "github.com/engity-com/bifroest/pkg/errors" - "github.com/engity-com/bifroest/pkg/session" - "github.com/gliderlabs/ssh" "net" "os" "strconv" "sync/atomic" "time" + + log "github.com/echocat/slf4g" + "github.com/echocat/slf4g/fields" + "github.com/gliderlabs/ssh" + + "github.com/engity-com/bifroest/pkg/authorization" + "github.com/engity-com/bifroest/pkg/errors" + "github.com/engity-com/bifroest/pkg/session" ) func (this *service) onNewConnConnection(ctx ssh.Context, orig net.Conn) net.Conn { diff --git a/pkg/service/context.go b/pkg/service/context.go index fccf5fa..e2663e7 100644 --- a/pkg/service/context.go +++ b/pkg/service/context.go @@ -2,15 +2,17 @@ package service import ( "fmt" + "io" + log "github.com/echocat/slf4g" + "github.com/gliderlabs/ssh" + gssh "golang.org/x/crypto/ssh" + "github.com/engity-com/bifroest/pkg/authorization" "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/environment" "github.com/engity-com/bifroest/pkg/net" "github.com/engity-com/bifroest/pkg/session" - "github.com/gliderlabs/ssh" - gssh "golang.org/x/crypto/ssh" - "io" ) type remote struct { diff --git a/pkg/service/housekeeper.go b/pkg/service/housekeeper.go index f2e7532..af39f12 100644 --- a/pkg/service/housekeeper.go +++ b/pkg/service/housekeeper.go @@ -2,14 +2,17 @@ package service import ( "context" + "fmt" + "sync/atomic" + "time" + log "github.com/echocat/slf4g" + "github.com/engity-com/bifroest/pkg/authorization" "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/environment" "github.com/engity-com/bifroest/pkg/errors" "github.com/engity-com/bifroest/pkg/session" - "sync/atomic" - "time" ) type houseKeeper struct { @@ -71,15 +74,15 @@ func (this *houseKeeper) checkedRun(ctx context.Context) (nextRunIn time.Duratio } }() - //defer func() { - // if v := recover(); v != nil { - // if err, ok := v.(error); ok { - // rErr = err - // } else { - // rErr = fmt.Errorf("panic while housekeeping occured: %v", v) - // } - // } - //}() + defer func() { + if v := recover(); v != nil { + if err, ok := v.(error); ok { + rErr = err + } else { + rErr = fmt.Errorf("panic while housekeeping occurred: %v", v) + } + } + }() l.Debug("housekeeping run started") diff --git a/pkg/service/service-authorization.go b/pkg/service/service-authorization.go index 8269305..4887c3c 100644 --- a/pkg/service/service-authorization.go +++ b/pkg/service/service-authorization.go @@ -1,11 +1,12 @@ package service import ( + "github.com/gliderlabs/ssh" + gssh "golang.org/x/crypto/ssh" + "github.com/engity-com/bifroest/pkg/authorization" "github.com/engity-com/bifroest/pkg/errors" "github.com/engity-com/bifroest/pkg/session" - "github.com/gliderlabs/ssh" - gssh "golang.org/x/crypto/ssh" ) func (this *service) handlePublicKey(ctx ssh.Context, key ssh.PublicKey) bool { @@ -148,3 +149,30 @@ func (this *service) resolveAuthorizationAndSession(sshSess ssh.Session) (author } return auth, sess, oldState, nil } + +func (this *service) onPtyRequest(ctx ssh.Context, pty ssh.Pty) bool { + auth, ok := ctx.Value(authorizationCtxKey).(authorization.Authorization) + if !ok { + return false + } + + logger := this.logger(ctx) + + ok, err := this.environments.DoesSupportPty(&environmentRequest{ + this, + &remote{ctx}, + auth, + }, pty) + if this.isRelevantError(err) { + logger.WithError(err).Warn("cannot evaluate if PTY is allowed or not for request") + return false + } + + if !ok { + logger.Debug("PTY was requested but is forbidden") + return false + } + + logger.Debug("PTY was requested and was permitted") + return true +} diff --git a/pkg/service/service-direct-tcp-ip.go b/pkg/service/service-direct-tcp-ip.go index 19ad7f2..187f463 100644 --- a/pkg/service/service-direct-tcp-ip.go +++ b/pkg/service/service-direct-tcp-ip.go @@ -1,14 +1,18 @@ package service import ( - "github.com/engity-com/bifroest/pkg/common" - "github.com/engity-com/bifroest/pkg/environment" - "github.com/gliderlabs/ssh" - gssh "golang.org/x/crypto/ssh" "io" "sync" "sync/atomic" + "syscall" "time" + + "github.com/gliderlabs/ssh" + gssh "golang.org/x/crypto/ssh" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/environment" + "github.com/engity-com/bifroest/pkg/errors" ) type localForwardChannelData struct { @@ -96,7 +100,7 @@ func (this *service) handleNewDirectTcpIp(_ *ssh.Server, _ *gssh.ServerConn, new if direction != "" { ld = ld.With("direction", direction) } - ld.WithError(rErr).Error("cannot successful handle port forwarding request; cancelling...") + ld.WithError(rErr).Error("cannot successful handle port forwarding request; canceling...") } else { ld.Info("port forwarding finished") } @@ -145,3 +149,21 @@ func (this *service) onReversePortForwardingRequested(_ ssh.Context, _ string, _ // TODO! Maybe more checks here in the future? return true } + +func (this *service) isAcceptableNewConnectionError(err error) bool { + if err == nil { + return false + } + + var sce syscall.Errno + if errors.As(err, &sce) { + switch sce { + case syscall.ECONNREFUSED, syscall.ETIMEDOUT, syscall.EHOSTDOWN, syscall.ENETUNREACH: + return true + default: + return false + } + } + + return false +} diff --git a/pkg/service/service-messages.go b/pkg/service/service-messages.go index 713593e..27e0e20 100644 --- a/pkg/service/service-messages.go +++ b/pkg/service/service-messages.go @@ -1,11 +1,13 @@ package service import ( + "io" + + "github.com/gliderlabs/ssh" + "github.com/engity-com/bifroest/pkg/authorization" "github.com/engity-com/bifroest/pkg/errors" "github.com/engity-com/bifroest/pkg/session" - "github.com/gliderlabs/ssh" - "io" ) func (this *service) handleBanner(ctx ssh.Context) string { diff --git a/pkg/service/service-session.go b/pkg/service/service-session.go index 0de5ef2..5b9566f 100644 --- a/pkg/service/service-session.go +++ b/pkg/service/service-session.go @@ -2,12 +2,14 @@ package service import ( "context" + "io" + + "github.com/gliderlabs/ssh" + gssh "golang.org/x/crypto/ssh" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/environment" "github.com/engity-com/bifroest/pkg/errors" - "github.com/gliderlabs/ssh" - gssh "golang.org/x/crypto/ssh" - "io" ) func (this *service) handleNewSshSession(srv *ssh.Server, conn *gssh.ServerConn, newChan gssh.NewChannel, ctx ssh.Context) { diff --git a/pkg/service/service.go b/pkg/service/service.go index c3e7f29..618c06b 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -3,8 +3,17 @@ package service import ( "context" "fmt" + "io" + "net" + "sync" + "sync/atomic" + "syscall" + log "github.com/echocat/slf4g" "github.com/echocat/slf4g/fields" + "github.com/gliderlabs/ssh" + gssh "golang.org/x/crypto/ssh" + "github.com/engity-com/bifroest/pkg/authorization" "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/configuration" @@ -13,13 +22,6 @@ import ( "github.com/engity-com/bifroest/pkg/errors" bnet "github.com/engity-com/bifroest/pkg/net" "github.com/engity-com/bifroest/pkg/session" - "github.com/gliderlabs/ssh" - gssh "golang.org/x/crypto/ssh" - "io" - "net" - "sync" - "sync/atomic" - "syscall" ) var ( @@ -31,6 +33,7 @@ var ( type Service struct { Configuration configuration.Configuration + Version common.Version Logger log.Logger } @@ -89,6 +92,8 @@ func (this *Service) Run(ctx context.Context) (rErr error) { lns[i].ln = ln } + this.logger().WithAll(common.VersionToMap(this.Version)).Info("started") + done := make(chan error, len(lns)) var wg sync.WaitGroup for _, ln := range lns { @@ -149,7 +154,7 @@ func (this *Service) prepare() (svc *service, err error) { ctx := context.Background() svc = &service{Service: this} - if svc.sessions, err = session.NewRepositoryFacade(ctx, &this.Configuration.Session); err != nil { + if svc.sessions, err = session.NewFacadeRepository(ctx, &this.Configuration.Session); err != nil { return fail(err) } if svc.authorizer, err = authorization.NewAuthorizerFacade(ctx, &this.Configuration.Flows); err != nil { @@ -178,6 +183,7 @@ func (this *Service) prepareServer(_ context.Context, svc *service) (err error) svc.server.ServerConfigCallback = svc.createNewServerConfig svc.server.ConnCallback = svc.onNewConnConnection svc.server.Handler = svc.handleSshShellSession + svc.server.PtyCallback = svc.onPtyRequest svc.server.ReversePortForwardingCallback = svc.onReversePortForwardingRequested svc.server.PublicKeyHandler = svc.handlePublicKey svc.server.PasswordHandler = svc.handlePassword @@ -266,7 +272,7 @@ func (this *service) isRelevantError(err error) bool { func (this *service) createNewServerConfig(ssh.Context) *gssh.ServerConfig { return &gssh.ServerConfig{ - ServerVersion: "SSH-2.0-Engity-Bifroest_0.1.0", + ServerVersion: "SSH-2.0-Engity-Bifroest_" + this.Version.Version(), MaxAuthTries: int(this.Configuration.Ssh.MaxAuthTries), } } diff --git a/pkg/service/service_linux.go b/pkg/service/service_linux.go deleted file mode 100644 index 0269b5a..0000000 --- a/pkg/service/service_linux.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build linux - -package service - -import ( - "github.com/engity-com/bifroest/pkg/errors" - "syscall" -) - -func (this *service) isAcceptableNewConnectionError(err error) bool { - if err == nil { - return false - } - - var sce syscall.Errno - if errors.As(err, &sce) { - switch sce { - case syscall.ECONNREFUSED, syscall.ETIMEDOUT, syscall.EHOSTDOWN, syscall.ENETUNREACH: - return true - } - } - - return false -} diff --git a/pkg/session/common.go b/pkg/session/common.go index eddb883..a6efacb 100644 --- a/pkg/session/common.go +++ b/pkg/session/common.go @@ -2,8 +2,9 @@ package session import ( "bytes" - "github.com/engity-com/bifroest/pkg/errors" "io" + + "github.com/engity-com/bifroest/pkg/errors" ) func isReaderEqualToBytes(left io.Reader, right []byte) (bool, error) { diff --git a/pkg/session/common_test.go b/pkg/session/common_test.go index a8076cb..b38ae4d 100644 --- a/pkg/session/common_test.go +++ b/pkg/session/common_test.go @@ -3,10 +3,11 @@ package session import ( "bytes" "fmt" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "math/rand" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_isReaderEqualToBytes(t *testing.T) { diff --git a/pkg/session/connection-interceptor.go b/pkg/session/connection-interceptor.go index 2983b52..93959df 100644 --- a/pkg/session/connection-interceptor.go +++ b/pkg/session/connection-interceptor.go @@ -1,11 +1,12 @@ package session import ( - log "github.com/echocat/slf4g" - "github.com/gliderlabs/ssh" "io" "net" "time" + + log "github.com/echocat/slf4g" + "github.com/gliderlabs/ssh" ) type ConnectionInterceptor interface { diff --git a/pkg/session/facade-repository.go b/pkg/session/facade-repository.go new file mode 100644 index 0000000..46e3470 --- /dev/null +++ b/pkg/session/facade-repository.go @@ -0,0 +1,58 @@ +package session + +import ( + "context" + "fmt" + "reflect" + + "github.com/engity-com/bifroest/pkg/configuration" + "github.com/engity-com/bifroest/pkg/errors" +) + +func NewFacadeRepository(ctx context.Context, conf *configuration.Session) (*FacadeRepository, error) { + if conf == nil { + panic("nil configuration") + } + instance, err := newRepositoryInstance(ctx, conf) + if err != nil { + return nil, err + } + return &FacadeRepository{instance}, nil +} + +type FacadeRepository struct { + CloseableRepository +} + +func newRepositoryInstance(ctx context.Context, conf *configuration.Session) (CloseableRepository, error) { + fail := func(err error) (CloseableRepository, error) { + return nil, fmt.Errorf("cannot initizalize session repository: %w", err) + } + + if conf.V == nil { + return fail(errors.Config.Newf("no session configured")) + } + + factory, ok := configurationTypeToRepositoryFactory[reflect.TypeOf(conf.V)] + if !ok { + return fail(errors.Config.Newf("cannot handle session type %v", reflect.TypeOf(conf.V))) + } + m := reflect.ValueOf(factory) + rets := m.Call([]reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(conf.V)}) + if err, ok := rets[1].Interface().(error); ok && err != nil { + return fail(err) + } + return rets[0].Interface().(CloseableRepository), nil +} + +var ( + configurationTypeToRepositoryFactory = make(map[reflect.Type]any) +) + +type RepositoryFactory[C any, R CloseableRepository] func(ctx context.Context, conf C) (R, error) + +func RegisterRepository[C any, R CloseableRepository](factory RepositoryFactory[C, R]) RepositoryFactory[C, R] { + ct := reflect.TypeFor[C]() + configurationTypeToRepositoryFactory[ct] = factory + return factory +} diff --git a/pkg/session/fs-connection-interceptor.go b/pkg/session/fs-connection-interceptor.go index e9ef7da..f6c21fb 100644 --- a/pkg/session/fs-connection-interceptor.go +++ b/pkg/session/fs-connection-interceptor.go @@ -3,13 +3,15 @@ package session import ( "context" "fmt" - log "github.com/echocat/slf4g" - "github.com/engity-com/bifroest/pkg/configuration" - "github.com/gliderlabs/ssh" - "github.com/google/uuid" "net" "sync/atomic" "time" + + log "github.com/echocat/slf4g" + "github.com/gliderlabs/ssh" + "github.com/google/uuid" + + "github.com/engity-com/bifroest/pkg/configuration" ) func (this *fs) ConnectionInterceptor(context.Context) (ConnectionInterceptor, error) { diff --git a/pkg/session/fs-created.go b/pkg/session/fs-created.go index 022b98a..bade094 100644 --- a/pkg/session/fs-created.go +++ b/pkg/session/fs-created.go @@ -2,10 +2,11 @@ package session import ( "fmt" - "github.com/engity-com/bifroest/pkg/common" - "github.com/engity-com/bifroest/pkg/net" "strings" "time" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/net" ) type fsCreated struct { diff --git a/pkg/session/fs-info.go b/pkg/session/fs-info.go index 314f351..aeababa 100644 --- a/pkg/session/fs-info.go +++ b/pkg/session/fs-info.go @@ -4,12 +4,14 @@ import ( "context" "encoding/json" "fmt" + "os" + "time" + + "github.com/google/uuid" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/configuration" "github.com/engity-com/bifroest/pkg/net" - "github.com/google/uuid" - "os" - "time" ) type fsInfo struct { diff --git a/pkg/session/fs-lastaccessed.go b/pkg/session/fs-lastaccessed.go index 9bd9853..17bbdc3 100644 --- a/pkg/session/fs-lastaccessed.go +++ b/pkg/session/fs-lastaccessed.go @@ -4,11 +4,12 @@ import ( "context" "encoding/json" "fmt" - "github.com/engity-com/bifroest/pkg/common" - "github.com/engity-com/bifroest/pkg/net" "os" "strings" "time" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/net" ) type fsLastAccessed struct { diff --git a/pkg/session/fs-repository.go b/pkg/session/fs-repository.go index 5f3fbb0..5a110a5 100644 --- a/pkg/session/fs-repository.go +++ b/pkg/session/fs-repository.go @@ -8,26 +8,32 @@ import ( "encoding/base64" "encoding/json" "fmt" - log "github.com/echocat/slf4g" - "github.com/engity-com/bifroest/pkg/common" - "github.com/engity-com/bifroest/pkg/configuration" - "github.com/engity-com/bifroest/pkg/errors" - "github.com/engity-com/bifroest/pkg/sys" - "github.com/google/uuid" - "github.com/mr-tron/base58" - "golang.org/x/crypto/ssh" "os" "path/filepath" "strings" "sync" "time" + + log "github.com/echocat/slf4g" + "github.com/google/uuid" + "github.com/mr-tron/base58" + "golang.org/x/crypto/ssh" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/configuration" + "github.com/engity-com/bifroest/pkg/errors" + "github.com/engity-com/bifroest/pkg/sys" ) const ( maxFsRepositoryPublicKeyLineSize = 6 * 1024 ) -func NewFsRepository(conf *configuration.SessionFs) (*FsRepository, error) { +var ( + _ = RegisterRepository(NewFsRepository) +) + +func NewFsRepository(_ context.Context, conf *configuration.SessionFs) (*FsRepository, error) { result := FsRepository{ conf: conf, } @@ -428,7 +434,6 @@ func (this *FsRepository) doFindAutoCleanIfAllowed(ctx context.Context, flow con Warn(successMessage) } } - return } func (this *FsRepository) doAutoCleanUnexpectedFilesIfAllowed(_ context.Context, sess *fs, opts *FindOpts) { @@ -517,7 +522,6 @@ func (this *FsRepository) doFindAutoCleanFlowContentIfAllowed(_ context.Context, Warn(successMessage) } } - return } func (this *FsRepository) doFindAutoCleanRootContentIfAllowed(_ context.Context, fn string, opts *FindOpts, successMessage string, cause error) { @@ -537,7 +541,6 @@ func (this *FsRepository) doFindAutoCleanRootContentIfAllowed(_ context.Context, Warn(successMessage) } } - return } func (this *FsRepository) Delete(ctx context.Context, s Session) error { @@ -680,7 +683,7 @@ func (this *FsRepository) findPublicKeyIn(ctx context.Context, flow configuratio line := scanner.Bytes() lineN++ if len(line) == 0 { - //Skip empty lines... + // Skip empty lines... continue } keyBytes, err := base64.StdEncoding.DecodeString(string(line)) diff --git a/pkg/session/fs.go b/pkg/session/fs.go index 9bc2949..c949e03 100644 --- a/pkg/session/fs.go +++ b/pkg/session/fs.go @@ -3,14 +3,16 @@ package session import ( "context" "fmt" - "github.com/engity-com/bifroest/pkg/common" - "github.com/engity-com/bifroest/pkg/configuration" - "github.com/engity-com/bifroest/pkg/sys" - "github.com/google/uuid" - "golang.org/x/crypto/ssh" "io" "os" "time" + + "github.com/google/uuid" + "golang.org/x/crypto/ssh" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/configuration" + "github.com/engity-com/bifroest/pkg/sys" ) const ( diff --git a/pkg/session/info.go b/pkg/session/info.go index 662970c..ed3658a 100644 --- a/pkg/session/info.go +++ b/pkg/session/info.go @@ -2,10 +2,12 @@ package session import ( "context" + "time" + + "github.com/google/uuid" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/configuration" - "github.com/google/uuid" - "time" ) type Info interface { diff --git a/pkg/session/predicates.go b/pkg/session/predicates.go index 7edb857..321c738 100644 --- a/pkg/session/predicates.go +++ b/pkg/session/predicates.go @@ -2,8 +2,9 @@ package session import ( "context" - "github.com/engity-com/bifroest/pkg/configuration" "time" + + "github.com/engity-com/bifroest/pkg/configuration" ) type Predicate func(context.Context, Session) (bool, error) diff --git a/pkg/session/repository-facade.go b/pkg/session/repository-facade.go deleted file mode 100644 index e5e1150..0000000 --- a/pkg/session/repository-facade.go +++ /dev/null @@ -1,41 +0,0 @@ -package session - -import ( - "context" - "fmt" - "github.com/engity-com/bifroest/pkg/configuration" - "reflect" -) - -func NewRepositoryFacade(ctx context.Context, conf *configuration.Session) (*RepositoryFacade, error) { - if conf == nil { - panic("nil configuration") - } - instance, err := newRepositoryInstance(ctx, conf) - if err != nil { - return nil, err - } - return &RepositoryFacade{instance}, nil -} - -type RepositoryFacade struct { - CloseableRepository -} - -func newRepositoryInstance(_ context.Context, conf *configuration.Session) (r CloseableRepository, err error) { - fail := func(err error) (CloseableRepository, error) { - return nil, fmt.Errorf("cannot initizalize session repository: %w", err) - } - - switch sessConv := conf.V.(type) { - case *configuration.SessionFs: - r, err = NewFsRepository(sessConv) - default: - return fail(fmt.Errorf("cannot handle session type %v", reflect.TypeOf(conf.V))) - } - - if err != nil { - return fail(fmt.Errorf("cannot initizalize session repository: %w", err)) - } - return r, nil -} diff --git a/pkg/session/repository.go b/pkg/session/repository.go index c5f4289..f7ebcf0 100644 --- a/pkg/session/repository.go +++ b/pkg/session/repository.go @@ -3,12 +3,14 @@ package session import ( "context" "errors" + "io" + log "github.com/echocat/slf4g" - "github.com/engity-com/bifroest/pkg/common" - "github.com/engity-com/bifroest/pkg/configuration" "github.com/google/uuid" "golang.org/x/crypto/ssh" - "io" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/configuration" ) var ( diff --git a/pkg/session/session.go b/pkg/session/session.go index 37f5dd2..ca989d9 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -2,11 +2,13 @@ package session import ( "context" + + "github.com/google/uuid" + "golang.org/x/crypto/ssh" + "github.com/engity-com/bifroest/pkg/common" "github.com/engity-com/bifroest/pkg/configuration" "github.com/engity-com/bifroest/pkg/errors" - "github.com/google/uuid" - "golang.org/x/crypto/ssh" ) var ( diff --git a/pkg/sftp/server.go b/pkg/sftp/server.go index 11ce9c5..65a59d5 100644 --- a/pkg/sftp/server.go +++ b/pkg/sftp/server.go @@ -3,12 +3,14 @@ package sftp import ( "errors" "fmt" + "io" + "os" + log "github.com/echocat/slf4g" "github.com/echocat/slf4g/level" - "github.com/engity-com/bifroest/pkg/common" "github.com/pkg/sftp" - "io" - "os" + + "github.com/engity-com/bifroest/pkg/common" ) type Server struct { @@ -42,13 +44,6 @@ func (this *Server) debugLogWriter() io.Writer { } } -func (this *Server) logger() log.Logger { - if v := this.Logger; v != nil { - return v - } - return log.GetRootLogger() -} - func (this *Server) workingDir() string { if v := this.WorkingDir; v != "" { return v diff --git a/pkg/sys/signal_linux.go b/pkg/sys/signal.go similarity index 50% rename from pkg/sys/signal_linux.go rename to pkg/sys/signal.go index 66f1a2a..f1217ab 100644 --- a/pkg/sys/signal_linux.go +++ b/pkg/sys/signal.go @@ -1,5 +1,3 @@ -//go:build linux - package sys import ( @@ -16,44 +14,6 @@ var ( type Signal uint8 -const ( - SIGABRT = Signal(syscall.SIGABRT) - SIGALRM = Signal(syscall.SIGALRM) - SIGBUS = Signal(syscall.SIGBUS) - SIGCHLD = Signal(syscall.SIGCHLD) - SIGCLD = Signal(syscall.SIGCLD) - SIGCONT = Signal(syscall.SIGCONT) - SIGFPE = Signal(syscall.SIGFPE) - SIGHUP = Signal(syscall.SIGHUP) - SIGILL = Signal(syscall.SIGILL) - SIGINT = Signal(syscall.SIGINT) - SIGIO = Signal(syscall.SIGIO) - SIGIOT = Signal(syscall.SIGIOT) - SIGKILL = Signal(syscall.SIGKILL) - SIGPIPE = Signal(syscall.SIGPIPE) - SIGPOLL = Signal(syscall.SIGPOLL) - SIGPROF = Signal(syscall.SIGPROF) - SIGPWR = Signal(syscall.SIGPWR) - SIGQUIT = Signal(syscall.SIGQUIT) - SIGSEGV = Signal(syscall.SIGSEGV) - SIGSTKFLT = Signal(syscall.SIGSTKFLT) - SIGSTOP = Signal(syscall.SIGSTOP) - SIGSYS = Signal(syscall.SIGSYS) - SIGTERM = Signal(syscall.SIGTERM) - SIGTRAP = Signal(syscall.SIGTRAP) - SIGTSTP = Signal(syscall.SIGTSTP) - SIGTTIN = Signal(syscall.SIGTTIN) - SIGTTOU = Signal(syscall.SIGTTOU) - SIGUNUSED = Signal(syscall.SIGUNUSED) - SIGURG = Signal(syscall.SIGURG) - SIGUSR1 = Signal(syscall.SIGUSR1) - SIGUSR2 = Signal(syscall.SIGUSR2) - SIGVTALRM = Signal(syscall.SIGVTALRM) - SIGWINCH = Signal(syscall.SIGWINCH) - SIGXCPU = Signal(syscall.SIGXCPU) - SIGXFSZ = Signal(syscall.SIGXFSZ) -) - func (this Signal) String() string { if this == 0 { return "" @@ -147,44 +107,6 @@ const ( ) var ( - strToSignal = map[string]Signal{ - "ABRT": SIGABRT, - "ALRM": SIGALRM, - "BUS": SIGBUS, - "CHLD": SIGCHLD, - "CLD": SIGCLD, - "CONT": SIGCONT, - "FPE": SIGFPE, - "HUP": SIGHUP, - "ILL": SIGILL, - "INT": SIGINT, - "IO": SIGIO, - "IOT": SIGIOT, - "KILL": SIGKILL, - "PIPE": SIGPIPE, - "POLL": SIGPOLL, - "PROF": SIGPROF, - "PWR": SIGPWR, - "QUIT": SIGQUIT, - "SEGV": SIGSEGV, - "STKFLT": SIGSTKFLT, - "STOP": SIGSTOP, - "SYS": SIGSYS, - "TERM": SIGTERM, - "TRAP": SIGTRAP, - "TSTP": SIGTSTP, - "TTIN": SIGTTIN, - "TTOU": SIGTTOU, - "UNUSED": SIGUNUSED, - "URG": SIGURG, - "USR1": SIGUSR1, - "USR2": SIGUSR2, - "VTALRM": SIGVTALRM, - "WINCH": SIGWINCH, - "XCPU": SIGXCPU, - "XFSZ": SIGXFSZ, - } - signalToStr = func(in map[string]Signal) map[Signal]string { result := make(map[Signal]string, len(in)) for str, sig := range in { diff --git a/pkg/sys/signal_unix.go b/pkg/sys/signal_unix.go new file mode 100644 index 0000000..8b48de7 --- /dev/null +++ b/pkg/sys/signal_unix.go @@ -0,0 +1,85 @@ +//go:build unix + +package sys + +import ( + "syscall" +) + +const ( + SIGABRT = Signal(syscall.SIGABRT) + SIGALRM = Signal(syscall.SIGALRM) + SIGBUS = Signal(syscall.SIGBUS) + SIGCHLD = Signal(syscall.SIGCHLD) + SIGCLD = Signal(syscall.SIGCLD) + SIGCONT = Signal(syscall.SIGCONT) + SIGFPE = Signal(syscall.SIGFPE) + SIGHUP = Signal(syscall.SIGHUP) + SIGILL = Signal(syscall.SIGILL) + SIGINT = Signal(syscall.SIGINT) + SIGIO = Signal(syscall.SIGIO) + SIGIOT = Signal(syscall.SIGIOT) + SIGKILL = Signal(syscall.SIGKILL) + SIGPIPE = Signal(syscall.SIGPIPE) + SIGPOLL = Signal(syscall.SIGPOLL) + SIGPROF = Signal(syscall.SIGPROF) + SIGPWR = Signal(syscall.SIGPWR) + SIGQUIT = Signal(syscall.SIGQUIT) + SIGSEGV = Signal(syscall.SIGSEGV) + SIGSTKFLT = Signal(syscall.SIGSTKFLT) + SIGSTOP = Signal(syscall.SIGSTOP) + SIGSYS = Signal(syscall.SIGSYS) + SIGTERM = Signal(syscall.SIGTERM) + SIGTRAP = Signal(syscall.SIGTRAP) + SIGTSTP = Signal(syscall.SIGTSTP) + SIGTTIN = Signal(syscall.SIGTTIN) + SIGTTOU = Signal(syscall.SIGTTOU) + SIGUNUSED = Signal(syscall.SIGUNUSED) + SIGURG = Signal(syscall.SIGURG) + SIGUSR1 = Signal(syscall.SIGUSR1) + SIGUSR2 = Signal(syscall.SIGUSR2) + SIGVTALRM = Signal(syscall.SIGVTALRM) + SIGWINCH = Signal(syscall.SIGWINCH) + SIGXCPU = Signal(syscall.SIGXCPU) + SIGXFSZ = Signal(syscall.SIGXFSZ) +) + +var ( + strToSignal = map[string]Signal{ + "ABRT": SIGABRT, + "ALRM": SIGALRM, + "BUS": SIGBUS, + "CHLD": SIGCHLD, + "CLD": SIGCLD, + "CONT": SIGCONT, + "FPE": SIGFPE, + "HUP": SIGHUP, + "ILL": SIGILL, + "INT": SIGINT, + "IO": SIGIO, + "IOT": SIGIOT, + "KILL": SIGKILL, + "PIPE": SIGPIPE, + "POLL": SIGPOLL, + "PROF": SIGPROF, + "PWR": SIGPWR, + "QUIT": SIGQUIT, + "SEGV": SIGSEGV, + "STKFLT": SIGSTKFLT, + "STOP": SIGSTOP, + "SYS": SIGSYS, + "TERM": SIGTERM, + "TRAP": SIGTRAP, + "TSTP": SIGTSTP, + "TTIN": SIGTTIN, + "TTOU": SIGTTOU, + "UNUSED": SIGUNUSED, + "URG": SIGURG, + "USR1": SIGUSR1, + "USR2": SIGUSR2, + "VTALRM": SIGVTALRM, + "WINCH": SIGWINCH, + "XCPU": SIGXCPU, + "XFSZ": SIGXFSZ, + } +) diff --git a/pkg/sys/signal_windows.go b/pkg/sys/signal_windows.go new file mode 100644 index 0000000..084b452 --- /dev/null +++ b/pkg/sys/signal_windows.go @@ -0,0 +1,41 @@ +//go:build windows + +package sys + +import ( + "syscall" +) + +const ( + SIGABRT = Signal(syscall.SIGABRT) + SIGALRM = Signal(syscall.SIGALRM) + SIGBUS = Signal(syscall.SIGBUS) + SIGFPE = Signal(syscall.SIGFPE) + SIGHUP = Signal(syscall.SIGHUP) + SIGILL = Signal(syscall.SIGILL) + SIGINT = Signal(syscall.SIGINT) + SIGKILL = Signal(syscall.SIGKILL) + SIGPIPE = Signal(syscall.SIGPIPE) + SIGQUIT = Signal(syscall.SIGQUIT) + SIGSEGV = Signal(syscall.SIGSEGV) + SIGTERM = Signal(syscall.SIGTERM) + SIGTRAP = Signal(syscall.SIGTRAP) +) + +var ( + strToSignal = map[string]Signal{ + "ABRT": SIGABRT, + "ALRM": SIGALRM, + "BUS": SIGBUS, + "FPE": SIGFPE, + "HUP": SIGHUP, + "ILL": SIGILL, + "INT": SIGINT, + "KILL": SIGKILL, + "PIPE": SIGPIPE, + "QUIT": SIGQUIT, + "SEGV": SIGSEGV, + "TERM": SIGTERM, + "TRAP": SIGTRAP, + } +) diff --git a/pkg/template/bool.go b/pkg/template/bool.go index cbe670c..c10558d 100644 --- a/pkg/template/bool.go +++ b/pkg/template/bool.go @@ -2,8 +2,9 @@ package template import ( "fmt" - "github.com/engity-com/bifroest/internal/text/template" "strings" + + "github.com/engity-com/bifroest/internal/text/template" ) func NewBool(plain string) (Bool, error) { diff --git a/pkg/template/bool_test.go b/pkg/template/bool_test.go index 6b12658..65e163f 100644 --- a/pkg/template/bool_test.go +++ b/pkg/template/bool_test.go @@ -3,8 +3,9 @@ package template import ( "errors" "fmt" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestBool(t *testing.T) { diff --git a/pkg/template/funcs.go b/pkg/template/funcs.go index 42a8e63..22af3cf 100644 --- a/pkg/template/funcs.go +++ b/pkg/template/funcs.go @@ -5,11 +5,6 @@ import ( "crypto/sha256" "encoding/base64" "fmt" - "github.com/Masterminds/sprig/v3" - "github.com/engity-com/bifroest/internal/text/template" - "github.com/engity-com/bifroest/pkg/common" - "github.com/engity-com/bifroest/pkg/sys" - "golang.org/x/crypto/ssh" "io" "io/fs" "os" @@ -18,6 +13,13 @@ import ( "reflect" "strings" "time" + + "github.com/Masterminds/sprig/v3" + "golang.org/x/crypto/ssh" + + "github.com/engity-com/bifroest/internal/text/template" + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/sys" ) const ( diff --git a/pkg/template/string.go b/pkg/template/string.go index abbd1b0..d34f31b 100644 --- a/pkg/template/string.go +++ b/pkg/template/string.go @@ -2,9 +2,10 @@ package template import ( "fmt" - "github.com/engity-com/bifroest/internal/text/template" "strings" "text/template/parse" + + "github.com/engity-com/bifroest/internal/text/template" ) func NewString(plain string) (String, error) { diff --git a/pkg/template/string_test.go b/pkg/template/string_test.go index b2ebcfb..a0016aa 100644 --- a/pkg/template/string_test.go +++ b/pkg/template/string_test.go @@ -2,8 +2,9 @@ package template import ( "fmt" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestString(t *testing.T) { diff --git a/pkg/template/strings.go b/pkg/template/strings.go new file mode 100644 index 0000000..5721925 --- /dev/null +++ b/pkg/template/strings.go @@ -0,0 +1,68 @@ +package template + +import ( + "fmt" +) + +func NewStrings(plains ...string) (Strings, error) { + buf := make(Strings, len(plains)) + var err error + for i, plain := range plains { + if err = buf[i].Set(plain); err != nil { + return nil, fmt.Errorf("[%d] %w", i, err) + } + } + return buf, nil +} + +func MustNewStrings(plains ...string) Strings { + buf, err := NewStrings(plains...) + if err != nil { + panic(err) + } + return buf +} + +type Strings []String + +func (this Strings) Render(data any) ([]string, error) { + result := make([]string, len(this)) + for i, v := range this { + rv, err := v.Render(data) + if err != nil { + return nil, fmt.Errorf("[%d] %w", i, err) + } + result[i] = rv + } + return result, nil +} + +func (this Strings) IsZero() bool { + return len(this) == 0 +} + +func (this Strings) IsEqualTo(other any) bool { + if other == nil { + return false + } + switch v := other.(type) { + case Strings: + return this.isEqualTo(&v) + case *Strings: + return this.isEqualTo(v) + default: + return false + } +} + +func (this Strings) isEqualTo(other *Strings) bool { + if len(this) != len(*other) { + return false + } + for i, tv := range this { + if !tv.isEqualTo(&(*other)[i]) { + return false + } + } + return true +} diff --git a/pkg/template/text-marshaller.go b/pkg/template/text-marshaller.go index bdcda3d..f2bf60f 100644 --- a/pkg/template/text-marshaller.go +++ b/pkg/template/text-marshaller.go @@ -3,8 +3,9 @@ package template import ( "encoding" "fmt" - "github.com/engity-com/bifroest/internal/text/template" "strings" + + "github.com/engity-com/bifroest/internal/text/template" ) func NewTextMarshaller[T TextMarshallerArgument, PT TextMarshallerArgumentP[T]](plain string) (TextMarshaller[T, PT], error) { diff --git a/pkg/template/uint64.go b/pkg/template/uint64.go index 8178dfb..fb37f40 100644 --- a/pkg/template/uint64.go +++ b/pkg/template/uint64.go @@ -2,9 +2,10 @@ package template import ( "fmt" - "github.com/engity-com/bifroest/internal/text/template" "strconv" "strings" + + "github.com/engity-com/bifroest/internal/text/template" ) func NewUint64(plain string) (Uint64, error) { diff --git a/pkg/template/uint64_test.go b/pkg/template/uint64_test.go index 7824b06..95fa64e 100644 --- a/pkg/template/uint64_test.go +++ b/pkg/template/uint64_test.go @@ -3,8 +3,9 @@ package template import ( "errors" "fmt" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestUint64(t *testing.T) { diff --git a/pkg/user/common_test.go b/pkg/user/common_test.go index e36316c..b2329af 100644 --- a/pkg/user/common_test.go +++ b/pkg/user/common_test.go @@ -1,20 +1,23 @@ +//nolint:golint,unused package user import ( "bytes" "fmt" - "github.com/engity-com/bifroest/pkg/sys" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "io" "os" "path/filepath" "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/engity-com/bifroest/pkg/sys" ) var ( - keepPkgUserFiles = os.Getenv("KEEP_PKG_USER_TEST_FILES") == "yes" + keepPkgUserFiles = os.Getenv("KEEP_PKG_USER_TEST_FILES") == "yes" //nolint:golint,unused ) func b(in string) []byte { @@ -68,7 +71,7 @@ func (this *testFile) setContent(with string) *testFile { require.NoError(this.t, f.Close()) }() - _, err = io.Copy(f, strings.NewReader(with)) + _, err = io.Copy(f, strings.NewReader(strings.ReplaceAll(with, "$space$", " "))) require.NoError(this.t, err) return this } diff --git a/pkg/user/ensure.go b/pkg/user/ensure.go index 9a2be5f..d122cbc 100644 --- a/pkg/user/ensure.go +++ b/pkg/user/ensure.go @@ -1,3 +1,5 @@ +//go:build unix + package user import ( @@ -10,11 +12,11 @@ import ( var ( // ErrUserDoesNotFulfilRequirement indicates that a User does not // meet the provided Requirement. - ErrUserDoesNotFulfilRequirement = errors.New("user does not fulfil requirement") + ErrUserDoesNotFulfilRequirement = errors.New("user does not fulfill requirement") // ErrGroupDoesNotFulfilRequirement indicates that a Group does not // meet the provided GroupRequirement. - ErrGroupDoesNotFulfilRequirement = errors.New("group does not fulfil requirement") + ErrGroupDoesNotFulfilRequirement = errors.New("group does not fulfill requirement") ) // Ensurer ensures that a User or Group meets the provided requirements. diff --git a/pkg/user/etc-colon-entry_test.go b/pkg/user/etc-colon-entry_test.go index c9ce43f..653f314 100644 --- a/pkg/user/etc-colon-entry_test.go +++ b/pkg/user/etc-colon-entry_test.go @@ -1,11 +1,14 @@ +//go:build unix + package user import ( "bytes" - "github.com/echocat/slf4g/sdk/testlog" - "github.com/stretchr/testify/require" "strings" "testing" + + "github.com/echocat/slf4g/sdk/testlog" + "github.com/stretchr/testify/require" ) func Test_etcColonEntry_decode(t *testing.T) { diff --git a/pkg/user/etc-colon-repository-handle.go b/pkg/user/etc-colon-repository-handle.go index c19f5a7..b96c576 100644 --- a/pkg/user/etc-colon-repository-handle.go +++ b/pkg/user/etc-colon-repository-handle.go @@ -1,12 +1,15 @@ +//go:build unix + package user import ( "fmt" - "github.com/engity-com/bifroest/pkg/common" "io" "os" "path/filepath" "syscall" + + "github.com/engity-com/bifroest/pkg/common" ) type etcColonRepositoryHandle[T any, PT etcColonEntryValue[T]] struct { diff --git a/pkg/user/etc-colon-repository-handles.go b/pkg/user/etc-colon-repository-handles.go index 2056c25..fc1aef7 100644 --- a/pkg/user/etc-colon-repository-handles.go +++ b/pkg/user/etc-colon-repository-handles.go @@ -1,12 +1,15 @@ +//go:build unix + package user import ( "fmt" - "github.com/engity-com/bifroest/pkg/common" - "github.com/engity-com/bifroest/pkg/sys" "os" "path/filepath" "syscall" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/sys" ) type etcColonRepositoryHandles struct { @@ -132,10 +135,8 @@ func (this *etcColonRepositoryHandles) lockFile(which *os.File, how int) error { } return fmt.Errorf("cannot %s %q: %w", op, which.Name(), err) } - select { - case doneErr := <-doneErrChan: - return fail(doneErr) - } + doneErr := <-doneErrChan + return fail(doneErr) } func (this *etcColonRepositoryHandles) close() (rErr error) { diff --git a/pkg/user/etc-colon-repository.go b/pkg/user/etc-colon-repository.go index 536f53f..16aa07b 100644 --- a/pkg/user/etc-colon-repository.go +++ b/pkg/user/etc-colon-repository.go @@ -6,14 +6,6 @@ import ( "bytes" "context" "fmt" - log "github.com/echocat/slf4g" - "github.com/echocat/slf4g/fields" - "github.com/engity-com/bifroest/pkg/common" - "github.com/engity-com/bifroest/pkg/errors" - "github.com/engity-com/bifroest/pkg/sys" - "github.com/fsnotify/fsnotify" - "github.com/otiai10/copy" - "github.com/shirou/gopsutil/process" "io/fs" "os" "path/filepath" @@ -23,6 +15,16 @@ import ( "sync/atomic" "time" "unsafe" + + log "github.com/echocat/slf4g" + "github.com/echocat/slf4g/fields" + "github.com/fsnotify/fsnotify" + "github.com/otiai10/copy" + "github.com/shirou/gopsutil/process" + + "github.com/engity-com/bifroest/pkg/common" + "github.com/engity-com/bifroest/pkg/errors" + "github.com/engity-com/bifroest/pkg/sys" ) var ( @@ -380,7 +382,7 @@ func (this *EtcColonRepository) Ensure(ctx context.Context, req *Requirement, op } tReq := req.OrDefaults() - existing, user, pResult, err := this.ensurePreChecks(ctx, &tReq, opts, this.mutex.RLocker()) + _, user, pResult, err := this.ensurePreChecks(ctx, &tReq, opts, this.mutex.RLocker()) if err != nil || pResult != EnsureResultUnknown { return user, pResult, err } @@ -394,6 +396,7 @@ func (this *EtcColonRepository) Ensure(ctx context.Context, req *Requirement, op } defer common.KeepError(&rErr, f.close) + var existing *etcPasswdRef existing, user, pResult, err = this.ensurePreChecks(ctx, &tReq, opts, nil) if err != nil || pResult != EnsureResultUnknown { return user, pResult, err diff --git a/pkg/user/etc-colon-repository_test.go b/pkg/user/etc-colon-repository_test.go index 6c08861..611893c 100644 --- a/pkg/user/etc-colon-repository_test.go +++ b/pkg/user/etc-colon-repository_test.go @@ -1,21 +1,25 @@ +//go:build unix + package user import ( "context" - errors "errors" - log "github.com/echocat/slf4g" - "github.com/echocat/slf4g/level" - "github.com/echocat/slf4g/sdk/testlog" - "github.com/echocat/slf4g/testing/recording" - "github.com/engity-com/bifroest/pkg/common" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "errors" "io/fs" "maps" "os" "path/filepath" "testing" "time" + + log "github.com/echocat/slf4g" + "github.com/echocat/slf4g/level" + "github.com/echocat/slf4g/sdk/testlog" + "github.com/echocat/slf4g/testing/recording" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/engity-com/bifroest/pkg/common" ) func Test_EtcColonRepository_Init(t *testing.T) { @@ -41,7 +45,7 @@ func Test_EtcColonRepository_Init(t *testing.T) { { name: "all-content", passwd: `root:x:0:0:root:/root:/bin/sh -foo:abc:1:2:Foo Name:/home/foo:/bin/foosh +foo:abc:1:2:Foo Name:/home/foo:/bin/foosh$space$ bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foo:abc:1:foo,bbb @@ -70,7 +74,7 @@ bar:XbarX:20453:10:100:::20818:`, { name: "fail-with-bad-name-in-passwd", passwd: `root:x:0:0:root:/root:/bin/sh -foo@:abc:1:2:Foo Name:/home/foo:/bin/foosh +foo@:abc:1:2:Foo Name:/home/foo:/bin/foosh bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foo:abc:1:aaa,bbb @@ -83,7 +87,7 @@ bar:XbarX:20453:10:100:::20818:`, { name: "fail-with-bad-name-in-group", passwd: `root:x:0:0:root:/root:/bin/sh -foo:abc:1:2:Foo Name:/home/foo:/bin/foosh +foo:abc:1:2:Foo Name:/home/foo:/bin/foosh bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foo@:abc:1:aaa,bbb @@ -96,7 +100,7 @@ bar:XbarX:20453:10:100:::20818:`, { name: "fail-with-bad-name-in-shadow", passwd: `root:x:0:0:root:/root:/bin/sh -foo:abc:1:2:Foo Name:/home/foo:/bin/foosh +foo:abc:1:2:Foo Name:/home/foo:/bin/foosh bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foo:abc:1:aaa,bbb @@ -112,7 +116,7 @@ bar:XbarX:20453:10:100:::20818:`, name: "allow-bad-name-in-passwd", allowBadName: true, passwd: `root:x:0:0:root:/root:/bin/sh -foo@:abc:1:2:Foo Name:/home/foo:/bin/foosh +foo@:abc:1:2:Foo Name:/home/foo:/bin/foosh$space$ bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foo:abc:1:aaa,bbb @@ -140,7 +144,7 @@ bar:XbarX:20453:10:100:::20818:`, name: "allow-bad-name-in-group", allowBadName: true, passwd: `root:x:0:0:root:/root:/bin/sh -foo:abc:1:2:Foo Name:/home/foo:/bin/foosh +foo:abc:1:2:Foo Name:/home/foo:/bin/foosh$space$ bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foo@:abc:1:aaa,bbb @@ -168,7 +172,7 @@ bar:XbarX:20453:10:100:::20818:`, name: "allow-bad-name-in-shadow", allowBadName: true, passwd: `root:x:0:0:root:/root:/bin/sh -foo:abc:1:2:Foo Name:/home/foo:/bin/foosh +foo:abc:1:2:Foo Name:/home/foo:/bin/foosh$space$ bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foo:abc:1:aaa,bbb @@ -197,7 +201,7 @@ bar:XbarX:20453:10:100:::20818:`, { name: "fail-with-line-in-passwd", passwd: `root:x:0:0:root:/root:/bin/sh -foo:abc:1:2:Foo Name:/home/foo:/bin/foosh: +foo:abc:1:2:Foo Name:/home/foo:/bin/foosh: bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foo:abc:1:aaa,bbb @@ -267,7 +271,7 @@ bar:XbarX:20453:10:100:::20818:`, name: "allow-bad-lines-in-group", allowBadLine: true, passwd: `root:x:0:0:root:/root:/bin/sh -foo:abc:1:2:Foo Name:/home/foo:/bin/foosh +foo:abc:1:2:Foo Name:/home/foo:/bin/foosh$space$ bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foo:abc:1:aaa,bbb: @@ -295,7 +299,7 @@ bar:XbarX:20453:10:100:::20818:`, name: "allow-bad-lines-in-shadow", allowBadLine: true, passwd: `root:x:0:0:root:/root:/bin/sh -foo:abc:1:2:Foo Name:/home/foo:/bin/foosh +foo:abc:1:2:Foo Name:/home/foo:/bin/foosh$space$ bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foo:abc:1:aaa,bbb @@ -325,7 +329,7 @@ bar:XbarX:20453:10:100:::20818:`, name: "allow-bad-lines-in-passwd-by-bad-names", allowBadLine: true, passwd: `root:x:0:0:root:/root:/bin/sh -foo@:abc:1:2:Foo Name:/home/foo:/bin/foosh +foo@:abc:1:2:Foo Name:/home/foo:/bin/foosh$space$ bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foo:abc:1:aaa,bbb @@ -353,7 +357,7 @@ bar:XbarX:20453:10:100:::20818:`, name: "allow-bad-lines-in-group-by-bad-names", allowBadLine: true, passwd: `root:x:0:0:root:/root:/bin/sh -foo:abc:1:2:Foo Name:/home/foo:/bin/foosh +foo:abc:1:2:Foo Name:/home/foo:/bin/foosh$space$ bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foo@:abc:1:aaa,bbb @@ -381,7 +385,7 @@ bar:XbarX:20453:10:100:::20818:`, name: "allow-bad-lines-in-shadow-by-bad-names", allowBadLine: true, passwd: `root:x:0:0:root:/root:/bin/sh -foo:abc:1:2:Foo Name:/home/foo:/bin/foosh +foo:abc:1:2:Foo Name:/home/foo:/bin/foosh$space$ bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foo:abc:1:aaa,bbb @@ -477,7 +481,7 @@ func Test_EtcColonRepository_onFsEvents(t *testing.T) { dir := newTestDir(t) passwdFile := dir.file("passwd").setContent(`root:x:0:0:root:/root:/bin/sh -foo:abc:1:2:Foo Name:/home/foo:/bin/foosh +foo:abc:1:2:Foo Name:/home/foo:/bin/foosh bar::11:12::/home/bar:/bin/barsh`) groupFile := dir.file("group").setContent(`root:x:0: foo:abc:1:aaa,bbb @@ -513,7 +517,7 @@ bar:XbarX:20453:10:100:::20818:`) { name: "modify-entry", passwd: `root:x:0:0:root:/root:/bin/sh -foos:abc:1:2:Foo Name:/home/foo:/bin/foosh +foos:abc:1:2:Foo Name:/home/foo:/bin/foosh$space$ bar::11:12::/home/bar:/bin/barsh`, group: `root:x:0: foos:abc:1:aaa,bbb @@ -1010,6 +1014,7 @@ bar:XbarX:20453:10:100:::20818:`) require.NoError(t, actualErr) assert.NoError(t, syncError) + time.Sleep(100 * time.Millisecond) }) } } diff --git a/pkg/user/etc-group_test.go b/pkg/user/etc-group_test.go index a034dae..34355b7 100644 --- a/pkg/user/etc-group_test.go +++ b/pkg/user/etc-group_test.go @@ -3,9 +3,10 @@ package user import ( + "testing" + "github.com/echocat/slf4g/sdk/testlog" "github.com/stretchr/testify/require" - "testing" ) func Test_etcGroupEntry_decode(t *testing.T) { diff --git a/pkg/user/etc-passwd_test.go b/pkg/user/etc-passwd_test.go index e1cdde8..8048742 100644 --- a/pkg/user/etc-passwd_test.go +++ b/pkg/user/etc-passwd_test.go @@ -3,9 +3,10 @@ package user import ( + "testing" + "github.com/echocat/slf4g/sdk/testlog" "github.com/stretchr/testify/require" - "testing" ) func Test_etcPasswdEntry_decode(t *testing.T) { diff --git a/pkg/user/etc-shadow.go b/pkg/user/etc-shadow.go index e48d8a4..85ef52b 100644 --- a/pkg/user/etc-shadow.go +++ b/pkg/user/etc-shadow.go @@ -5,9 +5,10 @@ package user import ( "bytes" "errors" - "github.com/engity-com/bifroest/pkg/crypto/unix/password" "strconv" "time" + + "github.com/engity-com/bifroest/pkg/crypto/unix/password" ) const ( diff --git a/pkg/user/etc-shadow_test.go b/pkg/user/etc-shadow_test.go index afa7e3a..373a514 100644 --- a/pkg/user/etc-shadow_test.go +++ b/pkg/user/etc-shadow_test.go @@ -3,8 +3,9 @@ package user import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func Test_etcShadowEntry_decode(t *testing.T) { diff --git a/pkg/user/etc-unix.go b/pkg/user/etc-unix.go index 22c1c0b..8e65002 100644 --- a/pkg/user/etc-unix.go +++ b/pkg/user/etc-unix.go @@ -146,17 +146,3 @@ func parseUint32Column(line [][]byte, columnIndex int, errEmpty, errIllegal erro } return uint32(v), true, nil } - -func parseUint64Column(line [][]byte, columnIndex int, errEmpty, errIllegal error) (_ uint64, hasValue bool, _ error) { - if len(line[columnIndex]) == 0 { - if errEmpty != nil { - return 0, false, errEmpty - } - return 0, false, nil - } - v, err := strconv.ParseUint(string(line[columnIndex]), 10, 32) - if err != nil { - return 0, false, errIllegal - } - return v, true, nil -} diff --git a/pkg/user/repository.go b/pkg/user/repository.go index ebc2885..f0e1746 100644 --- a/pkg/user/repository.go +++ b/pkg/user/repository.go @@ -1,10 +1,13 @@ +//go:build unix + package user import ( "context" - "github.com/engity-com/bifroest/pkg/errors" "io" "sync" + + "github.com/engity-com/bifroest/pkg/errors" ) var ( @@ -89,7 +92,7 @@ type SharedRepositoryProvider[T interface { Init(context.Context) error }] struct { V T - usages uint16 + usages int16 mutex sync.Mutex } diff --git a/pkg/user/requirement_unix.go b/pkg/user/requirement_unix.go index 3a6b194..77e95b3 100644 --- a/pkg/user/requirement_unix.go +++ b/pkg/user/requirement_unix.go @@ -111,14 +111,3 @@ func (this Requirement) String() string { return "" } } - -func (this Requirement) name() string { - name := strings.Clone(this.Name) - if len(name) > 0 { - return name - } - if uid := this.Uid; uid != nil { - return fmt.Sprintf("u%d", uid) - } - return "" -}