Skip to content

Commit 4711ba1

Browse files
authored
Remove backup api at /database/backup; change method from GET to POST (#33)
* Remove backup api at /database/backup; change method from GET to POST on /api/v3/plugins/database/backup * Inline H2BackupController::doExport * Remove "done: " prefix from backup response so its easier to use the response in scripts; add tests around authorized and unauthorized requests and using a non-default file name
1 parent ad16096 commit 4711ba1

File tree

5 files changed

+169
-38
lines changed

5 files changed

+169
-38
lines changed

README.MD

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ You can pass an optional argument `dest` that references the destination file pa
4040
For example calling `http://YOUR_GITBUCKET/api/v3/plugins/database/backup?dest=/var/backups/gitbucket.zip` will do an H2 backup of the gitbucket database into the file `/var/backups/gitbucket.zip`.
4141
Since `1.3.0`, the _dest_ parameter can denote a relative file path, in this case the file will be created relatively to `GITBUCKET_DATA`.
4242

43-
On success, you will receive a `HTTP 200` answer with a body containing `done: FULL_PATH_OF_BACKUP_FILE`.
43+
On success, you will receive a `HTTP 200` answer with a `text/plain` body containing `FULL_PATH_OF_BACKUP_FILE`.
4444

4545
### HTTP API Authorization
4646

@@ -83,6 +83,11 @@ sbt clean assembly
8383

8484
## Release Notes
8585

86+
### Unreleased
87+
- remove backup api at GET /database/backup
88+
- change method from GET to POST on /api/v3/plugins/database/backup
89+
- backup endpoint is secure by default, and requires an api token for a user with admin rights
90+
8691
### 1.9.0
8792
- compatibility with GitBucket 4.35.x
8893

@@ -102,7 +107,6 @@ sbt clean assembly
102107

103108
- compatibility with GitBucket 4.10, scala 2.12 [#20](https://github.com/gitbucket-plugins/gitbucket-h2-backup-plugin/issues/20)
104109
- allow to secure `database/backup` endpoint [#1](https://github.com/gitbucket-plugins/gitbucket-h2-backup-plugin/issues/1),[#19](https://github.com/gitbucket-plugins/gitbucket-h2-backup-plugin/issues/19)
105-
see [Securing backup endpoint](#securing-backup-endpoint) paragraph
106110

107111
### 1.3.0
108112

@@ -122,15 +126,3 @@ sbt clean assembly
122126

123127
- introduce gitbucket-h2-backup-plugin
124128
- allows to backup h2 database via a live dump
125-
126-
## Securing backup endpoint
127-
128-
In version 1.4.0, it is possible to secure the `database/backup` endpoint:
129-
130-
- launch GitBucket with System property _secure.backup_ set to true (for example `-Dsecure.backup=true` on the command line)
131-
- due to actual limitations of GitBucket & plugins security, once the previous setting is activated,
132-
a call to `http://YOUR_GITBUCKET/database/backup` will be temporary redirected `http://YOUR_GITBUCKET/api/v3/plugins/database/backup`.
133-
You have to follow this temporary redirection.
134-
- if you call the endpoint using _httpie_, use the `--follow` parameter
135-
- this secured endpoint route is TEMPORARY you should not call it directly.
136-
If you do think that it will change in the future when GitBucket will support secured routes for plugins.

build.sbt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
val ScalatraVersion = "2.7.1"
2+
13
organization := "fr.brouillard.gitbucket"
24
name := "gitbucket-h2-backup-plugin"
35
version := "1.9.0"
46
scalaVersion := "2.13.3"
57
gitbucketVersion := "4.35.0"
68
scalacOptions += "-deprecation"
9+
10+
libraryDependencies ++= Seq(
11+
"org.scalatest" %% "scalatest-funspec" % "3.2.3" % "test",
12+
"org.scalatest" %% "scalatest-funsuite" % "3.2.3" % "test",
13+
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
14+
)

src/main/scala/fr/brouillard/gitbucket/h2/controller/H2BackupController.scala

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,34 @@ package fr.brouillard.gitbucket.h2.controller
22

33
import java.io.File
44
import java.util.Date
5-
65
import fr.brouillard.gitbucket.h2._
7-
6+
import fr.brouillard.gitbucket.h2.controller.H2BackupController.{defaultBackupFileName, doBackup}
87
import gitbucket.core.controller.ControllerBase
8+
import gitbucket.core.model.Account
99
import gitbucket.core.util.AdminAuthenticator
1010
import gitbucket.core.util.Directory._
1111
import gitbucket.core.servlet.Database
12-
13-
import org.scalatra.Ok
12+
import org.scalatra.{ActionResult, Ok, Params}
1413
import org.slf4j.LoggerFactory
15-
1614
import org.scalatra.forms._
1715

16+
object H2BackupController {
17+
def defaultBackupFileName(): String = {
18+
val format = new java.text.SimpleDateFormat("yyyy-MM-dd_HH-mm")
19+
"gitbucket-db-" + format.format(new Date()) + ".zip"
20+
}
21+
22+
def doBackup(exportDatabase: File => Unit, loginAccount: Option[Account], params: Params): ActionResult = {
23+
loginAccount match {
24+
case Some(x) if x.isAdmin =>
25+
val filePath: String = params.getOrElse("dest", defaultBackupFileName())
26+
exportDatabase(new File(filePath))
27+
Ok(filePath, Map("Content-Type" -> "text/plain"))
28+
case _ => org.scalatra.Unauthorized()
29+
}
30+
}
31+
}
32+
1833
class H2BackupController extends ControllerBase with AdminAuthenticator {
1934
private val logger = LoggerFactory.getLogger(classOf[H2BackupController])
2035

@@ -27,7 +42,7 @@ class H2BackupController extends ControllerBase with AdminAuthenticator {
2742
// private val defaultBackupFile:String = new File(GitBucketHome, "gitbucket-database-backup.zip").toString;
2843

2944
def exportDatabase(exportFile: File): Unit = {
30-
val destFile = if (exportFile.isAbsolute()) exportFile else new File(GitBucketHome + "/backup", exportFile.toString)
45+
val destFile = if (exportFile.isAbsolute) exportFile else new File(GitBucketHome + "/backup", exportFile.toString)
3146

3247
val session = Database.getSession(request)
3348
val conn = session.conn
@@ -42,26 +57,23 @@ class H2BackupController extends ControllerBase with AdminAuthenticator {
4257
})
4358

4459
get("/api/v3/plugins/database/backup") {
45-
context.loginAccount match {
46-
case Some(x) if (x.isAdmin) => doExport()
47-
case _ => org.scalatra.Unauthorized()
48-
}
60+
doBackupMoved()
61+
}
62+
63+
post("/api/v3/plugins/database/backup") {
64+
doBackup(exportDatabase, context.loginAccount, params)
4965
}
5066

67+
// Legacy api that was insecure/open by default
5168
get("/database/backup") {
52-
if (sys.props.get("secure.backup") exists (_ equalsIgnoreCase "true"))
53-
org.scalatra.TemporaryRedirect("/api/v3/plugins/database/backup?dest=" + params.getOrElse("dest", defaultBackupFileName()))
54-
else {
55-
doExport()
56-
}
69+
doBackupMoved()
5770
}
5871

59-
private def doExport(): Unit = {
60-
val filePath: String = params.getOrElse("dest", defaultBackupFileName())
61-
exportDatabase(new File(filePath))
62-
Ok("done: " + filePath)
72+
private def doBackupMoved(): ActionResult = {
73+
org.scalatra.MethodNotAllowed("This has moved to POST /api/v3/plugins/database/backup")
6374
}
6475

76+
// Responds to a form post from a web page
6577
post("/database/backup", backupForm) { form: BackupForm =>
6678
exportDatabase(new File(form.destFile))
6779
val msg: String = "H2 Database has been exported to '" + form.destFile + "'."
@@ -70,8 +82,4 @@ class H2BackupController extends ControllerBase with AdminAuthenticator {
7082
redirect("/admin/h2backup")
7183
}
7284

73-
private def defaultBackupFileName(): String = {
74-
val format = new java.text.SimpleDateFormat("yyyy-MM-dd_HH-mm")
75-
"gitbucket-db-" + format.format(new Date()) + ".zip"
76-
}
7785
}

src/main/twirl/fr/brouillard/gitbucket/h2/export.scala.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<fieldset>
1616
<label><span class="strong">H2 Database Backup File:</span></label>
1717
<p class="muted">
18-
Allows to export/backup the H2 database content. The same action can be achieved via an HTTP GET call to @path/database/backup .
18+
Allows to export/backup the H2 database content. The same action can be achieved via an HTTP POST call to @path/api/v3/plugins/database/backup .
1919
</p>
2020
backup/<input type="text" name="dest" value="@dest" style="width: 400px" />
2121
</fieldset>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package fr.brouillard.gitbucket.h2.controller
2+
3+
import gitbucket.core.model.Account
4+
import gitbucket.core.servlet.ApiAuthenticationFilter
5+
import org.scalatest.funsuite.AnyFunSuite
6+
import org.scalatest.matchers.should.Matchers.{convertToAnyShouldWrapper, equal}
7+
import org.scalatra.{Ok, Params, ScalatraParams}
8+
import org.scalatra.test.scalatest.ScalatraFunSuite
9+
10+
import java.io.File
11+
import java.util.Date
12+
13+
class H2BackupControllerTests extends ScalatraFunSuite {
14+
addFilter(classOf[ApiAuthenticationFilter], path="/api/*")
15+
addFilter(classOf[H2BackupController], "/*")
16+
17+
test("get database backup api") {
18+
get("/api/v3/plugins/database/backup") {
19+
status should equal (405)
20+
body should include ("This has moved")
21+
}
22+
}
23+
24+
test("get database backup legacy") {
25+
get("/database/backup") {
26+
status should equal (405)
27+
body should include ("This has moved")
28+
}
29+
}
30+
31+
test("post database backup without credentials is unauthorized") {
32+
post("/api/v3/plugins/database/backup") {
33+
status should equal (401)
34+
}
35+
}
36+
37+
}
38+
39+
class H2BackupControllerObjectTests extends AnyFunSuite {
40+
private def assertDefaultFileName(name: String): Unit = {
41+
assert(name.startsWith("gitbucket-db"))
42+
assert(name.endsWith(".zip"))
43+
}
44+
45+
private def buildAccount(isAdmin: Boolean) = {
46+
Account(
47+
userName = "a",
48+
fullName = "b",
49+
mailAddress = "c",
50+
password = "d",
51+
isAdmin = isAdmin,
52+
url = None,
53+
registeredDate = new Date(),
54+
updatedDate = new Date(),
55+
lastLoginDate = None,
56+
image = None,
57+
isGroupAccount = false,
58+
isRemoved = false,
59+
description = None)
60+
}
61+
62+
test("generates default file name") {
63+
assertDefaultFileName(H2BackupController.defaultBackupFileName())
64+
}
65+
66+
test("post database backup with admin credentials is executed with default file name") {
67+
val account = buildAccount(true)
68+
val params: Params = new ScalatraParams(Map())
69+
70+
var executed = false;
71+
72+
val exportDatabase = (file: File) => {
73+
assert(!executed)
74+
assertDefaultFileName(file.getName)
75+
76+
executed = true
77+
}
78+
79+
val action = H2BackupController.doBackup(exportDatabase, Some(account), params)
80+
81+
assert(executed)
82+
assert(action.status == 200)
83+
84+
// Not JSON and not HTML
85+
assert(action.headers.get("Content-Type").contains("text/plain"))
86+
}
87+
88+
test("post database backup with admin credentials is executed with specific file name") {
89+
val fileName = "foo.zip"
90+
val account = buildAccount(true)
91+
val params: Params = new ScalatraParams(Map("dest" -> Seq(fileName)))
92+
93+
var executed = false;
94+
95+
val exportDatabase = (file: File) => {
96+
assert(!executed)
97+
assert(file.getName.equals(fileName))
98+
99+
executed = true
100+
}
101+
102+
val action = H2BackupController.doBackup(exportDatabase, Some(account), params)
103+
104+
assert(executed)
105+
assert(action.status == 200)
106+
107+
// Not JSON and not HTML
108+
assert(action.headers.get("Content-Type").contains("text/plain"))
109+
}
110+
111+
test("post database backup with unprivileged credentials is unauthorized") {
112+
val account = buildAccount(false)
113+
val params: Params = new ScalatraParams(Map())
114+
115+
val exportDatabase = (file: File) => {
116+
fail()
117+
}
118+
119+
val action = H2BackupController.doBackup(exportDatabase, Some(account), params)
120+
assert(action.status == 401)
121+
}
122+
123+
}

0 commit comments

Comments
 (0)