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 ""
-}