diff --git a/apps/job-info-consumer/consumer/go.mod b/apps/job-info-consumer/consumer/go.mod index 0412bfe53d..9ab10658d4 100644 --- a/apps/job-info-consumer/consumer/go.mod +++ b/apps/job-info-consumer/consumer/go.mod @@ -323,7 +323,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.18.0 // indirect golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.19.0 // indirect diff --git a/apps/job-info-consumer/consumer/go.sum b/apps/job-info-consumer/consumer/go.sum index 149f0c38f1..6188c5be1c 100644 --- a/apps/job-info-consumer/consumer/go.sum +++ b/apps/job-info-consumer/consumer/go.sum @@ -1183,7 +1183,7 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/cmd/util/auth/ask.go b/cmd/util/auth/ask.go new file mode 100644 index 0000000000..e1f6aee673 --- /dev/null +++ b/cmd/util/auth/ask.go @@ -0,0 +1,77 @@ +package auth + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "os" + + "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/spf13/cobra" + "golang.org/x/term" +) + +// Returns a responder that responds to authentication requirements of type +// `authn.MethodTypeAsk`. Reads the JSON Schema returned by the `ask` endpoint +// and uses it to ask appropriate questions to the user on their terminal, and +// then returns their response as serialized JSON. +func askResponder(cmd *cobra.Command) responder { + return func(request *json.RawMessage) ([]byte, error) { + compiler := jsonschema.NewCompiler() + compiler.ExtractAnnotations = true + + if err := compiler.AddResource("", bytes.NewReader(*request)); err != nil { + return nil, err + } + + schema, err := compiler.Compile("") + if err != nil { + return nil, err + } + + response := make(map[string]any, len(schema.Properties)) + for _, name := range schema.Required { + subschema := schema.Properties[name] + + if len(subschema.Types) < 1 { + return nil, fmt.Errorf("invalid schema: property %q has no type", name) + } + + typ := subschema.Types[0] + if typ == "object" { + return nil, fmt.Errorf("invalid schema: property %q has non-scalar type", name) + } + + fmt.Fprintf(cmd.ErrOrStderr(), "%s: ", name) + + var input []byte + var err error + + // If the property is marked as write only, assume it is a sensitive + // value and make sure we don't display it in the terminal + if subschema.WriteOnly { + input, err = term.ReadPassword(int(os.Stdin.Fd())) + fmt.Fprintln(cmd.ErrOrStderr()) + } else { + reader := bufio.NewScanner(cmd.InOrStdin()) + if reader.Scan() { + input = reader.Bytes() + } + err = reader.Err() + } + + if err != nil { + return nil, err + } + response[name] = string(input) + } + + respBytes, err := json.Marshal(response) + if err != nil { + return nil, err + } + + return respBytes, schema.Validate(response) + } +} diff --git a/cmd/util/auth/auth.go b/cmd/util/auth/auth.go index 25b1158c6c..74b376752e 100644 --- a/cmd/util/auth/auth.go +++ b/cmd/util/auth/auth.go @@ -19,11 +19,12 @@ import ( type responder = func(request *json.RawMessage) (response []byte, err error) -var supportedMethods map[authn.MethodType]responder = map[authn.MethodType]responder{ - authn.MethodTypeChallenge: challenge.Respond, -} - func RunAuthenticationFlow(cmd *cobra.Command) (string, error) { + supportedMethods := map[authn.MethodType]responder{ + authn.MethodTypeChallenge: challenge.Respond, + authn.MethodTypeAsk: askResponder(cmd), + } + client := util.GetAPIClientV2(cmd.Context()) methods, err := client.Auth().Methods(&apimodels.ListAuthnMethodsRequest{}) if err != nil { diff --git a/docs/docs/dev/auth_flow.md b/docs/docs/dev/auth_flow.md index f20f1434e9..53f0c1cbdd 100644 --- a/docs/docs/dev/auth_flow.md +++ b/docs/docs/dev/auth_flow.md @@ -83,6 +83,25 @@ return to the endpoint. } ``` +### `ask` authentication + +This method requires the user to manually input some information. This method +can be used to implement username and password authentication, shared secret +authentication, and even 2FA or security question auth. + +The required information is represented by a JSON Schema in the object itself. +The implementation should parse the JSON Schema and ask the user questions to +populate an object that is valid by it. + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://bacalhau.org/auth/ask", + "type": "object", + "$ref": "https://json-schema.org/draft/2020-12/schema", +} +``` + ## 2. Run the authn flow and submit the result for an access token The user agent decides which authentication method to use (e.g. by asking the diff --git a/docs/docs/running-node/auth.md b/docs/docs/running-node/auth.md index 4958b5fa9a..10ea719ed5 100644 --- a/docs/docs/running-node/auth.md +++ b/docs/docs/running-node/auth.md @@ -47,3 +47,22 @@ include acceptable client IDs, found by running `bacalhau id`. Once the node is restarted, only keys in the allowed list will be able to access any API. + +## Username and password access + +Users can authenticate using a username and password instead of specifying a +private key for access. Again, this just requires installation of an appropriate +policy. + + curl -sL https://raw.githubusercontent.com/bacalhau-project/bacalhau/main/pkg/authn/ask/ask_ns_password.rego -o ~/.bacalhau/ask_ns_password.rego + bacalhau config set Node.Auth.Methods.Password.Type ask + bacalhau config set Node.Auth.Methods.Password.PolicyPath ~/.bacalhau/ask_ns_password.rego + +Passwords are not stored in plaintext and are salted. The downloaded policy +expects password hashes and salts generated by `scrypt`. To generate a salted +password, the helper script in `pkg/authn/ask/gen_password` can be used: + + cd pkg/authn/ask/gen_password && go run . + +This will ask for a password and generate a salt and hash to authenticate with +it. Add the encoded username, salt and hash into the `ask_ns_password.rego`. diff --git a/go.mod b/go.mod index 891812c0a5..7027a857fb 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285 github.com/rs/zerolog v1.31.0 + github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 @@ -82,7 +83,7 @@ require ( go.uber.org/mock v0.4.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.26.0 - golang.org/x/crypto v0.17.0 + golang.org/x/crypto v0.18.0 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 k8s.io/apimachinery v0.29.0 @@ -373,7 +374,7 @@ require ( golang.org/x/oauth2 v0.13.0 // indirect golang.org/x/sync v0.6.0 golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect + golang.org/x/term v0.16.0 golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 golang.org/x/tools v0.16.1 // indirect diff --git a/go.sum b/go.sum index 6e43170e77..4c8c14f551 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,7 @@ github.com/BTBurke/k8sresource v1.2.0 h1:yIwuKJj4cQQVyWF5hGNhXmZNZ3VLLVH1jyGfL0a github.com/BTBurke/k8sresource v1.2.0/go.mod h1:3Sa2yHvNmOvwzP/WU8joqU4ZbBGUzToZPR9MbaDt38g= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Kubuxu/go-os-helper v0.0.1/go.mod h1:N8B+I7vPCT80IcP58r50u4+gEEcsZETFUpAzWW2ep1Y= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= @@ -72,6 +73,8 @@ github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8 github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= @@ -88,6 +91,7 @@ github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5 h1:iW0a5 github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5/go.mod h1:Y2QMoi1vgtOIfc+6DhrMOGkLoGzqSV2rKp4Sm+opsyA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -188,6 +192,7 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -261,6 +266,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dylibso/observe-sdk/go v0.0.0-20231201014635-141351c24659 h1:RGgHymaENttkVRf0YEzly0Cr2q8xB56WuDEsn8oFXHE= github.com/dylibso/observe-sdk/go v0.0.0-20231201014635-141351c24659/go.mod h1:7h4vx/+0cUjKN2f+ynM4tcC8kIjJqP6W2cLcn7buXl4= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= @@ -587,6 +594,7 @@ github.com/ipfs/go-graphsync v0.14.4/go.mod h1:yT0AfjFgicOoWdAlUJ96tQ5AkuGI4r1ta github.com/ipfs/go-ipfs-blockstore v1.3.0 h1:m2EXaWgwTzAfsmt5UdJ7Is6l4gJcaM/A12XwJyvYvMM= github.com/ipfs/go-ipfs-blockstore v1.3.0/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ= +github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk= github.com/ipfs/go-ipfs-chunker v0.0.5 h1:ojCf7HV/m+uS2vhUGWcogIIxiO5ubl5O57Q7NapWLY8= github.com/ipfs/go-ipfs-chunker v0.0.5/go.mod h1:jhgdF8vxRHycr00k13FM8Y0E+6BoalYeobXmUyTreP8= github.com/ipfs/go-ipfs-cmds v0.9.0 h1:K0VcXg1l1k6aY6sHnoxYcyimyJQbcV1ueXuWgThmK9Q= @@ -1054,6 +1062,7 @@ github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285/go.m github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= @@ -1069,6 +1078,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= @@ -1341,8 +1352,9 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/ops/aws/canary/lambda/go.mod b/ops/aws/canary/lambda/go.mod index 04056f3ff5..14aca0427b 100644 --- a/ops/aws/canary/lambda/go.mod +++ b/ops/aws/canary/lambda/go.mod @@ -304,14 +304,13 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.18.0 // indirect golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/oauth2 v0.13.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.16.1 // indirect diff --git a/ops/aws/canary/lambda/go.sum b/ops/aws/canary/lambda/go.sum index d7671afce6..8c8475f75a 100644 --- a/ops/aws/canary/lambda/go.sum +++ b/ops/aws/canary/lambda/go.sum @@ -1176,7 +1176,7 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/authn/ask/ask_ns_example.rego b/pkg/authn/ask/ask_ns_example.rego new file mode 100644 index 0000000000..abebbf467b --- /dev/null +++ b/pkg/authn/ask/ask_ns_example.rego @@ -0,0 +1,34 @@ +package bacalhau.authn + +import rego.v1 + +schema := { + "type": "object", + "properties": {"magic": {"type": "string"}}, + "required": ["magic"], +} + +token := t if { + input.magic == "open sesame" + + t := io.jwt.encode_sign( + { + "typ": "JWT", + "alg": "RS256", + }, + { + "iss": input.nodeId, + "sub": "aladdin", + "aud": [input.nodeId], + "iat": now, + "exp": one_month, + "ns": { + # Read-only access to all namespaces + "*": read_only, + # Writable access to own namespace + "genie": full_access, + }, + }, + input.signingKey, + ) +} diff --git a/pkg/authn/ask/ask_ns_password.rego b/pkg/authn/ask/ask_ns_password.rego new file mode 100644 index 0000000000..35e4604f5f --- /dev/null +++ b/pkg/authn/ask/ask_ns_password.rego @@ -0,0 +1,67 @@ +package bacalhau.authn + +import rego.v1 + +# Implements a policy where clients that supply a valid username and password +# are permitted access. Anonymous users are not permitted. +# +# Modify the `userlist` to control what users are permitted access. +# Modify the `ns` key of the token to control what namespaces they can access. + +schema := { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "username": {"type": "string"}, + "password": {"type": "string", "writeOnly": true}, + }, + "required": ["username", "password"], +} + +now := time.now_ns() / 1000 + +one_month := time.add_date(time.now_ns(), 0, 1, 0) / 1000 + +# userlist should be a map of usernames to scrypt-hashed passwords and salts. in +# a simple live setup they can be hard coded here as a map, and then apply +# appropriate file permissions to this policy. +userlist := { + # "username": ["hash", "salt"] +} + +valid_user := input.ask.username if { + input.ask.username in userlist + + hash := userlist[input.ask.username][0] + salt := userlist[input.ask.username][1] + hash == scrypt(input.ask.password, salt) +} + +token := io.jwt.encode_sign( + { + "typ": "JWT", + "alg": "RS256", + }, + { + "iss": input.nodeId, + "sub": valid_user, + "aud": [input.nodeId], + "iat": now, + "exp": one_month, + "ns": { + # Read-only access to all namespaces + "*": read_only, + # Writable access to own namespace + valid_user: full_access, + }, + }, + input.signingKey, +) + +namespace_read := 1 +namespace_write := 2 +namespace_download := 4 +namespace_cancel := 8 + +read_only := bits.and(namespace_read, namespace_download) +full_access := bits.and(bits.and(namespace_write, namespace_cancel), read_only) diff --git a/pkg/authn/ask/ask_ns_test_password.rego b/pkg/authn/ask/ask_ns_test_password.rego new file mode 100644 index 0000000000..c174d3afdb --- /dev/null +++ b/pkg/authn/ask/ask_ns_test_password.rego @@ -0,0 +1,71 @@ +package bacalhau.authn + +import rego.v1 + +# Implements a policy where clients that supply a valid username and password +# are permitted access. Anonymous users are not permitted. +# +# Modify the `userlist` to control what users are permitted access. +# Modify the `ns` key of the token to control what namespaces they can access. + +schema := { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "username": {"type": "string"}, + "password": {"type": "string", "writeOnly": true}, + }, + "required": ["username", "password"], +} + +now := time.now_ns() / 1000 + +one_month := time.add_date(time.now_ns(), 0, 1, 0) / 1000 + +# userlist should be a map of usernames to scrypt-hashed passwords and salts. in +# a simple live setup they can be hard coded here as a map, and then apply +# appropriate file permissions to this policy. +userlist := {"username": [ + # hash corresponding to "password" + "SN1U4DxjUhzyYG6p6nQ4by0IpudU8wdNs7Fpp42Ky9M=", + # a randomly generated salt + "d9ucnhE5kHEqm0YEWqN5qJmrHB+IDqjuPEwLkmZ9BGs=", +]} + +valid_user := user if { + some input.ask.username, _ in userlist + + user := input.ask.username + hash := userlist[input.ask.username][0] + salt := userlist[input.ask.username][1] + hash == scrypt(input.ask.password, salt) +} + +token := io.jwt.encode_sign( + { + "typ": "JWT", + "alg": "RS256", + }, + { + "iss": input.nodeId, + "sub": valid_user, + "aud": [input.nodeId], + "iat": now, + "exp": one_month, + "ns": { + # Read-only access to all namespaces + "*": read_only, + # Writable access to own namespace + valid_user: full_access, + }, + }, + input.signingKey, +) + +namespace_read := 1 +namespace_write := 2 +namespace_download := 4 +namespace_cancel := 8 + +read_only := bits.and(namespace_read, namespace_download) +full_access := bits.and(bits.and(namespace_write, namespace_cancel), read_only) diff --git a/pkg/authn/ask/authenticator.go b/pkg/authn/ask/authenticator.go new file mode 100644 index 0000000000..c81b73bc92 --- /dev/null +++ b/pkg/authn/ask/authenticator.go @@ -0,0 +1,90 @@ +package ask + +import ( + "context" + "crypto/rsa" + "encoding/json" + + "github.com/bacalhau-project/bacalhau/pkg/authn" + "github.com/bacalhau-project/bacalhau/pkg/lib/policy" + "github.com/lestrrat-go/jwx/jwk" + "github.com/pkg/errors" + "github.com/samber/lo" +) + +type policyData struct { + SigningKey jwk.Key `json:"signingKey"` + NodeID string `json:"nodeId"` + Ask map[string]string `json:"ask"` +} + +type requiredSchema = map[string]any + +const schemaRule = "bacalhau.authn.schema" + +type askAuthenticator struct { + authnPolicy *policy.Policy + key jwk.Key + nodeID string + + validate policy.Query[policyData, string] + schema policy.Query[any, requiredSchema] +} + +func NewAuthenticator(p *policy.Policy, key *rsa.PrivateKey, nodeID string) authn.Authenticator { + return askAuthenticator{ + authnPolicy: p, + key: lo.Must(jwk.New(key)), + nodeID: nodeID, + validate: policy.AddQuery[policyData, string](p, authn.PolicyTokenRule), + schema: policy.AddQuery[any, requiredSchema](p, schemaRule), + } +} + +// Authenticate implements authn.Authenticator. +func (authenticator askAuthenticator) Authenticate(ctx context.Context, req []byte) (authn.Authentication, error) { + var userInput map[string]string + err := json.Unmarshal(req, &userInput) + if err != nil { + return authn.Error(errors.Wrap(err, "invalid authentication data")) + } + + input := policyData{ + SigningKey: authenticator.key, + NodeID: authenticator.nodeID, + Ask: userInput, + } + + token, err := authenticator.validate(ctx, input) + if errors.Is(err, policy.ErrNoResult) { + return authn.Failed("credentials rejected"), nil + } else if err != nil { + return authn.Error(err) + } + + return authn.Authentication{Success: true, Token: token}, nil +} + +func (authenticator askAuthenticator) Schema(ctx context.Context) ([]byte, error) { + schema, err := authenticator.schema(ctx, nil) + if err != nil { + return nil, err + } + + return json.Marshal(schema) +} + +// IsInstalled implements authn.Authenticator. +func (authenticator askAuthenticator) IsInstalled(ctx context.Context) (bool, error) { + schema, err := authenticator.Schema(ctx) + return err == nil && schema != nil, err +} + +// Requirement implements authn.Authenticator. +func (authenticator askAuthenticator) Requirement() authn.Requirement { + params := lo.Must(authenticator.Schema(context.TODO())) + return authn.Requirement{ + Type: authn.MethodTypeAsk, + Params: (*json.RawMessage)(¶ms), + } +} diff --git a/pkg/authn/ask/authenticator_test.go b/pkg/authn/ask/authenticator_test.go new file mode 100644 index 0000000000..c753b9ab17 --- /dev/null +++ b/pkg/authn/ask/authenticator_test.go @@ -0,0 +1,82 @@ +//go:build unit || !integration + +package ask + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "embed" + "encoding/json" + "testing" + + "github.com/bacalhau-project/bacalhau/pkg/authn" + "github.com/bacalhau-project/bacalhau/pkg/lib/policy" + "github.com/bacalhau-project/bacalhau/pkg/logger" + "github.com/stretchr/testify/require" +) + +//go:embed *.rego +var policies embed.FS + +func setup(t *testing.T) authn.Authenticator { + logger.ConfigureTestLogging(t) + + authPolicy, err := (policy.FromFS(policies, "ask_ns_test_password.rego")) + require.NoError(t, err) + + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + return NewAuthenticator(authPolicy, rsaKey, "node") +} + +func try(t *testing.T, authenticator authn.Authenticator, r any) authn.Authentication { + req, err := json.Marshal(r) + require.NoError(t, err) + + auth, err := authenticator.Authenticate(context.Background(), req) + require.NoError(t, err) + return auth +} + +func TestRequirement(t *testing.T) { + authenticator := setup(t) + + requirement := authenticator.Requirement() + require.Equal(t, authn.MethodTypeAsk, requirement.Type) + require.NoError(t, json.Unmarshal(*requirement.Params, &requiredSchema{})) +} + +func TestUnknownUser(t *testing.T) { + authenticator := setup(t) + + auth := try(t, authenticator, map[string]string{ + "username": "robert", + "password": "password", + }) + require.False(t, auth.Success, auth.Reason) + require.Empty(t, auth.Token) +} + +func TestIncorrectPassword(t *testing.T) { + authenticator := setup(t) + + auth := try(t, authenticator, map[string]string{ + "username": "username", + "password": "drowssap", + }) + require.False(t, auth.Success, auth.Reason) + require.Empty(t, auth.Token) +} + +func TestGoodResponse(t *testing.T) { + authenticator := setup(t) + + auth := try(t, authenticator, map[string]string{ + "username": "username", + "password": "password", + }) + require.True(t, auth.Success, auth.Reason) + require.NotEmpty(t, auth.Token) +} diff --git a/pkg/authn/ask/gen_password/main.go b/pkg/authn/ask/gen_password/main.go new file mode 100644 index 0000000000..25e0706aa1 --- /dev/null +++ b/pkg/authn/ask/gen_password/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "os" + + "github.com/bacalhau-project/bacalhau/pkg/lib/policy" + "golang.org/x/term" +) + +const saltLength = 32 + +func main() { + fmt.Fprintf(os.Stderr, "Password: ") + + password, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + + salt := make([]byte, saltLength) + _, err = rand.Read(salt) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + + hash, err := policy.Scrypt(password, salt) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + + output := [][]byte{hash, salt} + err = json.NewEncoder(os.Stdout).Encode(output) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } +} diff --git a/pkg/authn/types.go b/pkg/authn/types.go index b5716bac00..f582adaef8 100644 --- a/pkg/authn/types.go +++ b/pkg/authn/types.go @@ -40,6 +40,9 @@ const ( // An authentication method that provides a challenge string that the user // must sign using their private key. MethodTypeChallenge MethodType = "challenge" + + // An authentication method that asks the user to supply some credentials. + MethodTypeAsk MethodType = "ask" ) // Requirement represents information about how to authenticate using a diff --git a/pkg/lib/policy/policy.go b/pkg/lib/policy/policy.go index 80b95c95e6..285f956ab7 100644 --- a/pkg/lib/policy/policy.go +++ b/pkg/lib/policy/policy.go @@ -67,7 +67,7 @@ type Query[Input, Output any] func(ctx context.Context, input Input) (Output, er // certain input type and returns a function that will execute the query when // given input of that type. func AddQuery[Input, Output any](runner *Policy, rule string) Query[Input, Output] { - opts := append(runner.modules, rego.Query("data."+rule)) + opts := append(runner.modules, rego.Query("data."+rule), scryptFn) query := lo.Must(rego.New(opts...).PrepareForEval(context.Background())) return func(ctx context.Context, t Input) (Output, error) { diff --git a/pkg/lib/policy/scrypt.go b/pkg/lib/policy/scrypt.go new file mode 100644 index 0000000000..15e7e41da7 --- /dev/null +++ b/pkg/lib/policy/scrypt.go @@ -0,0 +1,60 @@ +package policy + +import ( + "encoding/base64" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/rego" + "github.com/open-policy-agent/opa/types" + "golang.org/x/crypto/scrypt" +) + +// See https://pkg.go.dev/golang.org/x/crypto/scrypt +const ( + n = 32768 + r = 8 + p = 1 + keyLen = 32 +) + +func Scrypt(password, salt []byte) ([]byte, error) { + return scrypt.Key(password, salt, n, r, p, keyLen) +} + +// scryptFn exposes the `scrypt` password hashing primitive to Rego. +var scryptFn = rego.Function2( + ®o.Function{ + Name: "scrypt", + Description: "Run the scrypt key derivation function", + Decl: types.NewFunction(types.Args(types.S, types.S), types.S), + Memoize: true, + Nondeterministic: false, + }, + func(bctx rego.BuiltinContext, passwordTerm, saltTerm *ast.Term) (*ast.Term, error) { + var password, salt string + if err := ast.As(passwordTerm.Value, &password); err != nil { + return nil, err + } + if err := ast.As(saltTerm.Value, &salt); err != nil { + return nil, err + } + + saltBytes, err := base64.StdEncoding.DecodeString(salt) + if err != nil { + return nil, err + } + + passwordBytes := []byte(password) + hash, err := Scrypt(passwordBytes, saltBytes) + if err != nil { + return nil, err + } + + value, err := ast.InterfaceToValue(base64.StdEncoding.EncodeToString(hash)) + if err != nil { + return nil, err + } + + return ast.NewTerm(value), nil + }, +) diff --git a/pkg/node/factories.go b/pkg/node/factories.go index f692e5227b..9c424d1d15 100644 --- a/pkg/node/factories.go +++ b/pkg/node/factories.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/bacalhau-project/bacalhau/pkg/authn" + "github.com/bacalhau-project/bacalhau/pkg/authn/ask" "github.com/bacalhau-project/bacalhau/pkg/authn/challenge" "github.com/bacalhau-project/bacalhau/pkg/config" "github.com/bacalhau-project/bacalhau/pkg/executor" @@ -136,26 +137,36 @@ func NewStandardAuthenticatorsFactory() AuthenticatorsFactory { return AuthenticatorsFactoryFunc( func(ctx context.Context, nodeConfig NodeConfig) (authn.Provider, error) { var allErr error - authns := make(map[string]authn.Authenticator, len(nodeConfig.AuthConfig.Methods)) + privKey, allErr := config.GetClientPrivateKey() + if allErr != nil { + return nil, allErr + } + authns := make(map[string]authn.Authenticator, len(nodeConfig.AuthConfig.Methods)) for name, authnConfig := range nodeConfig.AuthConfig.Methods { switch authnConfig.Type { case authn.MethodTypeChallenge: - privKey, err := config.GetClientPrivateKey() + methodPolicy, err := policy.FromPathOrDefault(authnConfig.PolicyPath, challenge.AnonymousModePolicy) if err != nil { allErr = multierr.Append(allErr, err) continue } - methodPolicy, err := policy.FromPathOrDefault(authnConfig.PolicyPath, challenge.AnonymousModePolicy) + authns[name] = challenge.NewAuthenticator( + methodPolicy, + challenge.NewStringMarshaller(nodeConfig.NodeID), + privKey, + nodeConfig.NodeID, + ) + case authn.MethodTypeAsk: + methodPolicy, err := policy.FromPath(authnConfig.PolicyPath) if err != nil { allErr = multierr.Append(allErr, err) continue } - authns[name] = challenge.NewAuthenticator( + authns[name] = ask.NewAuthenticator( methodPolicy, - challenge.NewStringMarshaller(nodeConfig.NodeID), privKey, nodeConfig.NodeID, )