Repository: prest/prest
Pull request #747 (2022-12-14 00:10:38)

Prefer constant name.
Prefer using the constant available.

pull-request-quantifier[bot]: ### ![](https://img.shields.io/static/v1?label=Quantified&message=Extra%20Small&color=green)

This PR has `2` quantified lines of changes. In general, a change size of upto `200` lines is ideal for the best PR experience!

------

<details >
    <summary display="inline"> <strong>Quantification details</strong></summary>
    <p />

```
Label      : Extra Small
Size       : +1 -1
Percentile : 0.8%

Total files changed: 1

Change summary by file extension:
.go : +1 -1
```
> Change counts above are quantified counts, based on the [PullRequestQuantifier customizations](https://github.com/microsoft/PullRequestQuantifier/blob/main/docs/prquantifier-yaml.md).
    
</details>


<details>
    <summary display="inline"> <strong>Why proper sizing of changes matters</strong> </summary>
    <p/>
    <p/>

Optimal pull request sizes drive a better predictable PR flow as they strike a
balance between between PR complexity and PR review overhead. PRs within the
optimal size (typical small, or medium sized PRs) mean:

- Fast and predictable releases to production:
  - Optimal size changes are more likely to be reviewed faster with fewer
iterations.
  - Similarity in low PR complexity drives similar review times.
- Review quality is likely higher as complexity is lower:
  - Bugs are more likely to be detected.
  - Code inconsistencies are more likely to be detected.
- Knowledge sharing is improved within the participants:
  - Small portions can be assimilated better.
- Better engineering practices are exercised:
  - Solving big problems by dividing them in well contained, smaller problems.
  - Exercising separation of concerns within the code changes.

#### What can I do to optimize my changes

- Use the PullRequestQuantifier to quantify your PR accurately
  - Create a context profile for your repo using the [context generator](https://github.com/microsoft/PullRequestQuantifier/releases)
  - Exclude files that are not necessary to be reviewed or do not increase the review complexity. Example: Autogenerated code, docs, project IDE setting files, binaries, etc. Check out the `Excluded` section from your `prquantifier.yaml` context profile. 
  - Understand your typical change complexity, drive towards the desired complexity by adjusting the label mapping in your `prquantifier.yaml` context profile.
  - Only use the labels that matter to you, [see context specification](./docs/prquantifier-yaml.md) to customize your `prquantifier.yaml` context profile.
- Change your engineering behaviors
  - For PRs that fall outside of the desired spectrum, review the details and check if:
    - Your PR could be split in smaller, self-contained PRs instead
    - Your PR only solves one particular issue. (For example, don't refactor and code new features in the same PR).

#### How to interpret the change counts in git diff output

- One line was added: `+1 -0`
- One line was deleted: `+0 -1`
- One line was modified: `+1 -1` (git diff doesn't know about modified, it will
interpret that line like one addition plus one deletion)
- Change percentiles: Change characteristics (addition, deletion, modification)
of this PR in relation to all other PRs within the repository.

</details>

<p />

------

Was this comment helpful? <a href="https://pullrequestquantifierfeedback.azurewebsites.net/feedback?payload=eyJBdXRob3JOYW1lIjoiZGlhc2JydW5vIiwiUmVwb3NpdG9yeUxpbmsiOiJodHRwczovL2dpdGh1Yi5jb20vcHJlc3QvcHJlc3QiLCJQdWxsUmVxdWVzdExpbmsiOiJodHRwczovL2dpdGh1Yi5jb20vcHJlc3QvcHJlc3QvcHVsbC83NDciLCJFdmVudFR5cGUiOiJUaHVtYnNVcCJ9&amp;anonymous=True" target="_blank" title="Thumbs up"><strong>:thumbsup:</strong></a> <a href="https://pullrequestquantifierfeedback.azurewebsites.net/feedback?payload=eyJBdXRob3JOYW1lIjoiZGlhc2JydW5vIiwiUmVwb3NpdG9yeUxpbmsiOiJodHRwczovL2dpdGh1Yi5jb20vcHJlc3QvcHJlc3QiLCJQdWxsUmVxdWVzdExpbmsiOiJodHRwczovL2dpdGh1Yi5jb20vcHJlc3QvcHJlc3QvcHVsbC83NDciLCJFdmVudFR5cGUiOiJOZXV0cmFsIn0=&amp;anonymous=True" target="_blank" title="Neutral"><strong>&nbsp;:ok_hand:</strong></a> <a href="https://pullrequestquantifierfeedback.azurewebsites.net/feedback?payload=eyJBdXRob3JOYW1lIjoiZGlhc2JydW5vIiwiUmVwb3NpdG9yeUxpbmsiOiJodHRwczovL2dpdGh1Yi5jb20vcHJlc3QvcHJlc3QiLCJQdWxsUmVxdWVzdExpbmsiOiJodHRwczovL2dpdGh1Yi5jb20vcHJlc3QvcHJlc3QvcHVsbC83NDciLCJFdmVudFR5cGUiOiJUaHVtYnNEb3duIn0=&amp;anonymous=True" target="_blank" title="Thumbs down"><strong>&nbsp;:thumbsdown:</strong></a> (<a href="MAILTO:prquantifier@microsoft.com" title="Mail to prquantifier@microsoft.com">Email</a>)
[Customize PullRequestQuantifier](https://github.com/microsoft/PullRequestQuantifier/blob/main/docs/prquantifier-yaml.md) for this repository.


CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/prest/prest?pullRequest=747) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/prest/prest?pullRequest=747) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/prest/prest?pullRequest=747) it.</sub>

Patch:
diff --git a/README.md b/README.md
index 68c72eb..043c90f 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,6 @@
 [![GoDoc](https://godoc.org/github.com/prest/prest?status.png)](https://godoc.org/github.com/prest/prest)
 [![Go Report Card](https://goreportcard.com/badge/github.com/prest/prest)](https://goreportcard.com/report/github.com/prest/prest)
 [![codecov](https://codecov.io/gh/prest/prest/branch/main/graph/badge.svg?token=eVD9urwIEv)](https://codecov.io/gh/prest/prest)
-[![SourceLevel](https://app.sourcelevel.io/github/prest/-/prest.svg)](https://app.sourcelevel.io/github/prest/-/prest)
 [![Homebrew](https://img.shields.io/badge/dynamic/json.svg?url=https://formulae.brew.sh/api/formula/prestd.json&query=$.versions.stable&label=homebrew)](https://formulae.brew.sh/formula/prestd)
 [![Slack](https://img.shields.io/badge/slack-prestd-blueviolet.svg?logo=slack)](http://slack.prestd.com/)
 
diff --git a/adapters/postgres/postgres.go b/adapters/postgres/postgres.go
index 3ae3c56..4ee6039 100644
--- a/adapters/postgres/postgres.go
+++ b/adapters/postgres/postgres.go
@@ -318,7 +318,7 @@ func (adapter *Postgres) ReturningByRequest(r *http.Request) (returningSyntax st
 func sliceToJSONList(ifaceSlice interface{}) (returnValue string, err error) {
     v := reflect.ValueOf(ifaceSlice)
 
-	if v.Kind() == 0 {
+	if v.Kind() == reflect.Invalid {
 		return "[]", ErrEmptyOrInvalidSlice
 	}
 
diff --git a/config/config.go b/config/config.go
index 7edb03a..df2c34e 100644
--- a/config/config.go
+++ b/config/config.go
@@ -44,6 +44,14 @@ type AccessConf struct {
 	Tables      []TablesConf
 }
 
+// ExposeConf (expose data) information
+type ExposeConf struct {
+	Enabled         bool
+	DatabaseListing bool
+	SchemaListing   bool
+	TableListing    bool
+}
+
 // Prest basic config
 type Prest struct {
 	AuthEnabled          bool
@@ -78,6 +86,7 @@ type Prest struct {
 	MigrationsPath       string
 	QueriesPath          string
 	AccessConf           AccessConf
+	ExposeConf           ExposeConf
 	CORSAllowOrigin      []string
 	CORSAllowHeaders     []string
 	CORSAllowMethods     []string
@@ -147,6 +156,10 @@ func viperCfg() {
 	viper.SetDefault("cache.storagepath", "./")
 	viper.SetDefault("cache.sufixfile", ".cache.prestd.db")
 	viper.SetDefault("pluginpath", "./lib")
+	viper.SetDefault("expose.enabled", false)
+	viper.SetDefault("expose.tables", true)
+	viper.SetDefault("expose.schemas", true)
+	viper.SetDefault("expose.databases", true)
 
 	hDir, err := homedir.Dir()
 	if err != nil {
@@ -231,6 +244,10 @@ func Parse(cfg *Prest) (err error) {
 	cfg.Cache.Time = viper.GetInt("cache.time")
 	cfg.Cache.StoragePath = viper.GetString("cache.storagepath")
 	cfg.Cache.SufixFile = viper.GetString("cache.sufixfile")
+	cfg.ExposeConf.Enabled = viper.GetBool("expose.enabled")
+	cfg.ExposeConf.TableListing = viper.GetBool("expose.tables")
+	cfg.ExposeConf.SchemaListing = viper.GetBool("expose.schemas")
+	cfg.ExposeConf.DatabaseListing = viper.GetBool("expose.databases")
 	var cacheendpoints []CacheEndpoint
 	err = viper.UnmarshalKey("cache.endpoints", &cacheendpoints)
 	if err != nil {
diff --git a/docs/deployment/server-configuration.md b/docs/deployment/server-configuration.md
index 7e3acf7..16101ed 100644
--- a/docs/deployment/server-configuration.md
+++ b/docs/deployment/server-configuration.md
@@ -83,6 +83,12 @@ mode = "disable"
 sslcert = "./PATH"
 sslkey = "./PATH"
 sslrootcert = "./PATH"
+
+[expose]
+enabled = true
+databases = true
+schemas = true
+tables = true
 ```
 
 ## Authorization
@@ -145,6 +151,48 @@ password = "password"
 
 > to validate all endpoints with generated jwt token must be activated jwt option
 
+## Expose Data
+
+The expose data setting enables you to configure if you want users to be able to reach listing endpoints, such as:
+
+ - /databases
+ - /schemas
+ - /tables
+
+
+An example of a configuration file disabling all listings:
+
+```toml
+# previous toml content
+[expose]
+    enabled = true
+```
+
+If you want to disable just the database listing:
+
+```toml
+# previous toml content
+[expose]
+    databases = true
+```
+
+| Name | Description |
+| --- | --- |
+| enabled | Set this as `true` if you want to **disable** all listing endpoints available. |
+| databases | Set this as `false` if you want to **disable** *databases* listing endpoints only. |
+| schemas | Set this as `false` if you want to **disable** *schemas* listing endpoints only. |
+| tables | Set this as `false` if you want to **disable** *tables* listing endpoints only. |
+
+### Default values for Exposure Settings
+
+| Name | Default Value |
+| --- | --- |
+| enabled | `false` |
+| databases | `true` |
+| schemas | `true` |
+| tables | `true` |
+
+
 ## SSL
 
 There is 4 options to set on ssl mode:
diff --git a/middlewares/config.go b/middlewares/config.go
index 12f0bdc..3ca3bea 100644
--- a/middlewares/config.go
+++ b/middlewares/config.go
@@ -42,6 +42,9 @@ func initApp() {
 		if config.PrestConf.Cache.Enabled {
 			MiddlewareStack = append(MiddlewareStack, CacheMiddleware())
 		}
+		if config.PrestConf.ExposeConf.Enabled {
+			MiddlewareStack = append(MiddlewareStack, ExposureMiddleware())
+		}
 	}
 	app = negroni.New(MiddlewareStack...)
 }
diff --git a/middlewares/middlewares.go b/middlewares/middlewares.go
index d79030b..9a23e5c 100644
--- a/middlewares/middlewares.go
+++ b/middlewares/middlewares.go
@@ -159,3 +159,27 @@ func Cors(origin []string, headers []string) negroni.Handler {
 		next(w, r)
 	})
 }
+
+func ExposureMiddleware() negroni.Handler {
+	return negroni.HandlerFunc(func(rw http.ResponseWriter, rq *http.Request, next http.HandlerFunc) {
+		url := rq.URL.Path
+		exposeConf := config.PrestConf.ExposeConf
+
+		if strings.HasPrefix(url, "/databases") && !exposeConf.DatabaseListing {
+			http.Error(rw, "unauthorized listing", http.StatusUnauthorized)
+			return
+		}
+
+		if strings.HasPrefix(url, "/tables") && !exposeConf.TableListing {
+			http.Error(rw, "unauthorized listing", http.StatusUnauthorized)
+			return
+		}
+
+		if strings.HasPrefix(url, "/schemas") && !exposeConf.SchemaListing {
+			http.Error(rw, "unauthorized listing", http.StatusUnauthorized)
+			return
+		}
+
+		next(rw, rq)
+	})
+}
diff --git a/router/router.go b/router/router.go
index 705549c..fc8f366 100644
--- a/router/router.go
+++ b/router/router.go
@@ -51,6 +51,7 @@ func GetRouter() *mux.Router {
 	crudRoutes.HandleFunc("/{database}/{schema}/{table}", controllers.DeleteFromTable).Methods("DELETE")
 	crudRoutes.HandleFunc("/{database}/{schema}/{table}", controllers.UpdateTable).Methods("PUT", "PATCH")
 	router.PathPrefix("/").Handler(negroni.New(
+		middlewares.ExposureMiddleware(),
 		middlewares.AccessControl(),
 		middlewares.AuthMiddleware(),
 		middlewares.CacheMiddleware(),


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index b6572c2..ff7ce1e 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -223,3 +223,23 @@ func Test_Auth(t *testing.T) {
 		require.Equal(t, metadata[i], v)
 	}
 }
+
+func Test_ExposeDataConfig(t *testing.T) {
+	os.Setenv("PREST_CONF", "../testdata/prest_expose.toml")
+
+	viperCfg()
+	cfg := &Prest{}
+	err := Parse(cfg)
+	require.NoError(t, err)
+	require.Equal(t, true, cfg.ExposeConf.Enabled)
+	require.Equal(t, false, cfg.ExposeConf.DatabaseListing)
+	require.Equal(t, false, cfg.ExposeConf.SchemaListing)
+	require.Equal(t, false, cfg.ExposeConf.TableListing)
+
+	metadata := []string{"first_name", "last_name", "last_login"}
+	require.Equal(t, len(metadata), len(cfg.AuthMetadata))
+
+	for i, v := range cfg.AuthMetadata {
+		require.Equal(t, metadata[i], v)
+	}
+}
diff --git a/middlewares/config_test.go b/middlewares/config_test.go
index 9f39471..653655c 100644
--- a/middlewares/config_test.go
+++ b/middlewares/config_test.go
@@ -279,3 +279,28 @@ func Test_CORS_Middleware(t *testing.T) {
 	require.NoError(t, err)
 	require.Zero(t, len(body))
 }
+
+func TestExposeTablesMiddleware(t *testing.T) {
+	MiddlewareStack = []negroni.Handler{}
+	app = nil
+	os.Setenv("PREST_DEBUG", "true")
+	os.Setenv("PREST_CONF", "../testdata/prest_expose.toml")
+	config.Load()
+	app = nil
+	r := mux.NewRouter()
+	r.HandleFunc("/tables", controllers.GetTables).Methods("GET")
+	r.HandleFunc("/databases", controllers.GetDatabases).Methods("GET")
+	r.HandleFunc("/schemas", controllers.GetSchemas).Methods("GET")
+	n := GetApp()
+	n.UseHandler(r)
+	server := httptest.NewServer(n)
+	defer server.Close()
+	resp, _ := http.Get(server.URL + "/tables")
+	require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
+
+	resp, _ = http.Get(server.URL + "/databases")
+	require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
+
+	resp, _ = http.Get(server.URL + "/schemas")
+	require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
+}
diff --git a/testdata/prest_expose.toml b/testdata/prest_expose.toml
new file mode 100644
index 0000000..21f0388
--- /dev/null
+++ b/testdata/prest_expose.toml
@@ -0,0 +1,11 @@
+[auth]
+table = "prest_users"
+username = "username"
+password = "password"
+metadata = ["first_name", "last_name", "last_login"]
+
+[expose]
+enabled = true
+tables = false
+databases = false
+schemas = false
\ No newline at end of file