From 2ae5b5ec193cdb30698ad589ad65f0e3ee7842e5 Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Tue, 1 Jun 2021 03:09:16 +1000 Subject: [PATCH 01/23] Add initial API function call permission toggles to setting page This is just an initial HTML design to see how it looks visually, with no working code doing anything yet. Pretty sure these function call toggles will need to be moved to a separate permissions page, otherwise it'll be extremely unwieldy for anyone with multiple API keys. --- webui/templates/preferences.html | 190 +++++++++++++++++++++++++++++-- 1 file changed, 183 insertions(+), 7 deletions(-) diff --git a/webui/templates/preferences.html b/webui/templates/preferences.html index c524d3aba..7ca2b7780 100644 --- a/webui/templates/preferences.html +++ b/webui/templates/preferences.html @@ -6,10 +6,10 @@ [[ template "header" . ]]
-
+
 
-
+

Settings

Used when uploading databases

@@ -47,16 +47,168 @@

A Key Generation date + Databases + Allowed function calls - {{ row.key }} +

{{ row.key }}

+
+ +
+ {{ row.date_created | date : 'medium' }} + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Branches() +
+ + +
+
Metadata() +
+ + +
+
Columns() +
+ + +
+
Query() +
+ + +
+
Commits() +
+ + +
+
Releases() +
+ + +
+
Databases() +
+ + +
+
Tables() +
+ + +
+
Delete() +
+ + +
+
Tags() +
+ + +
+
Diff() +
+ + +
+
Upload() +
+ + +
+
Download() +
+ + +
+
Views() +
+ + +
+
Indexes() +
+ + +
+
Webpage() +
+ + +
+
+ - You don't have any API keys yet + You don't have any API keys yet - +
@@ -71,7 +223,7 @@

 {{ statusMessage }}

-
+
 
@@ -83,6 +235,30 @@

 {{ statusMessage }}

// API Keys $scope.apiKeys = [[ .APIKeys ]]; + // Set initial permissions to true for the API keys + $scope.radioBranches = "true"; + $scope.radioColumns = "true"; + $scope.radioCommits = "true"; + $scope.radioDatabases = "true"; + $scope.radioDelete = "true"; + $scope.radioDiff = "true"; + $scope.radioDownload = "true"; + $scope.radioIndexes = "true"; + $scope.radioMetadata = "true"; + $scope.radioQuery = "true"; + $scope.radioReleases = "true"; + $scope.radioTables = "true"; + $scope.radioTags = "true"; + $scope.radioUpload = "true"; + $scope.radioViews = "true"; + $scope.radioWebpage = "true"; + + // Placeholder values + // TODO: ... + $scope.dbList = ["SomeDatabase", "AnotherDatabase", "MyDatabase"]; + $scope.selectedDB = "MyDatabase"; + + // If the supplied display name is blank, we set a placeholder value instead $scope.FullName = ""; $scope.NamePlaceholder = ""; @@ -139,4 +315,4 @@

 {{ statusMessage }}

-[[ end ]] \ No newline at end of file +[[ end ]] From f56b156dd3d4d05333b52cd8b2cc84d038f4fe6e Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Wed, 2 Jun 2021 03:02:46 +1000 Subject: [PATCH 02/23] Initial code to send api permission button toggles to the server This is just the initial browser webUI to backend data sending. No validation, nor storage of the values, in the backend yet. --- webui/main.go | 88 ++++++++++++++++++++++++++++++ webui/templates/preferences.html | 92 +++++++++++++++++++++----------- 2 files changed, 148 insertions(+), 32 deletions(-) diff --git a/webui/main.go b/webui/main.go index 7e72a0eb7..d1d6bdff6 100644 --- a/webui/main.go +++ b/webui/main.go @@ -38,6 +38,93 @@ var ( store *gsm.MemcacheStore ) +// apiPermissionsUpdateHandler handles updating API permissions as requested from the User's Settings page +func apiPermissionsUpdateHandler(w http.ResponseWriter, r *http.Request) { + // Retrieve session data (if any) + loggedInUser, validSession, err := checkLogin(r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Ensure we have a valid logged in user + if validSession != true { + w.WriteHeader(http.StatusUnauthorized) + return + } + + // FIXME: General dev/debug output + d := fmt.Sprintf("Setting received for user: %v", loggedInUser) + fmt.Println(d) + + // TODO + // * Validate the input + // * ksuid.Parse() seems like it'll be useful for API keys + // * Should this function also receive the change in selected database for the api key? probably yes + // * Save the new values to a database table + // * So, figure out an appropriate structure. Maybe: + // * apikey_id or similar name : maybe bigint? + // * database_id : whatever we use for database ids + // * permissions : jsonb structure with name/value pairs for the API permissions + + // Retrieve API key + a := r.PostFormValue("apikey") + apiKey, err := url.QueryUnescape(a) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, err.Error()) + return + } + fmt.Printf("API key: %v\n", apiKey) + + // Retrieve permission name + // TODO: Validation for the permission name could just be a big case statement + p := r.PostFormValue("perm") + perm, err := url.QueryUnescape(p) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, err.Error()) + return + } + fmt.Printf("Permission name: %v\n", perm) + + // Retrieve new permission value + // TODO: Validation + v := r.PostFormValue("value") + value, err := url.QueryUnescape(v) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, err.Error()) + return + } + fmt.Printf("New value: %v\n", value) + + // TODO: From the docs, it seems like we could use ksuid.Parse() as a reasonable validator for provided api keys. + // But, we definitely need to test with some wrong values to see what happens (eg empty string, null, words, etc) + // Whatever we use, we should create a validator function for api keys using it, and apply that to our api end point + // as well. It doesn't validate them as well as I'd like. :/ + _, err = ksuid.Parse(apiKey) + if err != nil { + log.Printf("Validation failed for API key: '%s'- %s", apiKey, err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, err.Error()) + return + } + + // TODO: Return some kind of success flag to the caller + //d := com.APIKey{ + // Key: key, + // DateCreated: creationTime, + //} + data, err := json.Marshal(d) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Fprint(w, string(data)) +} + // apiKeyGenHandler generates a new API key, stores it in the PG database, and returns the details to the caller func apiKeyGenHandler(w http.ResponseWriter, r *http.Request) { // Retrieve session data (if any) @@ -3097,6 +3184,7 @@ func main() { http.Handle("/vis/", gz.GzipHandler(logReq(visualisePage))) http.Handle("/watchers/", gz.GzipHandler(logReq(watchersPage))) http.Handle("/x/apikeygen", gz.GzipHandler(logReq(apiKeyGenHandler))) + http.Handle("/x/apipermupdate", gz.GzipHandler(logReq(apiPermissionsUpdateHandler))) http.Handle("/x/branchnames", gz.GzipHandler(logReq(branchNamesHandler))) http.Handle("/x/callback", gz.GzipHandler(logReq(auth0CallbackHandler))) http.Handle("/x/checkname", gz.GzipHandler(logReq(checkNameHandler))) diff --git a/webui/templates/preferences.html b/webui/templates/preferences.html index 7ca2b7780..2f6d2393e 100644 --- a/webui/templates/preferences.html +++ b/webui/templates/preferences.html @@ -77,15 +77,15 @@

A Branches()
- - + +
Metadata()
- - + +
@@ -93,15 +93,15 @@

A Columns()
- - + +
Query()
- - + +
@@ -109,15 +109,15 @@

A Commits()
- - + +
Releases()
- - + +
@@ -125,15 +125,15 @@

A Databases()
- - + +
Tables()
- - + +
@@ -141,15 +141,15 @@

A Delete()
- - + +
Tags()
- - + +
@@ -157,15 +157,15 @@

A Diff()
- - + +
Upload()
- - + +
@@ -173,15 +173,15 @@

A Download()
- - + +
Views()
- - + +
@@ -189,15 +189,15 @@

A Indexes()
- - + +
Webpage()
- - + +
@@ -286,6 +286,34 @@

 {{ statusMessage }}

lock.show(); }; + // Call the server to update the api permission + $scope.apiPermUpdate = function(apikey, perm, value) { + console.log("apikey: " + apikey); + console.log("perm: " + perm); + console.log("value: " + value); + + $http({ + method: "POST", + url: "/x/apipermupdate/", + data: $httpParamSerializerJQLike({ + "apikey": encodeURIComponent(apikey), + "perm": encodeURIComponent(perm), + "value": encodeURIComponent(value) + }), + headers: { "Content-Type": "application/x-www-form-urlencoded" } + }).then(function (response) { + console.log(response.data); + + $scope.statusMessageColour = "green"; + $scope.statusMessage = "Server message: " + response.data; + }, function failure(response) { + console.log(response.data); + + $scope.statusMessageColour = "red"; + $scope.statusMessage = "Server message: " + response.data; + }); + }; + // Generate a new API key $scope.statusMessage = ""; $scope.statusMessageColour = "red"; From d482e348e3c84272c30556b6709b5b20318df48a Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Thu, 3 Jun 2021 02:49:19 +1000 Subject: [PATCH 03/23] Save point. Start working out the right PG table structure Looks like my dev database is out of date and needs updating too ;) --- common/postgresql.go | 9 ++++ common/types.go | 21 ++++++++ database/api_permissions.sql | 47 +++++++++++++++++ webui/main.go | 99 +++++++++++++++++++++++++++--------- 4 files changed, 153 insertions(+), 23 deletions(-) create mode 100644 database/api_permissions.sql diff --git a/common/postgresql.go b/common/postgresql.go index 41fbb66ab..e3db8b372 100644 --- a/common/postgresql.go +++ b/common/postgresql.go @@ -131,6 +131,15 @@ func APIKeySave(key, loggedInUser string, dateCreated time.Time) error { return nil } +// APIPermStore saves a new API key to the PostgreSQL database +func APIPermStore(loggedInUser, apiKey string, dbName string, perm APIPermission, value bool) error { + + // TODO: Everything for this function ;) + fmt.Printf("User: %v, api key: %v, database: %v, perm: %v, value: %v\n", loggedInUser, apiKey, dbName, perm, value) + + return nil +} + // CheckDBExists checks if a database exists. It does NOT perform any permission checks. // If an error occurred, the true/false value should be ignored, as only the error value is valid func CheckDBExists(dbOwner, dbFolder, dbName string) (bool, error) { diff --git a/common/types.go b/common/types.go index 589778cdd..165126ac7 100644 --- a/common/types.go +++ b/common/types.go @@ -241,6 +241,27 @@ type APIKey struct { DateCreated time.Time `json:"date_created"` } +type APIPermission uint + +const ( + APIPERM_BRANCHES = 0 // These are not iota, as it would be seriously bad for these numbers to change + APIPERM_COLUMNS = 1 + APIPERM_COMMITS = 2 + APIPERM_DATABASES = 3 + APIPERM_DELETE = 4 + APIPERM_DIFF = 5 + APIPERM_DOWNLOAD = 6 + APIPERM_INDEXES = 7 + APIPERM_METADATA = 8 + APIPERM_QUERY = 9 + APIPERM_RELEASES = 10 + APIPERM_TABLES = 11 + APIPERM_TAGS = 12 + APIPERM_UPLOAD = 13 + APIPERM_VIEWS = 14 + APIPERM_WEBPAGE = 15 +) + type Auth0Set struct { CallbackURL string ClientID string diff --git a/database/api_permissions.sql b/database/api_permissions.sql new file mode 100644 index 000000000..4875cb60a --- /dev/null +++ b/database/api_permissions.sql @@ -0,0 +1,47 @@ + +-- *** api_keys tables needed a unique key added to the key_id column *** + +-- +-- Name: api_keys api_keys_key_id; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_keys + ADD CONSTRAINT api_keys_key_id UNIQUE (key_id); + +-- *** Hang on, this ^^^ seems like it's just a constraint, but no index? Look into it when less sleepy. ;) + + +-- *** New table for holding API key permissions *** + +-- +-- Name: api_permissions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.api_permissions ( + key_id bigint, + user_id bigint, + db_id bigint, + permissions jsonb +); + +-- +-- Name: api_permissions api_permissions_api_keys_key_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_permissions + ADD CONSTRAINT api_permissions_api_keys_key_id_fk FOREIGN KEY (key_id) REFERENCES public.api_keys(key_id) ON UPDATE CASCADE ON DELETE CASCADE; + +-- +-- Name: api_permissions api_permissions_sqlite_databases_db_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_permissions + ADD CONSTRAINT api_permissions_sqlite_databases_db_id_fk FOREIGN KEY (db_id) REFERENCES public.sqlite_databases(db_id) ON UPDATE CASCADE ON DELETE CASCADE; + +-- +-- Name: api_permissions api_permissions_users_user_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_permissions + ADD CONSTRAINT api_permissions_users_user_id_fk FOREIGN KEY (user_id) REFERENCES public.users(user_id) ON UPDATE CASCADE ON DELETE CASCADE; + diff --git a/webui/main.go b/webui/main.go index d1d6bdff6..92c909af2 100644 --- a/webui/main.go +++ b/webui/main.go @@ -53,13 +53,10 @@ func apiPermissionsUpdateHandler(w http.ResponseWriter, r *http.Request) { return } - // FIXME: General dev/debug output - d := fmt.Sprintf("Setting received for user: %v", loggedInUser) - fmt.Println(d) - // TODO // * Validate the input // * ksuid.Parse() seems like it'll be useful for API keys + // * Better turn it into an API key validation function, and add it to the common library // * Should this function also receive the change in selected database for the api key? probably yes // * Save the new values to a database table // * So, figure out an appropriate structure. Maybe: @@ -75,39 +72,95 @@ func apiPermissionsUpdateHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, err.Error()) return } - fmt.Printf("API key: %v\n", apiKey) + + // Validate the API key + _, err = ksuid.Parse(apiKey) + if err != nil { + log.Printf("Validation failed for API key: '%s'- %s", apiKey, err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, err.Error()) + return + } // Retrieve permission name - // TODO: Validation for the permission name could just be a big case statement p := r.PostFormValue("perm") - perm, err := url.QueryUnescape(p) + p2, err := url.QueryUnescape(p) if err != nil { - w.WriteHeader(http.StatusInternalServerError) + w.WriteHeader(http.StatusBadRequest) fmt.Fprint(w, err.Error()) return } - fmt.Printf("Permission name: %v\n", perm) + var perm com.APIPermission + switch strings.ToLower(p2) { + case "branches": + perm = com.APIPERM_BRANCHES + case "columns": + perm = com.APIPERM_COLUMNS + case "commits": + perm = com.APIPERM_COMMITS + case "databases": + perm = com.APIPERM_DATABASES + case "delete": + perm = com.APIPERM_DELETE + case "diff": + perm = com.APIPERM_DIFF + case "download": + perm = com.APIPERM_DOWNLOAD + case "indexes": + perm = com.APIPERM_INDEXES + case "metadata": + perm = com.APIPERM_METADATA + case "query": + perm = com.APIPERM_QUERY + case "releases": + perm = com.APIPERM_RELEASES + case "tables": + perm = com.APIPERM_TABLES + case "tags": + perm = com.APIPERM_TAGS + case "upload": + perm = com.APIPERM_UPLOAD + case "views": + perm = com.APIPERM_VIEWS + case "webpage": + perm = com.APIPERM_WEBPAGE + default: + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, "Unknown permission name") + return + } - // Retrieve new permission value - // TODO: Validation + // Retrieve updated permission value v := r.PostFormValue("value") - value, err := url.QueryUnescape(v) + v2, err := url.QueryUnescape(v) if err != nil { - w.WriteHeader(http.StatusInternalServerError) + w.WriteHeader(http.StatusBadRequest) fmt.Fprint(w, err.Error()) return } - fmt.Printf("New value: %v\n", value) - // TODO: From the docs, it seems like we could use ksuid.Parse() as a reasonable validator for provided api keys. - // But, we definitely need to test with some wrong values to see what happens (eg empty string, null, words, etc) - // Whatever we use, we should create a validator function for api keys using it, and apply that to our api end point - // as well. It doesn't validate them as well as I'd like. :/ - _, err = ksuid.Parse(apiKey) - if err != nil { - log.Printf("Validation failed for API key: '%s'- %s", apiKey, err) + // Validate the provided value + value := false + switch strings.ToLower(v2) { + case "true": + value = true + case "false": + value = false + default: + // TODO: If we're processing a database-name update (eg to change the db an API key is for), then we'll need to ignore a missing value w.WriteHeader(http.StatusBadRequest) - fmt.Fprint(w, err.Error()) + fmt.Fprint(w, "Invalid true/false value") + return + } + + // TODO: Make sure the given API key has been issued to this user + + + // TODO: Store the updated value in the database + err = com.APIPermStore(loggedInUser, apiKey, "mydatabase", perm, value) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -116,7 +169,7 @@ func apiPermissionsUpdateHandler(w http.ResponseWriter, r *http.Request) { // Key: key, // DateCreated: creationTime, //} - data, err := json.Marshal(d) + data, err := json.Marshal("New value saved!") if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) From 046e861bc62998132667764f1e852370808bb965 Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Sun, 6 Jun 2021 05:38:45 +1000 Subject: [PATCH 04/23] Early stage code for saving the permissions in the database --- common/postgresql.go | 86 ++++++++++++++++++++++++++++++++++++++++++++ webui/main.go | 2 +- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/common/postgresql.go b/common/postgresql.go index e3db8b372..d40a998d7 100644 --- a/common/postgresql.go +++ b/common/postgresql.go @@ -137,6 +137,92 @@ func APIPermStore(loggedInUser, apiKey string, dbName string, perm APIPermission // TODO: Everything for this function ;) fmt.Printf("User: %v, api key: %v, database: %v, perm: %v, value: %v\n", loggedInUser, apiKey, dbName, perm, value) + // TODO: Check if the database name "all" was specified + + // Data structure for holding the API permission values + permData := make(map[APIPermission]bool) + + // Fetch the existing permission data for the api key (if set) + dbQuery := ` + WITH ids AS ( + SELECT user_id, db_id + FROM sqlite_databases + WHERE user_id = ( + SELECT user_id + FROM users + WHERE user_name = $1) + AND db_name = $2 + ), key_info AS ( + SELECT key_id + FROM api_keys, ids + WHERE api_keys.user_id = ids.user_id + AND key = $3 + ) + SELECT permissions + FROM api_permissions, ids, key_info + WHERE api_permissions.user_id = ids.user_id + AND api_permissions.db_id = ids.db_id + AND api_permissions.key_id = key_info.key_id` + err := pdb.QueryRow(dbQuery, loggedInUser, dbName, apiKey).Scan(&permData) + if err != nil { + log.Printf("Fetching API key permissions failed: %v\n", err) + return err + } + + // TODO: Detect if no existing row data was retrieved + // * In that case, it's an API key generated before permissions were available. So, we default + // to "all databases" and "all permissions are turned on" + if len(permData) == 0 { + // TODO: Figure out the "all databases" bit too + // * I guess "all databases" would mean storing null for the field? + // * Alternatively, we might need to store the database name as a string? + permData[APIPERM_BRANCHES] = true + permData[APIPERM_COLUMNS] = true + permData[APIPERM_COMMITS] = true + permData[APIPERM_DATABASES] = true + permData[APIPERM_DELETE] = true + permData[APIPERM_DIFF] = true + permData[APIPERM_DOWNLOAD] = true + permData[APIPERM_INDEXES] = true + permData[APIPERM_METADATA] = true + permData[APIPERM_QUERY] = true + permData[APIPERM_RELEASES] = true + permData[APIPERM_TABLES] = true + permData[APIPERM_TAGS] = true + permData[APIPERM_UPLOAD] = true + permData[APIPERM_VIEWS] = true + permData[APIPERM_WEBPAGE] = true + } + + // Incorporate the updated permission data from the user + permData[perm] = value + + // Store the updated permissions + dbQuery = ` + WITH ids AS ( + SELECT user_id, db_id + FROM sqlite_databases + WHERE user_id = ( + SELECT user_id + FROM users + WHERE user_name = $1) + AND db_name = $2 + ), key_info AS ( + SELECT key_id + FROM api_keys, ids + WHERE api_keys.user_id = ids.user_id + AND key = $3 + ) + INSERT INTO api_permissions (key_id, user_id, db_id, permissions) + SELECT (SELECT key_id FROM key_info), (SELECT user_id FROM ids), (SELECT db_id FROM ids), $4` + commandTag, err := pdb.Exec(dbQuery, loggedInUser, dbName, apiKey, permData) + if err != nil { + log.Printf("Updating permissions for API key '%v' failed: %v\n", apiKey, err) + return err + } + if numRows := commandTag.RowsAffected(); numRows != 1 { + log.Printf("Wrong number of rows (%d) affected when updating API key: %v permissions\n", numRows, apiKey) + } return nil } diff --git a/webui/main.go b/webui/main.go index 92c909af2..8ab252d2f 100644 --- a/webui/main.go +++ b/webui/main.go @@ -157,7 +157,7 @@ func apiPermissionsUpdateHandler(w http.ResponseWriter, r *http.Request) { // TODO: Store the updated value in the database - err = com.APIPermStore(loggedInUser, apiKey, "mydatabase", perm, value) + err = com.APIPermStore(loggedInUser, apiKey, "tempdb1.sqlite", perm, value) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) From 0f3a23333add6a11db6f4b5d489d9745220eb2ce Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Sun, 6 Jun 2021 16:56:12 +1000 Subject: [PATCH 05/23] Save point. Working function to save API permissions in the database --- common/postgresql.go | 88 ++++++++++++++++++++++---------------------- webui/main.go | 11 ++++-- 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/common/postgresql.go b/common/postgresql.go index d40a998d7..597c3108e 100644 --- a/common/postgresql.go +++ b/common/postgresql.go @@ -132,50 +132,38 @@ func APIKeySave(key, loggedInUser string, dateCreated time.Time) error { } // APIPermStore saves a new API key to the PostgreSQL database -func APIPermStore(loggedInUser, apiKey string, dbName string, perm APIPermission, value bool) error { - - // TODO: Everything for this function ;) - fmt.Printf("User: %v, api key: %v, database: %v, perm: %v, value: %v\n", loggedInUser, apiKey, dbName, perm, value) - - // TODO: Check if the database name "all" was specified - +func APIPermStore(loggedInUser, apiKey, dbName string, allDB bool, perm APIPermission, value bool) error { // Data structure for holding the API permission values permData := make(map[APIPermission]bool) - // Fetch the existing permission data for the api key (if set) + // Retrieve the existing API key permissions dbQuery := ` - WITH ids AS ( - SELECT user_id, db_id - FROM sqlite_databases - WHERE user_id = ( - SELECT user_id - FROM users - WHERE user_name = $1) - AND db_name = $2 + WITH uid AS ( + SELECT user_id + FROM users + WHERE user_name = $1 ), key_info AS ( SELECT key_id - FROM api_keys, ids - WHERE api_keys.user_id = ids.user_id - AND key = $3 + FROM api_keys, uid + WHERE api_keys.user_id = uid.user_id + AND key = $2 ) SELECT permissions - FROM api_permissions, ids, key_info - WHERE api_permissions.user_id = ids.user_id - AND api_permissions.db_id = ids.db_id + FROM api_permissions, uid, key_info + WHERE api_permissions.user_id = uid.user_id AND api_permissions.key_id = key_info.key_id` - err := pdb.QueryRow(dbQuery, loggedInUser, dbName, apiKey).Scan(&permData) + err := pdb.QueryRow(dbQuery, loggedInUser, apiKey).Scan(&permData) if err != nil { - log.Printf("Fetching API key permissions failed: %v\n", err) - return err + // Returning no rows is ok for this call + if err != pgx.ErrNoRows { + log.Printf("Fetching API key permissions failed: %v\n", err) + return err + } } - // TODO: Detect if no existing row data was retrieved - // * In that case, it's an API key generated before permissions were available. So, we default - // to "all databases" and "all permissions are turned on" + // If there isn't any permission data for the API key, it means the key was generated before permissions were + // available. So, we default to "all databases" and "all permissions are turned on" if len(permData) == 0 { - // TODO: Figure out the "all databases" bit too - // * I guess "all databases" would mean storing null for the field? - // * Alternatively, we might need to store the database name as a string? permData[APIPERM_BRANCHES] = true permData[APIPERM_COLUMNS] = true permData[APIPERM_COMMITS] = true @@ -197,25 +185,37 @@ func APIPermStore(loggedInUser, apiKey string, dbName string, perm APIPermission // Incorporate the updated permission data from the user permData[perm] = value + // If this api key applies to "all databases", then we store null in its db_id field + var dbID pgx.NullInt64 + if allDB != true { + var d int + d, err = databaseID(loggedInUser, "/", dbName) + if err != nil { + log.Printf("Retrieving database ID failed: %v\n", err) + return err + } + dbID.Int64 = int64(d) + dbID.Valid = true + } + // Store the updated permissions dbQuery = ` - WITH ids AS ( - SELECT user_id, db_id - FROM sqlite_databases - WHERE user_id = ( - SELECT user_id - FROM users - WHERE user_name = $1) - AND db_name = $2 + WITH uid AS ( + SELECT user_id + FROM users + WHERE user_name = $1 ), key_info AS ( SELECT key_id - FROM api_keys, ids - WHERE api_keys.user_id = ids.user_id - AND key = $3 + FROM api_keys, uid + WHERE api_keys.user_id = uid.user_id + AND key = $2 ) INSERT INTO api_permissions (key_id, user_id, db_id, permissions) - SELECT (SELECT key_id FROM key_info), (SELECT user_id FROM ids), (SELECT db_id FROM ids), $4` - commandTag, err := pdb.Exec(dbQuery, loggedInUser, dbName, apiKey, permData) + SELECT (SELECT key_id FROM key_info), (SELECT user_id FROM uid), $3, $4 + ON CONFLICT (user_id, key_id) + DO UPDATE + SET db_id = $3, permissions = $4` + commandTag, err := pdb.Exec(dbQuery, loggedInUser, apiKey, dbID, permData) if err != nil { log.Printf("Updating permissions for API key '%v' failed: %v\n", apiKey, err) return err diff --git a/webui/main.go b/webui/main.go index 8ab252d2f..eafed645d 100644 --- a/webui/main.go +++ b/webui/main.go @@ -82,6 +82,10 @@ func apiPermissionsUpdateHandler(w http.ResponseWriter, r *http.Request) { return } + // TODO: Retrieve the database name + + // TODO: Add the "All databases" value to the front end and here + // Retrieve permission name p := r.PostFormValue("perm") p2, err := url.QueryUnescape(p) @@ -139,7 +143,7 @@ func apiPermissionsUpdateHandler(w http.ResponseWriter, r *http.Request) { return } - // Validate the provided value + // Validate the provided permission value value := false switch strings.ToLower(v2) { case "true": @@ -155,9 +159,8 @@ func apiPermissionsUpdateHandler(w http.ResponseWriter, r *http.Request) { // TODO: Make sure the given API key has been issued to this user - - // TODO: Store the updated value in the database - err = com.APIPermStore(loggedInUser, apiKey, "tempdb1.sqlite", perm, value) + // Store the updated permissions in the database + err = com.APIPermStore(loggedInUser, apiKey, "tempdb1.sqlite", true, perm, value) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) From 9871e8d70c08024f290cc55bfc04bd962344f03d Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Sun, 6 Jun 2021 22:01:50 +1000 Subject: [PATCH 06/23] Save point. Initial working code to save api key database change to PG database Still a bunch of stuff to do, but it's taking shape --- common/postgresql.go | 120 +++++++++++------- webui/main.go | 204 +++++++++++++++++++++---------- webui/pages.go | 4 + webui/templates/preferences.html | 156 ++++++++++++++--------- 4 files changed, 320 insertions(+), 164 deletions(-) diff --git a/common/postgresql.go b/common/postgresql.go index 597c3108e..cb9b88026 100644 --- a/common/postgresql.go +++ b/common/postgresql.go @@ -96,43 +96,53 @@ func AddUser(auth0ID, userName, password, email, displayName, avatarURL string) return nil } -// APIKeySave saves a new API key to the PostgreSQL database -func APIKeySave(key, loggedInUser string, dateCreated time.Time) error { - // Make sure the API key isn't already in the database - dbQuery := ` - SELECT count(key) - FROM api_keys - WHERE key = $1` - var keyCount int - err := pdb.QueryRow(dbQuery, key).Scan(&keyCount) - if err != nil { - log.Printf("Checking if an API key exists failed: %v\n", err) - return err - } - if keyCount != 0 { - // API key is already in our system - log.Printf("Duplicate API key (%s) generated for user '%s'\n", key, loggedInUser) - return fmt.Errorf("API generator created duplicate key. Try again, just in case...") +// APIKeyDBSave changes which database an API key applies to +func APIKeyDBSave(loggedInUser, apiKey, dbName string, allDB bool) error { + var dbID pgx.NullInt64 + var err error + + // If this api key applies to "all databases", then we store null in its db_id field + if allDB != true { + var d int + d, err = databaseID(loggedInUser, "/", dbName) + if err != nil { + log.Printf("Retrieving database ID failed: %v\n", err) + return err + } + dbID.Int64 = int64(d) + dbID.Valid = true } - // Add the new API key to the database - dbQuery = ` - INSERT INTO api_keys (user_id, key, date_created) - SELECT (SELECT user_id FROM users WHERE lower(user_name) = lower($1)), $2, $3` - commandTag, err := pdb.Exec(dbQuery, loggedInUser, key, dateCreated) + // Store the updated database + dbQuery := ` + WITH uid AS ( + SELECT user_id + FROM users + WHERE user_name = $1 + ), key_info AS ( + SELECT key_id + FROM api_keys, uid + WHERE api_keys.user_id = uid.user_id + AND key = $2 + ) + INSERT INTO api_permissions (key_id, user_id, db_id) + SELECT (SELECT key_id FROM key_info), (SELECT user_id FROM uid), $3 + ON CONFLICT (user_id, key_id) + DO UPDATE + SET db_id = $3` + commandTag, err := pdb.Exec(dbQuery, loggedInUser, apiKey, dbID) if err != nil { - log.Printf("Adding API key to database failed: %v\n", err) + log.Printf("Updating database for API key '%v' failed: %v\n", apiKey, err) return err } if numRows := commandTag.RowsAffected(); numRows != 1 { - log.Printf("Wrong number of rows (%d) affected when adding API key: %v, username: %v\n", numRows, key, - loggedInUser) + log.Printf("Wrong number of rows (%d) affected when updating API key '%v' database \n", numRows, apiKey) } return nil } -// APIPermStore saves a new API key to the PostgreSQL database -func APIPermStore(loggedInUser, apiKey, dbName string, allDB bool, perm APIPermission, value bool) error { +// APIKeyPermSave updates the permissions for an API key +func APIKeyPermSave(loggedInUser, apiKey string, perm APIPermission, value bool) error { // Data structure for holding the API permission values permData := make(map[APIPermission]bool) @@ -185,19 +195,6 @@ func APIPermStore(loggedInUser, apiKey, dbName string, allDB bool, perm APIPermi // Incorporate the updated permission data from the user permData[perm] = value - // If this api key applies to "all databases", then we store null in its db_id field - var dbID pgx.NullInt64 - if allDB != true { - var d int - d, err = databaseID(loggedInUser, "/", dbName) - if err != nil { - log.Printf("Retrieving database ID failed: %v\n", err) - return err - } - dbID.Int64 = int64(d) - dbID.Valid = true - } - // Store the updated permissions dbQuery = ` WITH uid AS ( @@ -210,12 +207,12 @@ func APIPermStore(loggedInUser, apiKey, dbName string, allDB bool, perm APIPermi WHERE api_keys.user_id = uid.user_id AND key = $2 ) - INSERT INTO api_permissions (key_id, user_id, db_id, permissions) - SELECT (SELECT key_id FROM key_info), (SELECT user_id FROM uid), $3, $4 + INSERT INTO api_permissions (key_id, user_id, permissions) + SELECT (SELECT key_id FROM key_info), (SELECT user_id FROM uid), $3 ON CONFLICT (user_id, key_id) DO UPDATE - SET db_id = $3, permissions = $4` - commandTag, err := pdb.Exec(dbQuery, loggedInUser, apiKey, dbID, permData) + SET permissions = $3` + commandTag, err := pdb.Exec(dbQuery, loggedInUser, apiKey, permData) if err != nil { log.Printf("Updating permissions for API key '%v' failed: %v\n", apiKey, err) return err @@ -226,6 +223,41 @@ func APIPermStore(loggedInUser, apiKey, dbName string, allDB bool, perm APIPermi return nil } +// APIKeySave saves a new API key to the PostgreSQL database +func APIKeySave(key, loggedInUser string, dateCreated time.Time) error { + // Make sure the API key isn't already in the database + dbQuery := ` + SELECT count(key) + FROM api_keys + WHERE key = $1` + var keyCount int + err := pdb.QueryRow(dbQuery, key).Scan(&keyCount) + if err != nil { + log.Printf("Checking if an API key exists failed: %v\n", err) + return err + } + if keyCount != 0 { + // API key is already in our system + log.Printf("Duplicate API key (%s) generated for user '%s'\n", key, loggedInUser) + return fmt.Errorf("API generator created duplicate key. Try again, just in case...") + } + + // Add the new API key to the database + dbQuery = ` + INSERT INTO api_keys (user_id, key, date_created) + SELECT (SELECT user_id FROM users WHERE lower(user_name) = lower($1)), $2, $3` + commandTag, err := pdb.Exec(dbQuery, loggedInUser, key, dateCreated) + if err != nil { + log.Printf("Adding API key to database failed: %v\n", err) + return err + } + if numRows := commandTag.RowsAffected(); numRows != 1 { + log.Printf("Wrong number of rows (%d) affected when adding API key: %v, username: %v\n", numRows, key, + loggedInUser) + } + return nil +} + // CheckDBExists checks if a database exists. It does NOT perform any permission checks. // If an error occurred, the true/false value should be ignored, as only the error value is valid func CheckDBExists(dbOwner, dbFolder, dbName string) (bool, error) { diff --git a/webui/main.go b/webui/main.go index eafed645d..017bbecca 100644 --- a/webui/main.go +++ b/webui/main.go @@ -38,8 +38,140 @@ var ( store *gsm.MemcacheStore ) -// apiPermissionsUpdateHandler handles updating API permissions as requested from the User's Settings page -func apiPermissionsUpdateHandler(w http.ResponseWriter, r *http.Request) { +// apiKeyDbUpdateHandler handles updating the API key database as requested from the User's Settings page +func apiKeyDbUpdateHandler(w http.ResponseWriter, r *http.Request) { + // Retrieve session data (if any) + loggedInUser, validSession, err := checkLogin(r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Ensure we have a valid logged in user + if validSession != true { + w.WriteHeader(http.StatusUnauthorized) + return + } + + // Retrieve API key + a := r.PostFormValue("apikey") + apiKey, err := url.QueryUnescape(a) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, err.Error()) + return + } + + // Validate the API key + _, err = ksuid.Parse(apiKey) + if err != nil { + log.Printf("Validation failed for API key: '%s'- %s", apiKey, err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, err.Error()) + return + } + + // Retrieve the database name + d := r.PostFormValue("dbname") + dbName, err := url.QueryUnescape(d) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, err.Error()) + return + } + + // TODO: Validate the database name + + + // Check if the "all databases" variable was set + allDBs := false + z := r.PostFormValue("alldbs") + z2, err := url.QueryUnescape(z) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, err.Error()) + return + } + switch z2 { + case "true": + allDBs = true + case "false": + // Nothing to do here, as the value starts out initialised to false + default: + // Unknown value passed + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, err.Error()) + return + } + + // TODO: Make sure the given API key has been issued to this user + + + // Store the updated database for the API key + err = com.APIKeyDBSave(loggedInUser, apiKey, dbName, allDBs) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // TODO: Return some kind of success flag to the caller + data, err := json.Marshal("Database updated!") + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Fprint(w, string(data)) + +} + +// apiKeyGenHandler generates a new API key, stores it in the PG database, and returns the details to the caller +func apiKeyGenHandler(w http.ResponseWriter, r *http.Request) { + // Retrieve session data (if any) + loggedInUser, validSession, err := checkLogin(r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Ensure we have a valid logged in user + if validSession != true { + w.WriteHeader(http.StatusUnauthorized) + return + } + + // Generate new API key + creationTime := time.Now() + keyRaw, err := ksuid.NewRandom() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + key := keyRaw.String() + + // Save the API key in PG database + err = com.APIKeySave(key, loggedInUser, creationTime) + + // Log the key creation + log.Printf("New API key created for user '%s', key: '%s'\n", loggedInUser, key) + + // Return the API key to the caller + d := com.APIKey{ + Key: key, + DateCreated: creationTime, + } + data, err := json.Marshal(d) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Fprint(w, string(data)) +} + +// apiKeyPermsUpdateHandler handles updating API permissions as requested from the User's Settings page +func apiKeyPermsUpdateHandler(w http.ResponseWriter, r *http.Request) { // Retrieve session data (if any) loggedInUser, validSession, err := checkLogin(r) if err != nil { @@ -57,12 +189,6 @@ func apiPermissionsUpdateHandler(w http.ResponseWriter, r *http.Request) { // * Validate the input // * ksuid.Parse() seems like it'll be useful for API keys // * Better turn it into an API key validation function, and add it to the common library - // * Should this function also receive the change in selected database for the api key? probably yes - // * Save the new values to a database table - // * So, figure out an appropriate structure. Maybe: - // * apikey_id or similar name : maybe bigint? - // * database_id : whatever we use for database ids - // * permissions : jsonb structure with name/value pairs for the API permissions // Retrieve API key a := r.PostFormValue("apikey") @@ -82,10 +208,6 @@ func apiPermissionsUpdateHandler(w http.ResponseWriter, r *http.Request) { return } - // TODO: Retrieve the database name - - // TODO: Add the "All databases" value to the front end and here - // Retrieve permission name p := r.PostFormValue("perm") p2, err := url.QueryUnescape(p) @@ -151,7 +273,6 @@ func apiPermissionsUpdateHandler(w http.ResponseWriter, r *http.Request) { case "false": value = false default: - // TODO: If we're processing a database-name update (eg to change the db an API key is for), then we'll need to ignore a missing value w.WriteHeader(http.StatusBadRequest) fmt.Fprint(w, "Invalid true/false value") return @@ -160,63 +281,15 @@ func apiPermissionsUpdateHandler(w http.ResponseWriter, r *http.Request) { // TODO: Make sure the given API key has been issued to this user // Store the updated permissions in the database - err = com.APIPermStore(loggedInUser, apiKey, "tempdb1.sqlite", true, perm, value) + err = com.APIKeyPermSave(loggedInUser, apiKey, perm, value) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - // TODO: Return some kind of success flag to the caller - //d := com.APIKey{ - // Key: key, - // DateCreated: creationTime, - //} - data, err := json.Marshal("New value saved!") - if err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - fmt.Fprint(w, string(data)) -} - -// apiKeyGenHandler generates a new API key, stores it in the PG database, and returns the details to the caller -func apiKeyGenHandler(w http.ResponseWriter, r *http.Request) { - // Retrieve session data (if any) - loggedInUser, validSession, err := checkLogin(r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Ensure we have a valid logged in user - if validSession != true { - w.WriteHeader(http.StatusUnauthorized) - return - } - - // Generate new API key - creationTime := time.Now() - keyRaw, err := ksuid.NewRandom() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - key := keyRaw.String() - - // Save the API key in PG database - err = com.APIKeySave(key, loggedInUser, creationTime) - - // Log the key creation - log.Printf("New API key created for user '%s', key: '%s'\n", loggedInUser, key) - - // Return the API key to the caller - d := com.APIKey{ - Key: key, - DateCreated: creationTime, - } - data, err := json.Marshal(d) + // TODO: Return success info to the caller + data, err := json.Marshal("Updated permissions saved!") if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -3240,7 +3313,8 @@ func main() { http.Handle("/vis/", gz.GzipHandler(logReq(visualisePage))) http.Handle("/watchers/", gz.GzipHandler(logReq(watchersPage))) http.Handle("/x/apikeygen", gz.GzipHandler(logReq(apiKeyGenHandler))) - http.Handle("/x/apipermupdate", gz.GzipHandler(logReq(apiPermissionsUpdateHandler))) + http.Handle("/x/apikeydbupdate", gz.GzipHandler(logReq(apiKeyDbUpdateHandler))) + http.Handle("/x/apikeypermupdate", gz.GzipHandler(logReq(apiKeyPermsUpdateHandler))) http.Handle("/x/branchnames", gz.GzipHandler(logReq(branchNamesHandler))) http.Handle("/x/callback", gz.GzipHandler(logReq(auth0CallbackHandler))) http.Handle("/x/checkname", gz.GzipHandler(logReq(checkNameHandler))) diff --git a/webui/pages.go b/webui/pages.go index d6d6b53c5..6afc95896 100644 --- a/webui/pages.go +++ b/webui/pages.go @@ -1894,6 +1894,7 @@ func prefPage(w http.ResponseWriter, r *http.Request, loggedInUser string) { var pageData struct { APIKeys []com.APIKey Auth0 com.Auth0Set + DBNames []string DisplayName string Email string MaxRows int @@ -1935,6 +1936,9 @@ func prefPage(w http.ResponseWriter, r *http.Request, loggedInUser string) { return } + // TODO: Retrieve the list of user databases + pageData.DBNames = []string{"foo", "bar", "tempdb1.sqlite"} + // Add Auth0 info to the page data pageData.Auth0 = collectPageAuth0Info() diff --git a/webui/templates/preferences.html b/webui/templates/preferences.html index 2f6d2393e..6f83710dd 100644 --- a/webui/templates/preferences.html +++ b/webui/templates/preferences.html @@ -50,13 +50,13 @@

A Databases Allowed function calls - -

{{ row.key }}

+ +

{{ api.key }}

- +
- {{ row.date_created | date : 'medium' }} + {{ api.date_created | date : 'medium' }}
[[ template "footer" . ]] + + +[[ end ]] \ No newline at end of file diff --git a/webui/templates/preferences.html b/webui/templates/preferences.html index 500ee50fd..e6e9b11e7 100644 --- a/webui/templates/preferences.html +++ b/webui/templates/preferences.html @@ -47,165 +47,16 @@

A Key Generation date - Database(s) - Allowed function calls + Applies to? +   -

{{ api.key }}

-
- -
- +

{{ api.key }}

{{ api.date_created | date : 'medium' }} + {{ api.database_name }} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Branches() -
- - -
-
Metadata() -
- - -
-
Columns() -
- - -
-
Query() -
- - -
-
Commits() -
- - -
-
Releases() -
- - -
-
Databases() -
- - -
-
Tables() -
- - -
-
Delete() -
- - -
-
Tags() -
- - -
-
Diff() -
- - -
-
Upload() -
- - -
-
Download() -
- - -
-
Views() -
- - -
-
Indexes() -
- - -
-
Webpage() -
- - -
-
+ + @@ -239,72 +90,7 @@

 {{ statusMessage }}

// API Keys $scope.apiKeys = [[ .APIKeys ]]; - // Database names - $scope.dbList = [[ .DBNames ]]; - - // Set initial permissions to true for the API keys - // TODO: These will need updating, depending on what the server sets - $scope.radioBranches = "true"; - $scope.radioColumns = "true"; - $scope.radioCommits = "true"; - $scope.radioDatabases = "true"; - $scope.radioDelete = "true"; - $scope.radioDiff = "true"; - $scope.radioDownload = "true"; - $scope.radioIndexes = "true"; - $scope.radioMetadata = "true"; - $scope.radioQuery = "true"; - $scope.radioReleases = "true"; - $scope.radioTables = "true"; - $scope.radioTags = "true"; - $scope.radioUpload = "true"; - $scope.radioViews = "true"; - $scope.radioWebpage = "true"; - - // TODO: Update this to use what the server sets. eg existing value stored in the backend - $scope.allDatabases = true; - $scope.selectedDB = "All databases"; - - // Send the changed database selection to the server - $scope.changeDB = function(apiKey, newDB) { - // Send the new database info to the server - $http({ - method: "POST", - url: "/x/apikeydbupdate/", - data: $httpParamSerializerJQLike({ - "apikey": encodeURIComponent(apiKey), - "dbname": encodeURIComponent(newDB), - "alldbs": encodeURIComponent($scope.allDatabases), - }), - headers: { "Content-Type": "application/x-www-form-urlencoded" } - }).then(function (response) { - // Clear any error message - $scope.statusMessageColour = "green"; - $scope.statusMessage = ""; - }, function failure(response) { - $scope.statusMessageColour = "red"; - $scope.statusMessage = "Server message: " + response.data; - }); - } - - // Select "all databases" - $scope.allDBs = function(apiKey) { - $scope.selectedDB = "All databases"; - $scope.allDatabases = true; - // Send the change to the backend - $scope.changeDB(apiKey, ""); - } - - // Change the selected database - $scope.selectDB = function(apiKey, newDB) { - // TODO: Work with multiple api keys - $scope.selectedDB = newDB; - $scope.allDatabases = false; - - // Send the change to the backend - $scope.changeDB(apiKey, newDB); - } // If the supplied display name is blank, we set a placeholder value instead $scope.FullName = ""; From 2ea568362ab19c07ce0c59fef2b7059945aa9706 Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Fri, 11 Jun 2021 12:40:39 +1000 Subject: [PATCH 12/23] Save point. Changed perms from uint to string, added initial api perms page display code Completely untested, likely to be non-functional atm --- common/postgresql.go | 74 ++++++++++++++++++++++---------------------- common/types.go | 34 ++++++++++---------- webui/main.go | 67 +++++++++++++++++++-------------------- webui/pages.go | 67 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 87 deletions(-) diff --git a/common/postgresql.go b/common/postgresql.go index ea6863179..dc60c9f27 100644 --- a/common/postgresql.go +++ b/common/postgresql.go @@ -141,27 +141,6 @@ func APIKeyDBSave(loggedInUser, apiKey, dbName string, allDB bool) error { return nil } -// APIKeyPermissions returns the permissions and applicable database for an API key -func APIKeyPermissions(apiKey string) (apiDetails APIKey, err error) { - // Retrieve the API key database and permissions - // TODO: Make sure this query gets the right data (haven't test it yet) - dbQuery := ` - WITH key_info AS ( - SELECT key_id - FROM api_keys - WHERE key = $1 - ) - SELECT db.dbname, api.permissions - FROM sqlite_databases db, api_permissions api, key_info - WHERE api.db_id = db.db_id - AND api_permissions.key_id = key_info.key_id` - err = pdb.QueryRow(dbQuery, apiKey).Scan(&apiDetails.Database, &apiDetails.Permissions) - if err != nil { - log.Printf("Fetching API key database and permissions failed: %v\n", err) - } - return -} - // APIKeyPermSave updates the permissions for an API key func APIKeyPermSave(loggedInUser, apiKey string, perm APIPermission, value bool) error { // Data structure for holding the API permission values @@ -195,22 +174,22 @@ func APIKeyPermSave(loggedInUser, apiKey string, perm APIPermission, value bool) // If there isn't any permission data for the API key, it means the key was generated before permissions were // available. So, we default to "all databases" and "all permissions are turned on" if len(permData) == 0 { - permData[APIPERM_BRANCHES] = true - permData[APIPERM_COLUMNS] = true - permData[APIPERM_COMMITS] = true - permData[APIPERM_DATABASES] = true - permData[APIPERM_DELETE] = true - permData[APIPERM_DIFF] = true - permData[APIPERM_DOWNLOAD] = true - permData[APIPERM_INDEXES] = true - permData[APIPERM_METADATA] = true - permData[APIPERM_QUERY] = true - permData[APIPERM_RELEASES] = true - permData[APIPERM_TABLES] = true - permData[APIPERM_TAGS] = true - permData[APIPERM_UPLOAD] = true - permData[APIPERM_VIEWS] = true - permData[APIPERM_WEBPAGE] = true + permData[APIPermBranches] = true + permData[APIPermColumns] = true + permData[APIPermCommits] = true + permData[APIPermDatabases] = true + permData[APIPermDelete] = true + permData[APIPermDiff] = true + permData[APIPermDownload] = true + permData[APIPermIndexes] = true + permData[APIPermMetadata] = true + permData[APIPermQuery] = true + permData[APIPermReleases] = true + permData[APIPermTables] = true + permData[APIPermTags] = true + permData[APIPermUpload] = true + permData[APIPermViews] = true + permData[APIPermWebpage] = true } // Incorporate the updated permission data from the user @@ -2055,6 +2034,27 @@ func GetBranches(dbOwner, dbFolder, dbName string) (branches map[string]BranchEn return branches, nil } +// GetAPIKey returns the details for an API key +func GetAPIKey(apiKey string) (apiDetails APIKey, err error) { + // TODO: Make sure this query gets the right data (haven't tested it yet) + // TODO: Add the remaining API key details + dbQuery := ` + WITH key_info AS ( + SELECT key_id + FROM api_keys + WHERE key = $1 + ) + SELECT db.dbname, api.permissions + FROM sqlite_databases db, api_permissions api, key_info + WHERE api.db_id = db.db_id + AND api_permissions.key_id = key_info.key_id` + err = pdb.QueryRow(dbQuery, apiKey).Scan(&apiDetails.Database, &apiDetails.Permissions) + if err != nil { + log.Printf("Fetching API key database and permissions failed: %v\n", err) + } + return +} + // GetAPIKeys returns the list of API keys for a user func GetAPIKeys(user string) ([]APIKey, error) { dbQuery := ` diff --git a/common/types.go b/common/types.go index 77bd6d886..26d9e7dec 100644 --- a/common/types.go +++ b/common/types.go @@ -243,25 +243,25 @@ type APIKey struct { Permissions map[APIPermission]bool `json:"permissions"` } -type APIPermission uint +type APIPermission string const ( - APIPERM_BRANCHES = 0 // These are not iota, as it would be seriously bad for these numbers to change - APIPERM_COLUMNS = 1 - APIPERM_COMMITS = 2 - APIPERM_DATABASES = 3 - APIPERM_DELETE = 4 - APIPERM_DIFF = 5 - APIPERM_DOWNLOAD = 6 - APIPERM_INDEXES = 7 - APIPERM_METADATA = 8 - APIPERM_QUERY = 9 - APIPERM_RELEASES = 10 - APIPERM_TABLES = 11 - APIPERM_TAGS = 12 - APIPERM_UPLOAD = 13 - APIPERM_VIEWS = 14 - APIPERM_WEBPAGE = 15 + APIPermBranches = "branches" + APIPermColumns = "columns" + APIPermCommits = "commits" + APIPermDatabases = "databases" + APIPermDelete = "delete" + APIPermDiff = "diff" + APIPermDownload = "download" + APIPermIndexes = "indexes" + APIPermMetadata = "metadata" + APIPermQuery = "query" + APIPermReleases = "releases" + APIPermTables = "tables" + APIPermTags = "tags" + APIPermUpload = "upload" + APIPermViews = "views" + APIPermWebpage = "webpage" ) type Auth0Set struct { diff --git a/webui/main.go b/webui/main.go index 7f1d885bf..5714c9a7c 100644 --- a/webui/main.go +++ b/webui/main.go @@ -172,29 +172,29 @@ func apiKeyGenHandler(w http.ResponseWriter, r *http.Request) { // Create a structure holding the default permissions permData := make(map[com.APIPermission]bool) - permData[com.APIPERM_BRANCHES] = true - permData[com.APIPERM_COLUMNS] = true - permData[com.APIPERM_COMMITS] = true - permData[com.APIPERM_DATABASES] = true - permData[com.APIPERM_DELETE] = true - permData[com.APIPERM_DIFF] = true - permData[com.APIPERM_DOWNLOAD] = true - permData[com.APIPERM_INDEXES] = true - permData[com.APIPERM_METADATA] = true - permData[com.APIPERM_QUERY] = true - permData[com.APIPERM_RELEASES] = true - permData[com.APIPERM_TABLES] = true - permData[com.APIPERM_TAGS] = true - permData[com.APIPERM_UPLOAD] = true - permData[com.APIPERM_VIEWS] = true - permData[com.APIPERM_WEBPAGE] = true + permData[com.APIPermBranches] = true + permData[com.APIPermColumns] = true + permData[com.APIPermCommits] = true + permData[com.APIPermDatabases] = true + permData[com.APIPermDelete] = true + permData[com.APIPermDiff] = true + permData[com.APIPermDownload] = true + permData[com.APIPermIndexes] = true + permData[com.APIPermMetadata] = true + permData[com.APIPermQuery] = true + permData[com.APIPermReleases] = true + permData[com.APIPermTables] = true + permData[com.APIPermTags] = true + permData[com.APIPermUpload] = true + permData[com.APIPermViews] = true + permData[com.APIPermWebpage] = true // Return the API key to the caller d := com.APIKey{ Database: "", // Default to "all databases" Key: key, DateCreated: creationTime, - Permissions: permData, + Permissions: permData, } data, err := json.Marshal(d) if err != nil { @@ -249,37 +249,37 @@ func apiKeyPermsUpdateHandler(w http.ResponseWriter, r *http.Request) { var perm com.APIPermission switch strings.ToLower(p2) { case "branches": - perm = com.APIPERM_BRANCHES + perm = com.APIPermBranches case "columns": - perm = com.APIPERM_COLUMNS + perm = com.APIPermColumns case "commits": - perm = com.APIPERM_COMMITS + perm = com.APIPermCommits case "databases": - perm = com.APIPERM_DATABASES + perm = com.APIPermDatabases case "delete": - perm = com.APIPERM_DELETE + perm = com.APIPermDelete case "diff": - perm = com.APIPERM_DIFF + perm = com.APIPermDiff case "download": - perm = com.APIPERM_DOWNLOAD + perm = com.APIPermDownload case "indexes": - perm = com.APIPERM_INDEXES + perm = com.APIPermIndexes case "metadata": - perm = com.APIPERM_METADATA + perm = com.APIPermMetadata case "query": - perm = com.APIPERM_QUERY + perm = com.APIPermQuery case "releases": - perm = com.APIPERM_RELEASES + perm = com.APIPermReleases case "tables": - perm = com.APIPERM_TABLES + perm = com.APIPermTables case "tags": - perm = com.APIPERM_TAGS + perm = com.APIPermTags case "upload": - perm = com.APIPERM_UPLOAD + perm = com.APIPermUpload case "views": - perm = com.APIPERM_VIEWS + perm = com.APIPermViews case "webpage": - perm = com.APIPERM_WEBPAGE + perm = com.APIPermWebpage default: w.WriteHeader(http.StatusBadRequest) fmt.Fprint(w, "Unknown permission name") @@ -3330,6 +3330,7 @@ func main() { // Our pages http.Handle("/", gz.GzipHandler(logReq(mainHandler))) http.Handle("/about", gz.GzipHandler(logReq(aboutPage))) + http.Handle("/apiperms", gz.GzipHandler(logReq(apiPermissionsPage))) http.Handle("/branches/", gz.GzipHandler(logReq(branchesPage))) http.Handle("/commits/", gz.GzipHandler(logReq(commitsPage))) http.Handle("/compare/", gz.GzipHandler(logReq(comparePage))) diff --git a/webui/pages.go b/webui/pages.go index ff78c8111..b3f8daf9f 100644 --- a/webui/pages.go +++ b/webui/pages.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net/http" + "net/url" "strconv" "strings" "time" @@ -38,6 +39,72 @@ func aboutPage(w http.ResponseWriter, r *http.Request) { } } +// Renders the "API Key permissions" page. +func apiPermissionsPage(w http.ResponseWriter, r *http.Request) { + var pageData struct { + Auth0 com.Auth0Set + Meta com.MetaInfo + KeyInfo com.APIKey + } + + // Get all meta information + errCode, err := collectPageMetaInfo(r, &pageData.Meta, true, false) + if err != nil { + errorPage(w, r, errCode, err.Error()) + return + } + pageData.Meta.Title = "What is DBHub.io?" + + // Add Auth0 info to the page data + pageData.Auth0 = collectPageAuth0Info() + + // Get the API key from the user provided data + a := r.PostFormValue("apikey") + apiKey, err := url.QueryUnescape(a) + if err != nil { + errorPage(w, r, http.StatusInternalServerError, err.Error()) + return + } + + // Validate the API key + err = com.CheckAPIKey(apiKey) + if err != nil { + errorPage(w, r, http.StatusBadRequest, err.Error()) + return + } + + // If no API key was provided, display an error instead of the page content + if apiKey == "" { + errorPage(w, r, http.StatusBadRequest, "No API key provided") + return + } + + // Verify the API key belongs to the logged in user. eg don't allow looking at other people's API keys + keyOwner, err := com.GetAPIKeyUser(apiKey) + if err != nil { + errorPage(w, r, http.StatusInternalServerError, err.Error()) + return + } + if keyOwner != pageData.Meta.LoggedInUser { + errorPage(w, r, http.StatusBadRequest, "Unknown API key") + return + } + + // Retrieve the API key details + pageData.KeyInfo, err = com.GetAPIKey(apiKey) + if err != nil { + errorPage(w, r, http.StatusBadRequest, err.Error()) + return + } + + // Render the page + t := tmpl.Lookup("apipermsPage") + err = t.Execute(w, pageData) + if err != nil { + log.Printf("Error: %s", err) + } +} + // Render the branches page, which lists the branches for a database. func branchesPage(w http.ResponseWriter, r *http.Request) { // Structure to hold page data From a37f25798de33f7832f6821eaf043cad59733d63 Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Sat, 12 Jun 2021 20:45:05 +1000 Subject: [PATCH 13/23] Save point. Returning a map of api keys instead of a string slice Should make it easier for the frontend to iterate through --- common/postgresql.go | 83 ++++++++++++++++++++++++++++++++++++-------- webui/pages.go | 13 +------ 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/common/postgresql.go b/common/postgresql.go index dc60c9f27..9c0eb516b 100644 --- a/common/postgresql.go +++ b/common/postgresql.go @@ -2056,37 +2056,90 @@ func GetAPIKey(apiKey string) (apiDetails APIKey, err error) { } // GetAPIKeys returns the list of API keys for a user -func GetAPIKeys(user string) ([]APIKey, error) { +func GetAPIKeys(user string) (apiKeys map[string]APIKey, err error) { + // TODO: Do this as one query, probably using an outer join + + // Get the API key(s) and their creation dates + apiKeys = make(map[string]APIKey) dbQuery := ` - SELECT api.key, api.date_created, db.db_name, perms.permissions - FROM api_keys api - INNER JOIN api_permissions perms ON api.key_id = perms.key_id - INNER JOIN sqlite_databases db ON db.db_id = perms.db_id - WHERE api.user_id = ( + SELECT key, date_created + FROM api_keys + WHERE user_id = ( SELECT user_id FROM users WHERE lower(user_name) = lower($1) )` - rows, err := pdb.Query(dbQuery, user) + var rows *pgx.Rows + defer func() { + if rows != nil { + rows.Close() + } + }() + rows, err = pdb.Query(dbQuery, user) if err != nil { log.Printf("Database query failed: %v\n", err) - return nil, err + return } defer rows.Close() - var keys []APIKey for rows.Next() { var key string var dateCreated time.Time - var dbName pgx.NullString - var perms map[APIPermission]bool - err = rows.Scan(&key, &dateCreated, &dbName, &perms) + err = rows.Scan(&key, &dateCreated) if err != nil { log.Printf("Error retrieving API key list: %v\n", err) - return nil, err + return } - keys = append(keys, APIKey{Key: key, DateCreated: dateCreated, Database: dbName.String, Permissions: perms}) + apiKeys[key] = APIKey{Key: key, DateCreated: dateCreated} } - return keys, nil + + // Get the database and permissions for each key, if it exists + for key, details := range apiKeys { + dbQuery = ` + SELECT db.db_name, perms.permissions + FROM api_keys api + INNER JOIN api_permissions perms ON api.key_id = perms.key_id + INNER JOIN sqlite_databases db ON db.db_id = perms.db_id + WHERE api.user_id = ( + SELECT user_id + FROM users + WHERE lower(user_name) = lower($1) + ) + AND perms.key_id = ( + SELECT key_id + FROM api_keys + WHERE key = $2)` + var dbName pgx.NullString + var perms map[APIPermission]bool + err = pdb.QueryRow(dbQuery, user, key).Scan(&dbName, &perms) + if err != nil && err != pgx.ErrNoRows { + log.Printf("Error retrieving API key permissions: %v\n", err) + return + } + // If there aren't (yet) any permissions saved for the api key, we set the defaults + if err == pgx.ErrNoRows { + perms = make(map[APIPermission]bool) + perms[APIPermBranches] = true + perms[APIPermColumns] = true + perms[APIPermCommits] = true + perms[APIPermDatabases] = true + perms[APIPermDelete] = true + perms[APIPermDiff] = true + perms[APIPermDownload] = true + perms[APIPermIndexes] = true + perms[APIPermMetadata] = true + perms[APIPermQuery] = true + perms[APIPermReleases] = true + perms[APIPermTables] = true + perms[APIPermTags] = true + perms[APIPermUpload] = true + perms[APIPermViews] = true + perms[APIPermWebpage] = true + err = nil + } + + apiKeys[key] = APIKey{Key: key, DateCreated: details.DateCreated, Database: dbName.String, Permissions: perms} + } + return } // GetAPIKeyUser returns the owner of a given API key. Returns an empty string if the key has no known owner diff --git a/webui/pages.go b/webui/pages.go index b3f8daf9f..833ce45f0 100644 --- a/webui/pages.go +++ b/webui/pages.go @@ -1959,7 +1959,7 @@ func mergePage(w http.ResponseWriter, r *http.Request) { // Renders the user Settings page. func prefPage(w http.ResponseWriter, r *http.Request, loggedInUser string) { var pageData struct { - APIKeys []com.APIKey + APIKeys map[string]com.APIKey Auth0 com.Auth0Set DBNames []string DisplayName string @@ -2003,17 +2003,6 @@ func prefPage(w http.ResponseWriter, r *http.Request, loggedInUser string) { return } - //// TODO: Handle multiple API keys - // - //// TODO: Get the API key permissions and applicable database - //for _, apiKey := range pageData.APIKeys { - // dbName, perms , err := com.APIKeyPermissions(apiKey.Key) - // if err != nil { - // errorPage(w, r, http.StatusInternalServerError, "Retrieving API key details failed") - // return - // } - //} - // Create the list of databases belonging to the user dbList, err := com.UserDBs(loggedInUser, com.DB_BOTH) if err != nil { From fd821eb46814e6d60436abc457885c3996775177 Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Sun, 13 Jun 2021 02:32:53 +1000 Subject: [PATCH 14/23] Permission toggles now reflect their saved database values Took ages to figure this out. Needs testing with multiple api keys for a user too. That'll likely be done soon. --- common/postgresql.go | 3 +- webui/templates/preferences.html | 206 ++++++++++++++++++++++++++++++- 2 files changed, 202 insertions(+), 7 deletions(-) diff --git a/common/postgresql.go b/common/postgresql.go index 9c0eb516b..249c7195b 100644 --- a/common/postgresql.go +++ b/common/postgresql.go @@ -2115,7 +2115,8 @@ func GetAPIKeys(user string) (apiKeys map[string]APIKey, err error) { log.Printf("Error retrieving API key permissions: %v\n", err) return } - // If there aren't (yet) any permissions saved for the api key, we set the defaults + + // If there aren't (yet) any permissions saved for the api key, we enable everything by default if err == pgx.ErrNoRows { perms = make(map[APIPermission]bool) perms[APIPermBranches] = true diff --git a/webui/templates/preferences.html b/webui/templates/preferences.html index e6e9b11e7..da51e2fab 100644 --- a/webui/templates/preferences.html +++ b/webui/templates/preferences.html @@ -47,16 +47,165 @@

A Key Generation date - Applies to? -   + Database(s) + Allowed function calls -

{{ api.key }}

+

{{ api.key }}

+
+ +
+ {{ api.date_created | date : 'medium' }} - {{ api.database_name }} - - +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Branches() +
+ + +
+
Metadata() +
+ + +
+
Columns() +
+ + +
+
Query() +
+ + +
+
Commits() +
+ + +
+
Releases() +
+ + +
+
Databases() +
+ + +
+
Tables() +
+ + +
+
Delete() +
+ + +
+
Tags() +
+ + +
+
Diff() +
+ + +
+
Upload() +
+ + +
+
Download() +
+ + +
+
Views() +
+ + +
+
Indexes() +
+ + +
+
Webpage() +
+ + +
+
@@ -90,7 +239,52 @@

 {{ statusMessage }}

// API Keys $scope.apiKeys = [[ .APIKeys ]]; + // Database names + $scope.dbList = [[ .DBNames ]]; + + // Send the changed database selection to the server + $scope.changeDB = function(apiKey, newDB) { + // Work out the "all databases" value to send + let allDBs = false; + if (newDB === "") { + allDBs = true; + } + // Send the new database info to the server + $http({ + method: "POST", + url: "/x/apikeydbupdate/", + data: $httpParamSerializerJQLike({ + "apikey": encodeURIComponent(apiKey), + "dbname": encodeURIComponent(newDB), + "alldbs": encodeURIComponent(allDBs), + }), + headers: { "Content-Type": "application/x-www-form-urlencoded" } + }).then(function (response) { + // Clear any error message + $scope.statusMessageColour = "green"; + $scope.statusMessage = ""; + }, function failure(response) { + $scope.statusMessageColour = "red"; + $scope.statusMessage = "Server message: " + response.data; + }); + } + + // Select "all databases" + $scope.allDBs = function(apiKey) { + $scope.apiKeys[apiKey].database_name = "All databases"; + + // Send the change to the backend + $scope.changeDB(apiKey, ""); + } + + // Change the selected database + $scope.selectDB = function(apiKey, newDB) { + $scope.apiKeys[apiKey].database_name = newDB; + + // Send the change to the backend + $scope.changeDB(apiKey, newDB); + } // If the supplied display name is blank, we set a placeholder value instead $scope.FullName = ""; From 872b542cd4f5e8087852048ee3e4801487cda29a Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Sun, 13 Jun 2021 03:01:04 +1000 Subject: [PATCH 15/23] Select the "All databases" option in the webUI correctly --- webui/pages.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/webui/pages.go b/webui/pages.go index 833ce45f0..cf2c4ac30 100644 --- a/webui/pages.go +++ b/webui/pages.go @@ -2003,6 +2003,15 @@ func prefPage(w http.ResponseWriter, r *http.Request, loggedInUser string) { return } + // The API keys with no database value set should have their text say "All databases" + for key, j := range pageData.APIKeys { + if j.Database == "" { + tmp := pageData.APIKeys[key] + tmp.Database = "All databases" + pageData.APIKeys[key] = tmp + } + } + // Create the list of databases belonging to the user dbList, err := com.UserDBs(loggedInUser, com.DB_BOTH) if err != nil { From 81f9160b7df7e7eba960d319df5ab8aff54d0325 Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Sun, 13 Jun 2021 03:05:33 +1000 Subject: [PATCH 16/23] Default api keys to all permissions enabled. --- common/postgresql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/postgresql.go b/common/postgresql.go index 249c7195b..4bfef2398 100644 --- a/common/postgresql.go +++ b/common/postgresql.go @@ -2117,7 +2117,7 @@ func GetAPIKeys(user string) (apiKeys map[string]APIKey, err error) { } // If there aren't (yet) any permissions saved for the api key, we enable everything by default - if err == pgx.ErrNoRows { + if err == pgx.ErrNoRows || perms == nil { perms = make(map[APIPermission]bool) perms[APIPermBranches] = true perms[APIPermColumns] = true From 24f056eeca920fa8dcbb37ccd9e8cde37188d792 Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Sun, 13 Jun 2021 03:09:19 +1000 Subject: [PATCH 17/23] No need for a separate api permissions page yet --- webui/main.go | 1 - webui/pages.go | 67 -------- webui/templates/apiperms.html | 280 ---------------------------------- 3 files changed, 348 deletions(-) delete mode 100644 webui/templates/apiperms.html diff --git a/webui/main.go b/webui/main.go index 5714c9a7c..995a13f1e 100644 --- a/webui/main.go +++ b/webui/main.go @@ -3330,7 +3330,6 @@ func main() { // Our pages http.Handle("/", gz.GzipHandler(logReq(mainHandler))) http.Handle("/about", gz.GzipHandler(logReq(aboutPage))) - http.Handle("/apiperms", gz.GzipHandler(logReq(apiPermissionsPage))) http.Handle("/branches/", gz.GzipHandler(logReq(branchesPage))) http.Handle("/commits/", gz.GzipHandler(logReq(commitsPage))) http.Handle("/compare/", gz.GzipHandler(logReq(comparePage))) diff --git a/webui/pages.go b/webui/pages.go index cf2c4ac30..9d6097fd3 100644 --- a/webui/pages.go +++ b/webui/pages.go @@ -4,7 +4,6 @@ import ( "fmt" "log" "net/http" - "net/url" "strconv" "strings" "time" @@ -39,72 +38,6 @@ func aboutPage(w http.ResponseWriter, r *http.Request) { } } -// Renders the "API Key permissions" page. -func apiPermissionsPage(w http.ResponseWriter, r *http.Request) { - var pageData struct { - Auth0 com.Auth0Set - Meta com.MetaInfo - KeyInfo com.APIKey - } - - // Get all meta information - errCode, err := collectPageMetaInfo(r, &pageData.Meta, true, false) - if err != nil { - errorPage(w, r, errCode, err.Error()) - return - } - pageData.Meta.Title = "What is DBHub.io?" - - // Add Auth0 info to the page data - pageData.Auth0 = collectPageAuth0Info() - - // Get the API key from the user provided data - a := r.PostFormValue("apikey") - apiKey, err := url.QueryUnescape(a) - if err != nil { - errorPage(w, r, http.StatusInternalServerError, err.Error()) - return - } - - // Validate the API key - err = com.CheckAPIKey(apiKey) - if err != nil { - errorPage(w, r, http.StatusBadRequest, err.Error()) - return - } - - // If no API key was provided, display an error instead of the page content - if apiKey == "" { - errorPage(w, r, http.StatusBadRequest, "No API key provided") - return - } - - // Verify the API key belongs to the logged in user. eg don't allow looking at other people's API keys - keyOwner, err := com.GetAPIKeyUser(apiKey) - if err != nil { - errorPage(w, r, http.StatusInternalServerError, err.Error()) - return - } - if keyOwner != pageData.Meta.LoggedInUser { - errorPage(w, r, http.StatusBadRequest, "Unknown API key") - return - } - - // Retrieve the API key details - pageData.KeyInfo, err = com.GetAPIKey(apiKey) - if err != nil { - errorPage(w, r, http.StatusBadRequest, err.Error()) - return - } - - // Render the page - t := tmpl.Lookup("apipermsPage") - err = t.Execute(w, pageData) - if err != nil { - log.Printf("Error: %s", err) - } -} - // Render the branches page, which lists the branches for a database. func branchesPage(w http.ResponseWriter, r *http.Request) { // Structure to hold page data diff --git a/webui/templates/apiperms.html b/webui/templates/apiperms.html deleted file mode 100644 index 72e1333dd..000000000 --- a/webui/templates/apiperms.html +++ /dev/null @@ -1,280 +0,0 @@ -[[ define "apipermsPage" ]] - - -[[ template "head" . ]] - -[[ template "header" . ]] -
- - - - - - - - - - - - - - - - - - - - - -
KeyGeneration dateDatabase(s)Allowed function calls

{{ api.key }}

-
- -
-
{{ api.date_created | date : 'medium' }} -
- - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Branches() -
- - -
-
Metadata() -
- - -
-
Columns() -
- - -
-
Query() -
- - -
-
Commits() -
- - -
-
Releases() -
- - -
-
Databases() -
- - -
-
Tables() -
- - -
-
Delete() -
- - -
-
Tags() -
- - -
-
Diff() -
- - -
-
Upload() -
- - -
-
Download() -
- - -
-
Views() -
- - -
-
Indexes() -
- - -
-
Webpage() -
- - -
-
-
You don't have any API keys yet
-
-
-
-

 {{ statusMessage }}

-
-
-
-
- -
-
- - - -
-[[ template "footer" . ]] - - - -[[ end ]] \ No newline at end of file From 74bf0b25eb91ea1f1c4e1916e828d43731cbbc2a Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Mon, 14 Jun 2021 02:03:27 +1000 Subject: [PATCH 18/23] Initial concept code adding permission checks to the api end point Untested. It compiles, but that's it. *Might* work for some of the functions, but unlikely to work for the Databases(), Releases() and Tags() functions as they'll need special handling. ;) --- api/handlers.go | 127 +++++++++++++++++++++++++++---- api/main.go | 62 ++++++++++++--- common/postgresql.go | 42 +++++----- webui/templates/preferences.html | 5 ++ 4 files changed, 190 insertions(+), 46 deletions(-) diff --git a/api/handlers.go b/api/handlers.go index 0c9362688..e140b2a4f 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -21,13 +21,20 @@ import ( // * "dbname" is the name of the database func branchesHandler(w http.ResponseWriter, r *http.Request) { // Do auth check, grab request info - _, dbOwner, dbName, _, httpStatus, err := collectInfo(w, r) + loggedInUser, dbOwner, dbName, apiKey, _, httpStatus, err := collectInfo(w, r) if err != nil { jsonErr(w, err.Error(), httpStatus) return } dbFolder := "/" + // Make sure the API key has permission to run this function on the requested database + err = permissionCheck(loggedInUser, apiKey, dbName, "branches") + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + // Retrieve the branch list for the database brList, err := com.BranchListResponse(dbOwner, dbFolder, dbName) if err != nil { @@ -59,7 +66,7 @@ func branchesHandler(w http.ResponseWriter, r *http.Request) { // * "table" is the name of the table or view func columnsHandler(w http.ResponseWriter, r *http.Request) { // Do auth check, grab request info, open the database - sdb, httpStatus, err := collectInfoAndOpen(w, r) + sdb, httpStatus, err := collectInfoAndOpen(w, r, "columns") if err != nil { jsonErr(w, err.Error(), httpStatus) return @@ -117,13 +124,20 @@ func columnsHandler(w http.ResponseWriter, r *http.Request) { // * "dbname" is the name of the database func commitsHandler(w http.ResponseWriter, r *http.Request) { // Do auth check, grab request info - _, dbOwner, dbName, _, httpStatus, err := collectInfo(w, r) + loggedInUser, dbOwner, dbName, apiKey, _, httpStatus, err := collectInfo(w, r) if err != nil { jsonErr(w, err.Error(), httpStatus) return } dbFolder := "/" + // Make sure the API key has permission to run this function on the requested database + err = permissionCheck(loggedInUser, apiKey, dbName, "commits") + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + // Retrieve the commits commits, err := com.GetCommitList(dbOwner, dbFolder, dbName) if err != nil { @@ -147,7 +161,16 @@ func commitsHandler(w http.ResponseWriter, r *http.Request) { // * "apikey" is one of your API keys. These can be generated from your Settings page once logged in func databasesHandler(w http.ResponseWriter, r *http.Request) { // Authenticate the request - loggedInUser, err := checkAuth(w, r) + loggedInUser, apiKey, err := checkAuth(w, r) + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + + // Make sure the API key has permission to run this function on the requested database + // TODO: We probably need a special case for handling the Databases(), Releases(), and Tags() functions. + // Maybe set the dbName value here to a magic value, which permissionCheck() looks for? + err = permissionCheck(loggedInUser, apiKey, "what should we do here", "databases") if err != nil { jsonErr(w, err.Error(), http.StatusUnauthorized) return @@ -184,7 +207,7 @@ func databasesHandler(w http.ResponseWriter, r *http.Request) { // * "dbname" is the name of the database func deleteHandler(w http.ResponseWriter, r *http.Request) { // Authenticate the request - loggedInUser, err := checkAuth(w, r) + loggedInUser, apiKey, err := checkAuth(w, r) if err != nil { jsonErr(w, err.Error(), http.StatusUnauthorized) return @@ -199,6 +222,13 @@ func deleteHandler(w http.ResponseWriter, r *http.Request) { dbOwner := loggedInUser dbFolder := "/" + // Make sure the API key has permission to run this function on the requested database + err = permissionCheck(loggedInUser, apiKey, dbName, "delete") + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + // Invalidate the memcache data for the database err = com.InvalidateCacheEntry(loggedInUser, dbOwner, dbFolder, dbName, "") // Empty string indicates "for all versions" if err != nil { @@ -237,7 +267,7 @@ func deleteHandler(w http.ResponseWriter, r *http.Request) { // * "merge" specifies the merge strategy (possible values: "none", "preserve_pk", "new_pk"; optional, defaults to "none") // * "include_data" can be set to "1" to include the full data of all changed rows instead of just the primary keys (optional, defaults to 0) func diffHandler(w http.ResponseWriter, r *http.Request) { - loggedInUser, err := checkAuth(w, r) + loggedInUser, apiKey, err := checkAuth(w, r) if err != nil { jsonErr(w, err.Error(), http.StatusUnauthorized) return @@ -334,6 +364,18 @@ func diffHandler(w http.ResponseWriter, r *http.Request) { return } + // Make sure the API key has permission to run this function on the requested databases + err = permissionCheck(loggedInUser, apiKey, dbNameA, "diff") + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + err = permissionCheck(loggedInUser, apiKey, dbNameB, "diff") + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + // Perform diff diffs, err := com.Diff(dbOwnerA, "/", dbNameA, ca, dbOwnerB, "/", dbNameB, cb, loggedInUser, mergeStrategy, includeData) if err != nil { @@ -359,13 +401,20 @@ func diffHandler(w http.ResponseWriter, r *http.Request) { // * "dbname" is the name of the database func downloadHandler(w http.ResponseWriter, r *http.Request) { // Authenticate user and collect requested database details - loggedInUser, dbOwner, dbName, commitID, httpStatus, err := collectInfo(w, r) + loggedInUser, dbOwner, dbName, apiKey, commitID, httpStatus, err := collectInfo(w, r) if err != nil { jsonErr(w, err.Error(), httpStatus) return } dbFolder := "/" + // Make sure the API key has permission to run this function on the requested database + err = permissionCheck(loggedInUser, apiKey, dbName, "download") + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + // Return the requested database to the user _, err = com.DownloadDatabase(w, r, dbOwner, dbFolder, dbName, commitID, loggedInUser, "api") if err != nil { @@ -382,7 +431,7 @@ func downloadHandler(w http.ResponseWriter, r *http.Request) { // * "dbname" is the name of the database func indexesHandler(w http.ResponseWriter, r *http.Request) { // Do auth check, grab request info, open the database - sdb, httpStatus, err := collectInfoAndOpen(w, r) + sdb, httpStatus, err := collectInfoAndOpen(w, r, "indexes") if err != nil { jsonErr(w, err.Error(), httpStatus) return @@ -436,13 +485,20 @@ func indexesHandler(w http.ResponseWriter, r *http.Request) { // * "dbname" is the name of the database func metadataHandler(w http.ResponseWriter, r *http.Request) { // Do auth check, grab request info - _, dbOwner, dbName, _, httpStatus, err := collectInfo(w, r) + loggedInUser, dbOwner, dbName, apiKey, _, httpStatus, err := collectInfo(w, r) if err != nil { jsonErr(w, err.Error(), httpStatus) return } dbFolder := "/" + // Make sure the API key has permission to run this function on the requested database + err = permissionCheck(loggedInUser, apiKey, dbName, "metadata") + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + // Retrieve the metadata for the database meta, err := com.MetadataResponse(dbOwner, dbFolder, dbName) if err != nil { @@ -472,7 +528,7 @@ func metadataHandler(w http.ResponseWriter, r *http.Request) { // * "dbname" is the name of the database // * "sql" is the SQL query to run, base64 encoded func queryHandler(w http.ResponseWriter, r *http.Request) { - loggedInUser, err := checkAuth(w, r) + loggedInUser, apiKey, err := checkAuth(w, r) if err != nil { jsonErr(w, err.Error(), http.StatusUnauthorized) return @@ -486,6 +542,13 @@ func queryHandler(w http.ResponseWriter, r *http.Request) { } dbFolder := "/" + // Make sure the API key has permission to run this function on the requested database + err = permissionCheck(loggedInUser, apiKey, dbName, "query") + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + // Grab the incoming SQLite query rawInput := r.FormValue("sql") decodedStr, err := com.CheckUnicode(rawInput) @@ -532,13 +595,22 @@ func queryHandler(w http.ResponseWriter, r *http.Request) { // * "dbname" is the name of the database func releasesHandler(w http.ResponseWriter, r *http.Request) { // Do auth check, grab request info - _, dbOwner, dbName, _, httpStatus, err := collectInfo(w, r) + loggedInUser, dbOwner, dbName, apiKey, _, httpStatus, err := collectInfo(w, r) if err != nil { jsonErr(w, err.Error(), httpStatus) return } dbFolder := "/" + // Make sure the API key has permission to run this function on the requested database + // TODO: We probably need a special case for handling the Databases(), Releases(), and Tags() functions. + // Maybe set the dbName value here to a magic value, which permissionCheck() looks for? + err = permissionCheck(loggedInUser, apiKey, "what should we do here", "releases") + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + // Retrieve the list of releases rels, err := com.GetReleases(dbOwner, dbFolder, dbName) if err != nil { @@ -590,7 +662,7 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { // * "dbname" is the name of the database func tablesHandler(w http.ResponseWriter, r *http.Request) { // Do auth check, grab request info, open the database - sdb, httpStatus, err := collectInfoAndOpen(w, r) + sdb, httpStatus, err := collectInfoAndOpen(w, r, "tables") if err != nil { jsonErr(w, err.Error(), httpStatus) return @@ -622,13 +694,22 @@ func tablesHandler(w http.ResponseWriter, r *http.Request) { // * "dbname" is the name of the database func tagsHandler(w http.ResponseWriter, r *http.Request) { // Do auth check, grab request info - _, dbOwner, dbName, _, httpStatus, err := collectInfo(w, r) + loggedInUser, dbOwner, dbName, apiKey, _, httpStatus, err := collectInfo(w, r) if err != nil { jsonErr(w, err.Error(), httpStatus) return } dbFolder := "/" + // Make sure the API key has permission to run this function on the requested database + // TODO: We probably need a special case for handling the Databases(), Releases(), and Tags() functions. + // Maybe set the dbName value here to a magic value, which permissionCheck() looks for? + err = permissionCheck(loggedInUser, apiKey, "what should we do here", "tags") + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + // Retrieve the tags tags, err := com.GetTags(dbOwner, dbFolder, dbName) if err != nil { @@ -669,7 +750,7 @@ func uploadHandler(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, com.MaxDatabaseSize*1024*1024) // Authenticate the request - loggedInUser, err := checkAuth(w, r) + loggedInUser, apiKey, err := checkAuth(w, r) if err != nil { jsonErr(w, err.Error(), http.StatusUnauthorized) return @@ -682,6 +763,13 @@ func uploadHandler(w http.ResponseWriter, r *http.Request) { return } + // Make sure the API key has permission to run this function on the requested database + err = permissionCheck(loggedInUser, apiKey, dbName, "upload") + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + // The "public" user isn't allowed to make changes if loggedInUser == "public" { log.Printf("User from '%s' attempted to add a database using the public certificate", r.RemoteAddr) @@ -745,7 +833,7 @@ func uploadHandler(w http.ResponseWriter, r *http.Request) { // * "dbname" is the name of the database being queried func viewsHandler(w http.ResponseWriter, r *http.Request) { // Do auth check, grab request info, open the database - sdb, httpStatus, err := collectInfoAndOpen(w, r) + sdb, httpStatus, err := collectInfoAndOpen(w, r, "views") if err != nil { jsonErr(w, err.Error(), httpStatus) return @@ -777,13 +865,20 @@ func viewsHandler(w http.ResponseWriter, r *http.Request) { // * "dbname" is the name of the database being queried func webpageHandler(w http.ResponseWriter, r *http.Request) { // Authenticate user and collect requested database details - _, dbOwner, dbName, _, httpStatus, err := collectInfo(w, r) + loggedInUser, dbOwner, dbName, apiKey, _, httpStatus, err := collectInfo(w, r) if err != nil { jsonErr(w, err.Error(), httpStatus) return } dbFolder := "/" + // Make sure the API key has permission to run this function on the requested database + err = permissionCheck(loggedInUser, apiKey, dbName, "webpage") + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + // Return the database webUI URL to the user var z com.WebpageResponseContainer z.WebPage = "https://" + com.Conf.Web.ServerName + "/" + dbOwner + dbFolder + dbName diff --git a/api/main.go b/api/main.go index eb45bf89e..694e90188 100644 --- a/api/main.go +++ b/api/main.go @@ -151,19 +151,22 @@ func main() { } // checkAuth authenticates and logs the incoming request -func checkAuth(w http.ResponseWriter, r *http.Request) (loggedInUser string, err error) { +func checkAuth(w http.ResponseWriter, r *http.Request) (loggedInUser, apiKey string, err error) { // Extract the API key from the request - apiKey := r.FormValue("apikey") + a := r.FormValue("apikey") // Check if an API key was provided - if apiKey != "" { + if a != "" { // Validate the API key - err = com.CheckAPIKey(apiKey) + err = com.CheckAPIKey(a) if err != nil { err = fmt.Errorf("Incorrect or unknown API key and certificate") return } + // API key passed validation + apiKey = a + // Look up the owner of the API key loggedInUser, err = com.GetAPIKeyUser(apiKey) } else { @@ -177,6 +180,12 @@ func checkAuth(w http.ResponseWriter, r *http.Request) (loggedInUser string, err return } + // If the client authenticated through their certificate instead of an API key, then we need to pass + // that information along for special handling + if apiKey == "" { + apiKey = "clientcert" + } + // Log the incoming request logReq(r, loggedInUser) return @@ -184,11 +193,11 @@ func checkAuth(w http.ResponseWriter, r *http.Request) (loggedInUser string, err // collectInfo is an internal function which: // 1. Authenticates incoming requests -// 2. Extracts the database owner, name, and commit ID from the request +// 2. Extracts the database owner, name, api key, and commit ID from the request // 3. Checks permissions -func collectInfo(w http.ResponseWriter, r *http.Request) (loggedInUser, dbOwner, dbName, commitID string, httpStatus int, err error) { +func collectInfo(w http.ResponseWriter, r *http.Request) (loggedInUser, dbOwner, dbName, apiKey, commitID string, httpStatus int, err error) { // Authenticate the request - loggedInUser, err = checkAuth(w, r) + loggedInUser, apiKey, err = checkAuth(w, r) if err != nil { httpStatus = http.StatusUnauthorized return @@ -222,15 +231,22 @@ func collectInfo(w http.ResponseWriter, r *http.Request) (loggedInUser, dbOwner, // 2. Fetches the database from Minio // 3. Opens the database, returning the connection handle // This function exists purely because this code is common to most of the handlers -func collectInfoAndOpen(w http.ResponseWriter, r *http.Request) (sdb *sqlite.Conn, httpStatus int, err error) { +func collectInfoAndOpen(w http.ResponseWriter, r *http.Request, permName com.APIPermission) (sdb *sqlite.Conn, httpStatus int, err error) { // Authenticate the request and collect details for the requested database - loggedInUser, dbOwner, dbName, commitID, httpStatus, err := collectInfo(w, r) + loggedInUser, dbOwner, dbName, apiKey, commitID, httpStatus, err := collectInfo(w, r) if err != nil { httpStatus = http.StatusInternalServerError return } dbFolder := "/" + // Make sure the API key has permission to run this function on the requested database + err = permissionCheck(loggedInUser, apiKey, dbName, permName) + if err != nil { + jsonErr(w, err.Error(), http.StatusUnauthorized) + return + } + // Get Minio bucket bucket, id, _, err := com.MinioLocation(dbOwner, dbFolder, dbName, commitID, loggedInUser) if err != nil { @@ -344,3 +360,31 @@ func logReq(r *http.Request, loggedInUser string) { loggedInUser, time.Now().Format(time.RFC3339Nano), r.Method, r.URL, r.Proto, r.Referer(), r.Header.Get("User-Agent")) } + +// permissionCheck checks if a given incoming api key request is allowed to run on the requested database +func permissionCheck(loggedInUser, apiKey, dbName string, permName com.APIPermission) (err error) { + // Retrieve the permission details for the api key + var apiDetails com.APIKey + apiDetails, err = com.APIKeyPerms(loggedInUser, apiKey) + if err != nil { + return + } + + // If the user authenticated using their client certificate, we skip the permission checks as they have + // full access to all their databases + if apiKey != "clientcert" { + // Ensure the database name matches + // TODO: We probably need a special case for handling the Databases(), Releases(), and Tags() functions. + if apiDetails.Database != dbName && apiDetails.Database != "" { // Empty string in the database means "All databases allowed" + err = fmt.Errorf("Permission denied") + return + } + + // Ensure the required function permission has been granted + if apiDetails.Permissions[permName] != true { + err = fmt.Errorf("Permission denied") + return + } + } + return +} diff --git a/common/postgresql.go b/common/postgresql.go index 4bfef2398..b8491555a 100644 --- a/common/postgresql.go +++ b/common/postgresql.go @@ -141,6 +141,27 @@ func APIKeyDBSave(loggedInUser, apiKey, dbName string, allDB bool) error { return nil } +// APIKeyPerms returns the permission details of an API key +func APIKeyPerms(loggedInUser, apiKey string) (apiDetails APIKey, err error) { + // TODO: Make sure this query gets the right data (haven't tested it yet) + // TODO: Add the remaining API key details + dbQuery := ` + WITH key_info AS ( + SELECT key_id + FROM api_keys + WHERE key = $1 + ) + SELECT db.dbname, api.permissions + FROM sqlite_databases db, api_permissions api, key_info + WHERE api.db_id = db.db_id + AND api_permissions.key_id = key_info.key_id` + err = pdb.QueryRow(dbQuery, apiKey).Scan(&apiDetails.Database, &apiDetails.Permissions) + if err != nil { + log.Printf("Fetching API key database and permissions failed: %v\n", err) + } + return +} + // APIKeyPermSave updates the permissions for an API key func APIKeyPermSave(loggedInUser, apiKey string, perm APIPermission, value bool) error { // Data structure for holding the API permission values @@ -2034,27 +2055,6 @@ func GetBranches(dbOwner, dbFolder, dbName string) (branches map[string]BranchEn return branches, nil } -// GetAPIKey returns the details for an API key -func GetAPIKey(apiKey string) (apiDetails APIKey, err error) { - // TODO: Make sure this query gets the right data (haven't tested it yet) - // TODO: Add the remaining API key details - dbQuery := ` - WITH key_info AS ( - SELECT key_id - FROM api_keys - WHERE key = $1 - ) - SELECT db.dbname, api.permissions - FROM sqlite_databases db, api_permissions api, key_info - WHERE api.db_id = db.db_id - AND api_permissions.key_id = key_info.key_id` - err = pdb.QueryRow(dbQuery, apiKey).Scan(&apiDetails.Database, &apiDetails.Permissions) - if err != nil { - log.Printf("Fetching API key database and permissions failed: %v\n", err) - } - return -} - // GetAPIKeys returns the list of API keys for a user func GetAPIKeys(user string) (apiKeys map[string]APIKey, err error) { // TODO: Do this as one query, probably using an outer join diff --git a/webui/templates/preferences.html b/webui/templates/preferences.html index da51e2fab..afa54d671 100644 --- a/webui/templates/preferences.html +++ b/webui/templates/preferences.html @@ -286,6 +286,11 @@

 {{ statusMessage }}

$scope.changeDB(apiKey, newDB); } + // TODO: Add the API key revoking piece(s) + $scope.apiKeyRevoke = function(apiKey) { + // ... + } + // If the supplied display name is blank, we set a placeholder value instead $scope.FullName = ""; $scope.NamePlaceholder = ""; From f9b4a3fb20d8ead6eace5f953f1facf860d6a787 Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Mon, 14 Jun 2021 02:13:09 +1000 Subject: [PATCH 19/23] Trivial wording tweak Things aren't just "the server" any more, so the old wording is kind of outdated. ;) --- webui/templates/preferences.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webui/templates/preferences.html b/webui/templates/preferences.html index afa54d671..ed996e521 100644 --- a/webui/templates/preferences.html +++ b/webui/templates/preferences.html @@ -242,7 +242,7 @@

 {{ statusMessage }}

// Database names $scope.dbList = [[ .DBNames ]]; - // Send the changed database selection to the server + // Send the changed database selection to the backend $scope.changeDB = function(apiKey, newDB) { // Work out the "all databases" value to send let allDBs = false; @@ -250,7 +250,7 @@

 {{ statusMessage }}

allDBs = true; } - // Send the new database info to the server + // Send the new database info to the backend $http({ method: "POST", url: "/x/apikeydbupdate/", @@ -343,7 +343,7 @@

 {{ statusMessage }}

$scope.statusMessage = ""; $scope.statusMessageColour = "red"; $scope.apiKeyGen = function() { - // Call the server to generate a new API key + // Call the backend to generate a new API key $http.get("/x/apikeygen").then( function success(response) { $scope.statusMessageColour = "green"; From 376ebb421465a82b990c64598514ce6023747036 Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Sat, 19 Jun 2021 17:54:30 +1000 Subject: [PATCH 20/23] WIP. Fix SQL query for retrieving permission data --- common/postgresql.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/postgresql.go b/common/postgresql.go index b8491555a..344333c8f 100644 --- a/common/postgresql.go +++ b/common/postgresql.go @@ -151,10 +151,10 @@ func APIKeyPerms(loggedInUser, apiKey string) (apiDetails APIKey, err error) { FROM api_keys WHERE key = $1 ) - SELECT db.dbname, api.permissions + SELECT db.db_name, api.permissions FROM sqlite_databases db, api_permissions api, key_info WHERE api.db_id = db.db_id - AND api_permissions.key_id = key_info.key_id` + AND api.key_id = key_info.key_id` err = pdb.QueryRow(dbQuery, apiKey).Scan(&apiDetails.Database, &apiDetails.Permissions) if err != nil { log.Printf("Fetching API key database and permissions failed: %v\n", err) From 9df8d5ea0ddfac90d8a731cb3d5e8004a58ac262 Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Sat, 19 Jun 2021 22:03:50 +1000 Subject: [PATCH 21/23] WIP. Remove some code duplication, make progress with fetching API key permissions --- common/postgresql.go | 97 +++++++++++++++++++++++--------------------- common/util.go | 22 ++++++++++ webui/main.go | 18 +------- 3 files changed, 74 insertions(+), 63 deletions(-) diff --git a/common/postgresql.go b/common/postgresql.go index 344333c8f..7df38f1fc 100644 --- a/common/postgresql.go +++ b/common/postgresql.go @@ -143,21 +143,57 @@ func APIKeyDBSave(loggedInUser, apiKey, dbName string, allDB bool) error { // APIKeyPerms returns the permission details of an API key func APIKeyPerms(loggedInUser, apiKey string) (apiDetails APIKey, err error) { - // TODO: Make sure this query gets the right data (haven't tested it yet) - // TODO: Add the remaining API key details + // TODO: The multiple SQL queries below are probably do-able with a single query, except I'm not real awake atm. + // So will just make it work like this for now. + var keyID pgx.NullInt64 dbQuery := ` - WITH key_info AS ( - SELECT key_id - FROM api_keys - WHERE key = $1 - ) - SELECT db.db_name, api.permissions - FROM sqlite_databases db, api_permissions api, key_info - WHERE api.db_id = db.db_id - AND api.key_id = key_info.key_id` - err = pdb.QueryRow(dbQuery, apiKey).Scan(&apiDetails.Database, &apiDetails.Permissions) + SELECT key_id + FROM api_keys + WHERE key = $1` + err = pdb.QueryRow(dbQuery, apiKey).Scan(&keyID) if err != nil { - log.Printf("Fetching API key database and permissions failed: %v\n", err) + log.Printf("Fetching API key ID failed: %v\n", err) + } + + var dbID pgx.NullInt64 + dbQuery = ` + SELECT db_id, permissions + FROM api_permissions + WHERE key_id = $1` + err = pdb.QueryRow(dbQuery, keyID).Scan(&dbID, &apiDetails.Permissions) + if err != nil && err != pgx.ErrNoRows { + log.Printf("Fetching database ID and permissions failed: %v\n", err) + return + } + + // If no results were returned, it means no permissions have been set for this api key yet, so use the default of + // "everything enabled" + if err == pgx.ErrNoRows { + // Return "All databases" and "all permissions enabled" + apiDetails.Permissions = APIKeyPermDefaults() + err = nil + return + } + + // If a database ID was returned then look up the database name + if dbID.Valid { + dbQuery = ` + SELECT db.db_name + FROM sqlite_databases db + WHERE db.db_id = $1` + err = pdb.QueryRow(dbQuery, dbID).Scan(&apiDetails.Database) + if err != nil { + log.Printf("Fetching database name failed: %v\n", err) + } + } + + // Just for safety, in case something weird is happening + if apiDetails.Permissions == nil { + // Not sure this case would ever be hit? It would mean there is a database assigned to the api key, but no + // permissions. In theory, that shouldn't be able to happen. Maybe set some defaults here, just in case? + apiDetails.Permissions = APIKeyPermDefaults() + log.Printf("Unexpected weirdness with API key permissions. The api key '%v' has a database set, but no permissions\n", apiKey) + return } return } @@ -195,22 +231,7 @@ func APIKeyPermSave(loggedInUser, apiKey string, perm APIPermission, value bool) // If there isn't any permission data for the API key, it means the key was generated before permissions were // available. So, we default to "all databases" and "all permissions are turned on" if len(permData) == 0 { - permData[APIPermBranches] = true - permData[APIPermColumns] = true - permData[APIPermCommits] = true - permData[APIPermDatabases] = true - permData[APIPermDelete] = true - permData[APIPermDiff] = true - permData[APIPermDownload] = true - permData[APIPermIndexes] = true - permData[APIPermMetadata] = true - permData[APIPermQuery] = true - permData[APIPermReleases] = true - permData[APIPermTables] = true - permData[APIPermTags] = true - permData[APIPermUpload] = true - permData[APIPermViews] = true - permData[APIPermWebpage] = true + permData = APIKeyPermDefaults() } // Incorporate the updated permission data from the user @@ -2118,23 +2139,7 @@ func GetAPIKeys(user string) (apiKeys map[string]APIKey, err error) { // If there aren't (yet) any permissions saved for the api key, we enable everything by default if err == pgx.ErrNoRows || perms == nil { - perms = make(map[APIPermission]bool) - perms[APIPermBranches] = true - perms[APIPermColumns] = true - perms[APIPermCommits] = true - perms[APIPermDatabases] = true - perms[APIPermDelete] = true - perms[APIPermDiff] = true - perms[APIPermDownload] = true - perms[APIPermIndexes] = true - perms[APIPermMetadata] = true - perms[APIPermQuery] = true - perms[APIPermReleases] = true - perms[APIPermTables] = true - perms[APIPermTags] = true - perms[APIPermUpload] = true - perms[APIPermViews] = true - perms[APIPermWebpage] = true + perms = APIKeyPermDefaults() err = nil } diff --git a/common/util.go b/common/util.go index 80adcd171..1588a90c0 100644 --- a/common/util.go +++ b/common/util.go @@ -423,6 +423,28 @@ func AddDatabase(loggedInUser, dbOwner, dbFolder, dbName string, createBranch bo return numBytes, c.ID, sha, nil } +// APIKeyPermDefaults creates a map with the default API key permissions +func APIKeyPermDefaults() (Perms map[APIPermission]bool) { + Perms = make(map[APIPermission]bool) + Perms[APIPermBranches] = true + Perms[APIPermColumns] = true + Perms[APIPermCommits] = true + Perms[APIPermDatabases] = true + Perms[APIPermDelete] = true + Perms[APIPermDiff] = true + Perms[APIPermDownload] = true + Perms[APIPermIndexes] = true + Perms[APIPermMetadata] = true + Perms[APIPermQuery] = true + Perms[APIPermReleases] = true + Perms[APIPermTables] = true + Perms[APIPermTags] = true + Perms[APIPermUpload] = true + Perms[APIPermViews] = true + Perms[APIPermWebpage] = true + return +} + // CommitPublicFlag returns the public flag of a given commit func CommitPublicFlag(loggedInUser, dbOwner, dbFolder, dbName, commitID string) (public bool, err error) { var DB SQLiteDBinfo diff --git a/webui/main.go b/webui/main.go index 995a13f1e..b1ba32bb2 100644 --- a/webui/main.go +++ b/webui/main.go @@ -171,23 +171,7 @@ func apiKeyGenHandler(w http.ResponseWriter, r *http.Request) { log.Printf("New API key created for user '%s', key: '%s'\n", loggedInUser, key) // Create a structure holding the default permissions - permData := make(map[com.APIPermission]bool) - permData[com.APIPermBranches] = true - permData[com.APIPermColumns] = true - permData[com.APIPermCommits] = true - permData[com.APIPermDatabases] = true - permData[com.APIPermDelete] = true - permData[com.APIPermDiff] = true - permData[com.APIPermDownload] = true - permData[com.APIPermIndexes] = true - permData[com.APIPermMetadata] = true - permData[com.APIPermQuery] = true - permData[com.APIPermReleases] = true - permData[com.APIPermTables] = true - permData[com.APIPermTags] = true - permData[com.APIPermUpload] = true - permData[com.APIPermViews] = true - permData[com.APIPermWebpage] = true + permData := com.APIKeyPermDefaults() // Return the API key to the caller d := com.APIKey{ From d71bb03be09cf5efb7c3cb7f93908381ff697436 Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Fri, 25 Jun 2021 02:03:04 +1000 Subject: [PATCH 22/23] WIP. Stub test go file, for fleshing out once we have the Docker bit ready Not feeling well atm, so prob not doing anything more than this tonight. --- api/main_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 api/main_test.go diff --git a/api/main_test.go b/api/main_test.go new file mode 100644 index 000000000..18a6a8833 --- /dev/null +++ b/api/main_test.go @@ -0,0 +1,12 @@ +package main + +// Initial tests for the API functions +// * These are designed to run in a Docker container, with the DBHub.io +// daemons running in the same container (listening on localhost), with +// the testing database loaded + +import "testing" + +func TestSomething(t *testing.T) { + // TBD +} \ No newline at end of file From a1a8af319169aeae8bfe0f8ed06aed66f319f051 Mon Sep 17 00:00:00 2001 From: Justin Clift Date: Sat, 10 Jul 2021 19:55:48 +1000 Subject: [PATCH 23/23] WIP. Some database schema updates. --- database/dbhub.sql | 70 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 13 deletions(-) diff --git a/database/dbhub.sql b/database/dbhub.sql index dfb8d3165..f138005a3 100644 --- a/database/dbhub.sql +++ b/database/dbhub.sql @@ -2,8 +2,8 @@ -- PostgreSQL database dump -- --- Dumped from database version 10.12 --- Dumped by pg_dump version 10.12 +-- Dumped from database version 12.6 +-- Dumped by pg_dump version 12.6 SET statement_timeout = 0; SET lock_timeout = 0; @@ -17,32 +17,32 @@ SET client_min_messages = warning; SET row_security = off; -- --- Name: permissions; Type: TYPE; Schema: public; Owner: - +-- Name: jsquery; Type: EXTENSION; Schema: -; Owner: - -- -CREATE TYPE public.permissions AS ENUM ( - 'r', - 'rw' -); +CREATE EXTENSION IF NOT EXISTS jsquery WITH SCHEMA public; -- --- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: - +-- Name: EXTENSION jsquery; Type: COMMENT; Schema: -; Owner: - -- -CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; +COMMENT ON EXTENSION jsquery IS 'data type for jsonb inspection'; -- --- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: - +-- Name: permissions; Type: TYPE; Schema: public; Owner: - -- -COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; +CREATE TYPE public.permissions AS ENUM ( + 'r', + 'rw' +); SET default_tablespace = ''; -SET default_with_oids = false; +SET default_table_access_method = heap; -- -- Name: api_keys; Type: TABLE; Schema: public; Owner: - @@ -75,6 +75,18 @@ CREATE SEQUENCE public.api_keys_key_id_seq ALTER SEQUENCE public.api_keys_key_id_seq OWNED BY public.api_keys.key_id; +-- +-- Name: api_permissions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.api_permissions ( + key_id bigint, + user_id bigint, + db_id bigint, + permissions jsonb +); + + -- -- Name: database_downloads; Type: TABLE; Schema: public; Owner: - -- @@ -135,7 +147,7 @@ CREATE TABLE public.database_licences ( display_order integer, lic_id integer NOT NULL, full_name text, - file_format text DEFAULT 'text'::text NOT NULL + file_format text ); @@ -653,6 +665,14 @@ ALTER TABLE ONLY public.users ALTER COLUMN user_id SET DEFAULT nextval('public.u ALTER TABLE ONLY public.vis_query_runs ALTER COLUMN query_run_id SET DEFAULT nextval('public.vis_query_runs_query_run_id_seq'::regclass); +-- +-- Name: api_keys api_keys_key_id; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_keys + ADD CONSTRAINT api_keys_key_id UNIQUE (key_id); + + -- -- Name: api_keys api_keys_pk; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -962,6 +982,30 @@ ALTER TABLE ONLY public.api_keys ADD CONSTRAINT api_keys_users_user_id_fk FOREIGN KEY (user_id) REFERENCES public.users(user_id) ON UPDATE CASCADE ON DELETE SET NULL; +-- +-- Name: api_permissions api_permissions_api_keys_key_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_permissions + ADD CONSTRAINT api_permissions_api_keys_key_id_fk FOREIGN KEY (key_id) REFERENCES public.api_keys(key_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: api_permissions api_permissions_sqlite_databases_db_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_permissions + ADD CONSTRAINT api_permissions_sqlite_databases_db_id_fk FOREIGN KEY (db_id) REFERENCES public.sqlite_databases(db_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: api_permissions api_permissions_users_user_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_permissions + ADD CONSTRAINT api_permissions_users_user_id_fk FOREIGN KEY (user_id) REFERENCES public.users(user_id) ON UPDATE CASCADE ON DELETE CASCADE; + + -- -- Name: database_downloads database_downloads_db_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --