Repository: bacalhau-project/bacalhau (language: python)

Issue:
auth token written when not required. 
## Reproduce
Using these policies with token `"abc"`: 
https://github.com/bacalhau-project/bacalhau/blob/main/pkg/authn/ask/ask_ns_secret.rego
https://github.com/bacalhau-project/bacalhau/blob/main/pkg/authz/policies/policy_ns_anon.rego
### when asked for a token, press enter, don't type anything
```
~> bacalhau --api-host=0.0.0.0 --repo=/home/frrist/.bacalhau2/ node list
15:37:54.144 | INF pkg/repo/fs.go:81 > Initializing repo at '/home/frrist/.bacalhau2/' for environment 'production'
token: <just press enter>
 ID        TYPE       LABELS  CPU  MEMORY  DISK  GPU 
 QmchH1Ym  Requester  
 ```   
 ### observe a token is created anyways
 ```                            
~> cat ~/.bacalhau2/tokens.json
{"http://0.0.0.0:1234":"<omitted>"}
```



Discussion / hints:
Yes, dumbo bug, the rule that checks whether the secret is valid is never actually referenced.

Fix patch:
diff --git a/Makefile b/Makefile
index 1759f80d86..92a3df3ea0 100644
--- a/Makefile
+++ b/Makefile
@@ -287,7 +287,9 @@ clean: clean-plugins
 # Target: test
 ################################################################################
 .PHONY: test
-test:
+test: unit-test bash-test
+
+.PHONY: unit-test
 # unittests parallelize well (default go test behavior is to parallelize)
 	go test ./... -v --tags=unit
 
@@ -301,6 +303,10 @@ integration-test:
 # integration tests parallelize less well (hence -p 1)
 	go test ./... -v --tags=integration -p 1
 
+.PHONY: bash-test
+bash-test: ${BINARY_PATH}
+	cd test && bin/bashtub *.sh
+
 .PHONY: grc-test
 grc-test:
 	grc go test ./... -v
@@ -494,4 +500,4 @@ spellcheck-code:  ## Runs a spellchecker over all code - MVP just does one file
 
 .PHONY: spellcheck-docs
 spellcheck-docs:  ## Runs a spellchecker over all documentation - MVP just does one directory
-	cspell -c .cspell-docs.json lint ./docs/docs/dev/**
\ No newline at end of file
+	cspell -c .cspell-docs.json lint ./docs/docs/dev/**
diff --git a/bin/.keep b/bin/.keep
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/cmd/cli/config/set.go b/cmd/cli/config/set.go
index 2307a89559..88cc74c516 100644
--- a/cmd/cli/config/set.go
+++ b/cmd/cli/config/set.go
@@ -107,11 +107,7 @@ func setConfig(key string, values ...string) error {
 			return err
 		}
 		methodNamePath := fmt.Sprintf("%s.%s", types.AuthMethods, cfg.Method)
-		methodNameTypePath := fmt.Sprintf("%s.type", methodNamePath)
-		methodNamePolicyPath := fmt.Sprintf("%s.policypath", methodNamePath)
-		viperWriter.Set(methodNamePath, cfg.Method)
-		viperWriter.Set(methodNameTypePath, cfg.Policy.Type)
-		viperWriter.Set(methodNamePolicyPath, cfg.Policy.PolicyPath)
+		viperWriter.Set(methodNamePath, cfg.Policy)
 	}
 
 	parser, err := getParser(curValue, key)
diff --git a/cmd/util/auth/ask.go b/cmd/util/auth/ask.go
index e1f6aee673..82346766c6 100644
--- a/cmd/util/auth/ask.go
+++ b/cmd/util/auth/ask.go
@@ -50,7 +50,7 @@ func askResponder(cmd *cobra.Command) responder {
 
 			// 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 {
+			if subschema.WriteOnly && term.IsTerminal(int(os.Stdin.Fd())) {
 				input, err = term.ReadPassword(int(os.Stdin.Fd()))
 				fmt.Fprintln(cmd.ErrOrStderr())
 			} else {
diff --git a/pkg/authn/ask/ask_ns_password.rego b/pkg/authn/ask/ask_ns_password.rego
index 35e4604f5f..c5224ecebf 100644
--- a/pkg/authn/ask/ask_ns_password.rego
+++ b/pkg/authn/ask/ask_ns_password.rego
@@ -63,5 +63,5 @@ 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)
+read_only := bits.or(namespace_read, namespace_download)
+full_access := bits.or(bits.or(namespace_write, namespace_cancel), read_only)
diff --git a/pkg/authn/ask/ask_ns_secret.rego b/pkg/authn/ask/ask_ns_secret.rego
index 45f8cb8a99..ab56e6f18e 100644
--- a/pkg/authn/ask/ask_ns_secret.rego
+++ b/pkg/authn/ask/ask_ns_secret.rego
@@ -30,28 +30,31 @@ valid_secret if {
 	expected_secret == input.ask.token
 }
 
-token := io.jwt.encode_sign(
-	{
-		"typ": "JWT",
-		"alg": "RS256",
-	},
-	{
-		"iss": input.nodeId,
-		"aud": [input.nodeId],
-		"iat": now,
-		"exp": one_month,
-		"ns": {
-			# Full access to all namespaces
-			"*": full_access,
+token := t if {
+	valid_secret
+	t := io.jwt.encode_sign(
+		{
+			"typ": "JWT",
+			"alg": "RS256",
 		},
-	},
-	input.signingKey,
-)
+		{
+			"iss": input.nodeId,
+			"aud": [input.nodeId],
+			"iat": now,
+			"exp": one_month,
+			"ns": {
+				# Full access to all namespaces
+				"*": 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)
+read_only := bits.or(namespace_read, namespace_download)
+full_access := bits.or(bits.or(namespace_write, namespace_cancel), read_only)
diff --git a/pkg/authn/challenge/challenge_ns_anon.rego b/pkg/authn/challenge/challenge_ns_anon.rego
index 1091cfcd61..49bcf28bc6 100644
--- a/pkg/authn/challenge/challenge_ns_anon.rego
+++ b/pkg/authn/challenge/challenge_ns_anon.rego
@@ -37,5 +37,5 @@ 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)
+read_only := bits.or(namespace_read, namespace_download)
+full_access := bits.or(bits.or(namespace_write, namespace_cancel), read_only)
diff --git a/pkg/authn/challenge/challenge_ns_no_anon.rego b/pkg/authn/challenge/challenge_ns_no_anon.rego
index a463f4e005..d13b9c7362 100644
--- a/pkg/authn/challenge/challenge_ns_no_anon.rego
+++ b/pkg/authn/challenge/challenge_ns_no_anon.rego
@@ -46,5 +46,5 @@ 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)
+read_only := bits.or(namespace_read, namespace_download)
+full_access := bits.or(bits.or(namespace_write, namespace_cancel), read_only)
diff --git a/pkg/authz/policies/policy_ns_anon.rego b/pkg/authz/policies/policy_ns_anon.rego
index 0dbefb2fb9..c11404d4a1 100644
--- a/pkg/authz/policies/policy_ns_anon.rego
+++ b/pkg/authz/policies/policy_ns_anon.rego
@@ -39,9 +39,21 @@ allow if {
 
 
 # The permissions the access token grants on the job namespace
-job_namespace_perms := token_namespaces[job_namespace]
+job_namespace_perms := bits.or(token_namespaces[job_namespace], token_namespaces["*"]) if {
+    token_namespaces[job_namespace]
+    token_namespaces["*"]
+}
+
+job_namespace_perms := token_namespaces[job_namespace] if {
+    token_namespaces[job_namespace]
+}
+
+job_namespace_perms := token_namespaces["*"] if {
+    token_namespaces["*"]
+}
 
 # The namespace that the submitted job is going into
+default job_namespace := ""
 job_namespace := ns if {
     jobRequest := yaml.unmarshal(input.http.body)
     ns := jobRequest["namespace"]


Test patch:
diff --git a/pkg/authn/ask/ask_ns_test_password.rego b/pkg/authn/ask/ask_ns_test_password.rego
index c174d3afdb..3c6031f5b8 100644
--- a/pkg/authn/ask/ask_ns_test_password.rego
+++ b/pkg/authn/ask/ask_ns_test_password.rego
@@ -67,5 +67,5 @@ 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)
+read_only := bits.or(namespace_read, namespace_download)
+full_access := bits.or(bits.or(namespace_write, namespace_cancel), read_only)
diff --git a/test/bin/bacalhau.sh b/test/bin/bacalhau.sh
new file mode 100644
index 0000000000..5a5c44f982
--- /dev/null
+++ b/test/bin/bacalhau.sh
@@ -0,0 +1,62 @@
+export IFS=$'\n\t'
+
+new_repo() {
+    export BACALHAU_DIR=$(mktemp -d)
+    bacalhau id >/dev/null 2>&1
+}
+
+clean_repo() {
+    rm -rf $BACALHAU_DIR
+}
+
+declare -i RUNNING_NODES=0
+
+create_node() {
+    TYPE=$1
+    shift 1
+    $BACALHAU serve --peer=none --node-type=$TYPE $@ 1>$BACALHAU_DIR/out.log 2>$BACALHAU_DIR/err.log &
+    NODE_PID=$!
+    RUNNING_NODES+=1
+    {
+        while ! ls $BACALHAU_DIR/bacalhau.run 2>/dev/null; do
+            if ! ps $NODE_PID; then
+                echo "$BACALHAU serve --peer=none --node-type=$TYPE $@ failed to start?" 1>&2;
+                echo `$BACALHAU serve --peer=none --node-type=$TYPE $@` 1>&2
+                exit 1
+            fi
+        done
+    } 1>/dev/null
+    if test "$TYPE" = "requester"; then
+        source $BACALHAU_DIR/bacalhau.run
+    fi
+}
+
+teardown_nodes() {
+    for i in $(seq 1 +1 $RUNNING_NODES 2>/dev/null || true); do
+        kill -15 %$i
+        wait %$i 2>/dev/null
+    done;
+}
+
+require_docker() {
+    subject docker info
+    assert_equal 0 $status
+}
+
+before_all() {
+    ROOT=$(git rev-parse --show-toplevel)
+    BACALHAU_BINARY=$(find $ROOT/bin -name 'bacalhau')
+    BACALHAU=$BACALHAU_BINARY
+    export LOG_LEVEL=WARN
+    # export BACALHAU_NODE_SERVERAPI_HOST='localhost'
+    export PATH=$(dirname $BACALHAU_BINARY):$PATH
+}
+
+before_each() {
+    new_repo
+}
+
+after_each() {
+    teardown_nodes
+    clean_repo
+}
diff --git a/test/bin/bashtub b/test/bin/bashtub
new file mode 100755
index 0000000000..e206eb99f6
--- /dev/null
+++ b/test/bin/bashtub
@@ -0,0 +1,182 @@
+#!/usr/bin/env bash
+
+# The MIT License (MIT)
+#
+# Copyright (c) 2015 Shin'ya Ueoka
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+shopt -s expand_aliases
+set +x
+
+declare -i TEST_CASE_COUNT=0
+declare -a FAILED_CASES=()
+declare -a FAILURE_LOCATIONS=()
+declare -a FAILURE_REASONS=()
+
+to_sentence() {
+  declare space_separated
+  space_separated=${1#testcase_}
+  space_separated=${space_separated//_/ }
+  echo "$(tr '[:lower:]' '[:upper:]' <<< ${space_separated:0:1})${space_separated:1}"
+}
+
+assert_equal_matcher() {
+  echo "\`$1' expected but was \`$2'"
+  [[ "$1" == "$2" ]]
+}
+
+assert_not_equal_matcher() {
+  echo "\`$1' not expected but was \`$2'"
+  [[ ! "$1" == "$2" ]]
+}
+
+assert_match_matcher() {
+  echo "\`$2' was expected to match \`$1'"
+  [[ "$2" =~ "$1" ]]
+}
+
+assert_true_matcher() {
+  echo "\`$*' was expected to return true"
+  subject "$@"
+  [[ $status -eq 0 ]]
+}
+
+assert_false_matcher() {
+  echo "\`$*' was expected to return false"
+  subject "$@"
+  [[ $status -ne 0 ]]
+}
+
+located_assertion_base() {
+  declare location case_name matcher
+  location="$1:$2:in \`$3'"
+  case_name=$(to_sentence "$3")
+  matcher=$4
+  shift 4;
+
+  TEST_CASE_COUNT+=1
+  reason=$($matcher "$@")
+  if [[ $? -eq 0 ]]; then
+    echo -n $'\e[32m.\e[m'
+  else
+    echo -n $'\e[31mF\e[m'
+    FAILED_CASES+=("$case_name")
+    FAILURE_LOCATIONS+=("$location")
+    FAILURE_REASONS+=("$reason")
+  fi
+}
+
+subject() {
+  eval "$({ err=$({ out=$(eval $@); sta=$?; } 2>&1; declare -p out sta >&2); declare -p err; } 2>&1)"
+  stdout="$out" stderr="$err" status="$sta"
+}
+
+print_result() {
+  echo -e '\n'
+
+  if [[ ${#FAILED_CASES[@]} -eq 0 ]]; then
+    echo "$TEST_CASE_COUNT assertions, 0 failures"
+    return 0
+  else
+    echo "Failures:"
+    for ((i = 0; i < ${#FAILED_CASES[@]}; ++i)) {
+      echo "$((i + 1))) ${FAILED_CASES[$i]}"
+      echo -n $'\e[31m'
+      echo "    ${FAILURE_LOCATIONS[$i]}"
+      echo "    ${FAILURE_REASONS[$i]}"
+      echo -n $'\e[m'
+    }
+    echo
+    echo -n $'\e[31m'
+    echo "$TEST_CASE_COUNT assertions, ${#FAILED_CASES[@]} failures"
+    echo -n $'\e[m'
+    return 1
+  fi
+}
+
+declare_assertions() {
+  local matcher
+  for matcher in $(compgen -A function | grep '_matcher$'); do
+    aliased_id=${matcher%_matcher}
+    alias $aliased_id='located_assertion_base $BASH_SOURCE $LINENO $FUNCNAME '"$matcher"
+  done
+}
+
+help_and_exit() {
+  echo "\
+Usage: $0 [OPTION]... SOURCE_FILE...
+Run a unit-test from SOURCE_FILE(s).
+
+    --help     display this help and exit
+    --version  output version information and exit"
+  exit 0
+}
+
+version_and_exit() {
+  echo "bashtub 0.2"
+  exit 0
+}
+
+unrecognized_option_and_exit() {
+  echo "\
+$0: unrecognized option -- \`$1'
+Try \`$0 --help' for more information." >&2
+  exit 1
+}
+
+for param in "$@"; do
+  case $param in
+  --help) help_and_exit;;
+  --version) version_and_exit;;
+  --*) unrecognized_option_and_exit "${param#--}" ;;
+  -*) unrecognized_option_and_exit "${param#-}" ;;
+  *) break ;;
+  esac
+  shift
+done
+
+declare_assertions
+
+logfile=$(mktemp -t tmp.XXXXXX)
+exec 3>$logfile
+
+for f in "$@"; do
+  ( source "$f"
+    declare -f before_all 2>/dev/null 1>&2 && before_all
+    for testcase in $(compgen -A function | grep '^testcase_'); do
+      ( source $logfile
+        printf $'%s\t%s: ' $f $(to_sentence "$testcase")
+        declare -f before_each 2>/dev/null 1>&2 && before_each
+        $testcase
+        declare -f after_each 2>/dev/null 1>&2 && after_each
+        printf $'\n'
+        declare -p FAILED_CASES FAILURE_LOCATIONS FAILURE_REASONS TEST_CASE_COUNT >&3
+      )
+    done
+    declare -f after_all 2>/dev/null 1>&2 && after_all
+    true
+  )
+done
+
+source $logfile
+rm $logfile
+exec 3>&-
+
+print_result
diff --git a/test/config.sh b/test/config.sh
new file mode 100755
index 0000000000..d0f38068a0
--- /dev/null
+++ b/test/config.sh
@@ -0,0 +1,33 @@
+#!bin/bashtub
+
+source bin/bacalhau.sh
+
+testcase_config_file_remains_empty_after_list() {
+    subject bacalhau config list
+    assert_equal 0 $status
+
+    subject ls $BACALHAU_DIR/config.yaml
+    assert_equal 1 $status
+}
+
+testcase_config_set_is_persistent() {
+    TEST_VALUE=$RANDOM
+    subject bacalhau config set 'User.InstallationID' $TEST_VALUE
+    assert_equal 0 $status
+
+    subject file $BACALHAU_DIR/config.yaml
+    assert_equal 0 $status
+
+    subject bacalhau config list --output=csv
+    assert_match "user.installationid,$TEST_VALUE" "$stdout"
+}
+
+testcase_config_default_ignores_persisted_values() {
+    TEST_VALUE=$RANDOM
+    subject bacalhau config set 'User.InstallationID' $TEST_VALUE
+    assert_equal 0 $status
+
+    subject bacalhau config default
+    assert_equal 0 $status
+    assert_match 'InstallationID: ""' "$stdout"
+}
diff --git a/test/update.sh b/test/update.sh
new file mode 100644
index 0000000000..5b6c0faefc
--- /dev/null
+++ b/test/update.sh
@@ -0,0 +1,11 @@
+#!bin/bashtub
+
+source bin/bacalhau.sh
+
+testcase_default_config_has_updates_enabled() {
+    subject bacalhau config list --output=csv
+    assert_equal 0 $status
+    assert_not_equal $(echo "$stdout" | grep 'update.checkfrequency' | cut -d, -f2) '0'
+    assert_not_equal $(echo "$stdout" | grep 'update.checkfrequency' | cut -d, -f2) ''
+    assert_equal $(echo "$stdout" | grep 'update.skipchecks' | cut -d, -f2) 'false'
+}