Skip to content

Commit

Permalink
Substitute variables in path names of template repos too (#25294)
Browse files Browse the repository at this point in the history
### Summary

Extend the template variable substitution to replace file paths. This
can be helpful for setting up log files & directories that should match
the repository name.

### PR Changes

 - Move files matching glob pattern when setting up repos from template
- For security, added ~escaping~ sanitization for cross-platform support
and to prevent directory traversal (thanks @silverwind for the
reference)
 - Added unit testing for escaping function 
- Fixed the integration tests for repo template generation by passing
the repo_template_id
- Updated the integration testfiles to add some variable substitution &
assert the outputs

I had to fix the existing repo template integration test and extend it
to add a check for variable substitutions.

Example:

![image](https://github.com/go-gitea/gitea/assets/12700993/621feb09-0ef3-460e-afa8-da74cd84fa4e)
  • Loading branch information
kdumontnu committed Jun 20, 2023
1 parent e50c3e8 commit 8220e50
Show file tree
Hide file tree
Showing 13 changed files with 77 additions and 10 deletions.
2 changes: 2 additions & 0 deletions docs/content/doc/usage/template-repositories.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ a/b/c/d.json

In any file matched by the above globs, certain variables will be expanded.

Matching filenames and paths can also be expanded, and are conservatively sanitized to support cross-platform filesystems.

All variables must be of the form `$VAR` or `${VAR}`. To escape an expansion, use a double `$$`, such as `$$VAR` or `$${VAR}`

| Variable | Expands To | Transformable |
Expand Down
32 changes: 30 additions & 2 deletions modules/repository/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -48,7 +49,7 @@ var defaultTransformers = []transformer{
{Name: "TITLE", Transform: util.ToTitleCase},
}

func generateExpansion(src string, templateRepo, generateRepo *repo_model.Repository) string {
func generateExpansion(src string, templateRepo, generateRepo *repo_model.Repository, sanitizeFileName bool) string {
expansions := []expansion{
{Name: "REPO_NAME", Value: generateRepo.Name, Transformers: defaultTransformers},
{Name: "TEMPLATE_NAME", Value: templateRepo.Name, Transformers: defaultTransformers},
Expand All @@ -74,6 +75,9 @@ func generateExpansion(src string, templateRepo, generateRepo *repo_model.Reposi

return os.Expand(src, func(key string) string {
if expansion, ok := expansionMap[key]; ok {
if sanitizeFileName {
return fileNameSanitize(expansion)
}
return expansion
}
return key
Expand Down Expand Up @@ -191,10 +195,24 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
}

if err := os.WriteFile(path,
[]byte(generateExpansion(string(content), templateRepo, generateRepo)),
[]byte(generateExpansion(string(content), templateRepo, generateRepo, false)),
0o644); err != nil {
return err
}

substPath := filepath.FromSlash(filepath.Join(tmpDirSlash,
generateExpansion(base, templateRepo, generateRepo, true)))

// Create parent subdirectories if needed or continue silently if it exists
if err := os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil {
return err
}

// Substitute filename variables
if err := os.Rename(path, substPath); err != nil {
return err
}

break
}
}
Expand Down Expand Up @@ -353,3 +371,13 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ

return generateRepo, nil
}

// Sanitize user input to valid OS filenames
//
// Based on https://github.com/sindresorhus/filename-reserved-regex
// Adds ".." to prevent directory traversal
func fileNameSanitize(s string) string {
re := regexp.MustCompile(`(?i)\.\.|[<>:\"/\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`)

return strings.TrimSpace(re.ReplaceAllString(s, "_"))
}
11 changes: 11 additions & 0 deletions modules/repository/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,14 @@ func TestGiteaTemplate(t *testing.T) {
})
}
}

func TestFileNameSanitize(t *testing.T) {
assert.Equal(t, "test_CON", fileNameSanitize("test_CON"))
assert.Equal(t, "test CON", fileNameSanitize("test CON "))
assert.Equal(t, "__traverse__", fileNameSanitize("../traverse/.."))
assert.Equal(t, "http___localhost_3003_user_test.git", fileNameSanitize("http://localhost:3003/user/test.git"))
assert.Equal(t, "_", fileNameSanitize("CON"))
assert.Equal(t, "_", fileNameSanitize("con"))
assert.Equal(t, "_", fileNameSanitize("\u0000"))
assert.Equal(t, "目标", fileNameSanitize("目标"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
x��AJ�0�a�9�\@Ij2��C�w�"��h�i���޷q���~�{_ � ����+c�)M���* rȉSD&��M��*�l�pm*��5fE_�P�8���D�QC�ɕa�o?��+\>���f۸����O��HH9G"x��{w��;��8
i�s�������0��9�/�IH
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
aacbdfe9e1c4b47f60abe81849045fa4e96f1d75
2a83b349fa234131fc5db6f2a0498d3f4d3d6038
38 changes: 31 additions & 7 deletions tests/integration/repo_generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/tests"

"github.com/stretchr/testify/assert"
)

func testRepoGenerate(t *testing.T, session *TestSession, templateOwnerName, templateRepoName, generateOwnerName, generateRepoName string) *httptest.ResponseRecorder {
func testRepoGenerate(t *testing.T, session *TestSession, templateID, templateOwnerName, templateRepoName, generateOwnerName, generateRepoName string) *httptest.ResponseRecorder {
generateOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: generateOwnerName})

// Step0: check the existence of the generated repo
Expand All @@ -41,28 +43,50 @@ func testRepoGenerate(t *testing.T, session *TestSession, templateOwnerName, tem
_, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", generateOwner.ID)).Attr("data-value")
assert.True(t, exists, fmt.Sprintf("Generate owner '%s' is not present in select box", generateOwnerName))
req = NewRequestWithValues(t, "POST", link, map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"uid": fmt.Sprintf("%d", generateOwner.ID),
"repo_name": generateRepoName,
"git_content": "true",
"_csrf": htmlDoc.GetCSRF(),
"uid": fmt.Sprintf("%d", generateOwner.ID),
"repo_name": generateRepoName,
"repo_template": templateID,
"git_content": "true",
})
session.MakeRequest(t, req, http.StatusSeeOther)

// Step4: check the existence of the generated repo
req = NewRequestf(t, "GET", "/%s/%s", generateOwnerName, generateRepoName)
session.MakeRequest(t, req, http.StatusOK)

// Step5: check substituted values in Readme
req = NewRequestf(t, "GET", "/%s/%s/raw/branch/master/README.md", generateOwnerName, generateRepoName)
resp = session.MakeRequest(t, req, http.StatusOK)
body := fmt.Sprintf(`# %s Readme
Owner: %s
Link: /%s/%s
Clone URL: %s%s/%s.git`,
generateRepoName,
strings.ToUpper(generateOwnerName),
generateOwnerName,
generateRepoName,
setting.AppURL,
generateOwnerName,
generateRepoName)
assert.Equal(t, body, resp.Body.String())

// Step6: check substituted values in substituted file path ${REPO_NAME}
req = NewRequestf(t, "GET", "/%s/%s/raw/branch/master/%s.log", generateOwnerName, generateRepoName, generateRepoName)
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, generateRepoName, resp.Body.String())

return resp
}

func TestRepoGenerate(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
testRepoGenerate(t, session, "user27", "template1", "user1", "generated1")
testRepoGenerate(t, session, "44", "user27", "template1", "user1", "generated1")
}

func TestRepoGenerateToOrg(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
testRepoGenerate(t, session, "user27", "template1", "user2", "generated2")
testRepoGenerate(t, session, "44", "user27", "template1", "user2", "generated2")
}

0 comments on commit 8220e50

Please sign in to comment.