Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Allow-/Block-List for Migrate & Mirrors #13610

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7a083b9
add black list and white list support for migrating repositories
lunny Aug 30, 2019
3f2b34b
fix fmt
lunny Aug 30, 2019
ad5a226
fix lint
lunny Aug 30, 2019
7280964
fix vendor
lunny Aug 30, 2019
cdff51c
fix modules.txt
lunny Sep 7, 2019
ab362e5
Merge branch 'master' into allow-block_list_migrate-mirror_8040
6543 Nov 17, 2020
ec4dbc8
clean diff
6543 Nov 17, 2020
770dab6
specify log message
6543 Nov 17, 2020
05e45bf
use blocklist/allowlist
6543 Nov 17, 2020
a078736
allways use lowercase to match url
6543 Nov 17, 2020
dec70f1
Apply allow/block
6543 Nov 17, 2020
d2c1619
Merge branch 'master' into allow-block_list_migrate-mirror_8040
6543 Nov 18, 2020
840fc85
Settings: use existing "migrations" section
6543 Nov 21, 2020
af32a09
Merge branch 'master' into allow-block_list_migrate-mirror_8040
6543 Nov 21, 2020
9f5e0de
convert domains lower case
6543 Nov 23, 2020
3ede511
Merge branch 'master' into allow-block_list_migrate-mirror_8040
6543 Nov 23, 2020
37f45f4
dont store unused value
6543 Nov 23, 2020
e0934b8
Block private addresses for migration by default
6543 Nov 23, 2020
43982b4
fix lint
6543 Nov 23, 2020
9cd404a
Merge branch 'master' into allow-block_list_migrate-mirror_8040
6543 Nov 23, 2020
6725fd5
use proposed-upstream func to detect private IP addr
6543 Nov 24, 2020
6b8ecc4
a nit
6543 Nov 24, 2020
6ef7267
add own error for blocked migration, add tests, imprufe api
6543 Nov 24, 2020
c66cf83
fix test
6543 Nov 24, 2020
3b57ffc
fix-if-localhost-is-ipv4
6543 Nov 24, 2020
027f6f1
Merge branch 'master' into allow-block_list_migrate-mirror_8040
6543 Nov 24, 2020
ab53576
Merge branch 'master' into allow-block_list_migrate-mirror_8040
6543 Nov 26, 2020
8372dd1
rename error & error message
6543 Nov 26, 2020
b9dda50
Merge branch 'master' into allow-block_list_migrate-mirror_8040
6543 Nov 26, 2020
aa8ec6f
Merge branch 'master' into allow-block_list_migrate-mirror_8040
6543 Nov 28, 2020
bb5ce58
rename setting options
6543 Nov 28, 2020
9815e9d
Merge branch 'master' into allow-block_list_migrate-mirror_8040
6543 Nov 28, 2020
e13cd15
Apply suggestions from code review
zeripath Nov 28, 2020
6602023
Merge branch 'master' into allow-block_list_migrate-mirror_8040
6543 Nov 28, 2020
870ca5b
Merge branch 'master' into allow-block_list_migrate-mirror_8040
lunny Nov 28, 2020
e091333
Merge branch 'master' into allow-block_list_migrate-mirror_8040
zeripath Nov 28, 2020
7850e5e
Merge branch 'master' into allow-block_list_migrate-mirror_8040
6543 Nov 28, 2020
3c67197
Merge branch 'master' into allow-block_list_migrate-mirror_8040
techknowlogick Nov 28, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,14 @@ QUEUE_CONN_STR = "addrs=127.0.0.1:6379 db=0"
MAX_ATTEMPTS = 3
; Backoff time per http/https request retry (seconds)
RETRY_BACKOFF = 3
; Allowed domains for migrating, default is blank. Blank means everything will be allowed.
; Multiple domains could be separated by commas.
ALLOWED_DOMAINS =
; Blocklist for migrating, default is blank. Multiple domains could be separated by commas.
; When ALLOWED_DOMAINS is not blank, this option will be ignored.
BLOCKED_DOMAINS =
; Allow private addresses defined by RFC 1918, RFC 1122, RFC 4632 and RFC 4291 (false by default)
ALLOW_LOCALNETWORKS = false
6543 marked this conversation as resolved.
Show resolved Hide resolved

; default storage for attachments, lfs and avatars
[storage]
Expand Down
3 changes: 3 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,9 @@ Task queue configuration has been moved to `queue.task`. However, the below conf

- `MAX_ATTEMPTS`: **3**: Max attempts per http/https request on migrations.
- `RETRY_BACKOFF`: **3**: Backoff time per http/https request retry (seconds)
- `ALLOWED_DOMAINS`: **\<empty\>**: Domains allowlist for migrating repositories, default is blank. It means everything will be allowed. Multiple domains could be separated by commas.
- `BLOCKED_DOMAINS`: **\<empty\>**: Domains blocklist for migrating repositories, default is blank. Multiple domains could be separated by commas. When `ALLOWED_DOMAINS` is not blank, this option will be ignored.
- `ALLOW_LOCALNETWORKS`: **false**: Allow private addresses defined by RFC 1918, RFC 1122, RFC 4632 and RFC 4291

## Mirror (`mirror`)

Expand Down
3 changes: 3 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ IS_INPUT_FILE = false

- `MAX_ATTEMPTS`: **3**: 在迁移过程中的 http/https 请求重试次数。
- `RETRY_BACKOFF`: **3**: 等待下一次重试的时间,单位秒。
- `ALLOWED_DOMAINS`: **\<empty\>**: 迁移仓库的域名白名单,默认为空,表示允许从任意域名迁移仓库,多个域名用逗号分隔。
- `BLOCKED_DOMAINS`: **\<empty\>**: 迁移仓库的域名黑名单,默认为空,多个域名用逗号分隔。如果 `ALLOWED_DOMAINS` 不为空,此选项将会被忽略。
- `ALLOW_LOCALNETWORKS`: **false**: Allow private addresses defined by RFC 1918

## LFS (`lfs`)

Expand Down
12 changes: 11 additions & 1 deletion integrations/api_repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ func TestAPIRepoMigrate(t *testing.T) {
{ctxUserID: 2, userID: 1, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-bad", expectedStatus: http.StatusForbidden},
{ctxUserID: 2, userID: 3, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-org", expectedStatus: http.StatusCreated},
{ctxUserID: 2, userID: 6, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-bad-org", expectedStatus: http.StatusForbidden},
{ctxUserID: 2, userID: 3, cloneURL: "https://localhost:3000/user/test_repo.git", repoName: "local-ip", expectedStatus: http.StatusUnprocessableEntity},
{ctxUserID: 2, userID: 3, cloneURL: "https://10.0.0.1/user/test_repo.git", repoName: "private-ip", expectedStatus: http.StatusUnprocessableEntity},
}

defer prepareTestEnv(t)()
Expand All @@ -325,8 +327,16 @@ func TestAPIRepoMigrate(t *testing.T) {
if resp.Code == http.StatusUnprocessableEntity {
respJSON := map[string]string{}
DecodeJSON(t, resp, &respJSON)
if assert.Equal(t, "Remote visit addressed rate limitation.", respJSON["message"]) {
switch respJSON["message"] {
case "Remote visit addressed rate limitation.":
t.Log("test hit github rate limitation")
case "migrate from '10.0.0.1' is not allowed: the host resolve to a private ip address '10.0.0.1'":
assert.EqualValues(t, "private-ip", testCase.repoName)
case "migrate from 'localhost:3000' is not allowed: the host resolve to a private ip address '::1'",
"migrate from 'localhost:3000' is not allowed: the host resolve to a private ip address '127.0.0.1'":
assert.EqualValues(t, "local-ip", testCase.repoName)
default:
t.Errorf("unexpected error '%v' on url '%s'", respJSON["message"], testCase.cloneURL)
}
} else {
assert.EqualValues(t, testCase.expectedStatus, resp.Code)
Expand Down
23 changes: 23 additions & 0 deletions models/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,29 @@ func IsErrWontSign(err error) bool {
return ok
}

// ErrMigrationNotAllowed explains why a migration from an url is not allowed
type ErrMigrationNotAllowed struct {
Host string
NotResolvedIP bool
PrivateNet string
}

func (e *ErrMigrationNotAllowed) Error() string {
if e.NotResolvedIP {
return fmt.Sprintf("migrate from '%s' is not allowed: unknown hostname", e.Host)
}
if len(e.PrivateNet) != 0 {
return fmt.Sprintf("migrate from '%s' is not allowed: the host resolve to a private ip address '%s'", e.Host, e.PrivateNet)
}
return fmt.Sprintf("migrate from '%s is not allowed'", e.Host)
}

// IsErrMigrationNotAllowed checks if an error is a ErrMigrationNotAllowed
func IsErrMigrationNotAllowed(err error) bool {
_, ok := err.(*ErrMigrationNotAllowed)
return ok
}

// __________ .__
// \______ \____________ ____ ____ | |__
// | | _/\_ __ \__ \ / \_/ ___\| | \
Expand Down
46 changes: 46 additions & 0 deletions modules/matchlist/matchlist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package matchlist

import (
"strings"

"github.com/gobwas/glob"
)

// Matchlist represents a block or allow list
type Matchlist struct {
ruleGlobs []glob.Glob
}

// NewMatchlist creates a new block or allow list
func NewMatchlist(rules ...string) (*Matchlist, error) {
for i := range rules {
rules[i] = strings.ToLower(rules[i])
}
list := Matchlist{
ruleGlobs: make([]glob.Glob, 0, len(rules)),
}

for _, rule := range rules {
rg, err := glob.Compile(rule)
if err != nil {
return nil, err
}
list.ruleGlobs = append(list.ruleGlobs, rg)
}

return &list, nil
}

// Match will matches
func (b *Matchlist) Match(u string) bool {
for _, r := range b.ruleGlobs {
if r.Match(u) {
return true
}
}
return false
}
75 changes: 74 additions & 1 deletion modules/migrations/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ package migrations
import (
"context"
"fmt"
"net"
"net/url"
"strings"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/matchlist"
"code.gitea.io/gitea/modules/migrations/base"
"code.gitea.io/gitea/modules/setting"
)
Expand All @@ -20,19 +24,59 @@ type MigrateOptions = base.MigrateOptions

var (
factories []base.DownloaderFactory

allowList *matchlist.Matchlist
blockList *matchlist.Matchlist
)

// RegisterDownloaderFactory registers a downloader factory
func RegisterDownloaderFactory(factory base.DownloaderFactory) {
factories = append(factories, factory)
}

func isMigrateURLAllowed(remoteURL string) error {
u, err := url.Parse(strings.ToLower(remoteURL))
if err != nil {
return err
}

if strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https") {
if len(setting.Migrations.AllowedDomains) > 0 {
if !allowList.Match(u.Host) {
return &models.ErrMigrationNotAllowed{Host: u.Host}
}
} else {
if blockList.Match(u.Host) {
return &models.ErrMigrationNotAllowed{Host: u.Host}
}
}
}

if !setting.Migrations.AllowLocalNetworks {
addrList, err := net.LookupIP(strings.Split(u.Host, ":")[0])
if err != nil {
return &models.ErrMigrationNotAllowed{Host: u.Host, NotResolvedIP: true}
}
for _, addr := range addrList {
if isIPPrivate(addr) || !addr.IsGlobalUnicast() {
return &models.ErrMigrationNotAllowed{Host: u.Host, PrivateNet: addr.String()}
}
}
}

return nil
}

// MigrateRepository migrate repository according MigrateOptions
func MigrateRepository(ctx context.Context, doer *models.User, ownerName string, opts base.MigrateOptions) (*models.Repository, error) {
err := isMigrateURLAllowed(opts.CloneAddr)
if err != nil {
return nil, err
}

var (
downloader base.Downloader
uploader = NewGiteaLocalUploader(ctx, doer, ownerName, opts.RepoName)
err error
)

for _, factory := range factories {
Expand Down Expand Up @@ -308,3 +352,32 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts

return nil
}

// Init migrations service
func Init() error {
var err error
allowList, err = matchlist.NewMatchlist(setting.Migrations.AllowedDomains...)
if err != nil {
return fmt.Errorf("init migration allowList domains failed: %v", err)
}

blockList, err = matchlist.NewMatchlist(setting.Migrations.BlockedDomains...)
if err != nil {
return fmt.Errorf("init migration blockList domains failed: %v", err)
}

return nil
}

// isIPPrivate reports whether ip is a private address, according to
// RFC 1918 (IPv4 addresses) and RFC 4193 (IPv6 addresses).
// from https://github.com/golang/go/pull/42793
// TODO remove if https://github.com/golang/go/issues/29146 got resolved
func isIPPrivate(ip net.IP) bool {
if ip4 := ip.To4(); ip4 != nil {
return ip4[0] == 10 ||
(ip4[0] == 172 && ip4[1]&0xf0 == 16) ||
(ip4[0] == 192 && ip4[1] == 168)
}
return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc
}
34 changes: 34 additions & 0 deletions modules/migrations/migrate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
"testing"

"code.gitea.io/gitea/modules/setting"

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

func TestMigrateWhiteBlocklist(t *testing.T) {
setting.Migrations.AllowedDomains = []string{"github.com"}
assert.NoError(t, Init())

err := isMigrateURLAllowed("https://gitlab.com/gitlab/gitlab.git")
assert.Error(t, err)

err = isMigrateURLAllowed("https://github.com/go-gitea/gitea.git")
assert.NoError(t, err)

setting.Migrations.AllowedDomains = []string{}
setting.Migrations.BlockedDomains = []string{"github.com"}
assert.NoError(t, Init())

err = isMigrateURLAllowed("https://gitlab.com/gitlab/gitlab.git")
assert.NoError(t, err)

err = isMigrateURLAllowed("https://github.com/go-gitea/gitea.git")
assert.Error(t, err)
}
22 changes: 20 additions & 2 deletions modules/setting/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@

package setting

import (
"strings"
)

var (
// Migrations settings
Migrations = struct {
MaxAttempts int
RetryBackoff int
MaxAttempts int
RetryBackoff int
AllowedDomains []string
BlockedDomains []string
AllowLocalNetworks bool
}{
MaxAttempts: 3,
RetryBackoff: 3,
Expand All @@ -19,4 +26,15 @@ func newMigrationsService() {
sec := Cfg.Section("migrations")
Migrations.MaxAttempts = sec.Key("MAX_ATTEMPTS").MustInt(Migrations.MaxAttempts)
Migrations.RetryBackoff = sec.Key("RETRY_BACKOFF").MustInt(Migrations.RetryBackoff)

Migrations.AllowedDomains = sec.Key("ALLOWED_DOMAINS").Strings(",")
for i := range Migrations.AllowedDomains {
Migrations.AllowedDomains[i] = strings.ToLower(Migrations.AllowedDomains[i])
}
Migrations.BlockedDomains = sec.Key("BLOCKED_DOMAINS").Strings(",")
for i := range Migrations.BlockedDomains {
Migrations.BlockedDomains[i] = strings.ToLower(Migrations.BlockedDomains[i])
}

Migrations.AllowLocalNetworks = sec.Key("ALLOW_LOCALNETWORKS").MustBool(false)
}
2 changes: 2 additions & 0 deletions routers/api/v1/repo/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *models.User, remoteA
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' contains invalid characters.", err.(models.ErrNameCharsNotAllowed).Name))
case models.IsErrNamePatternNotAllowed(err):
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(models.ErrNamePatternNotAllowed).Pattern))
case models.IsErrMigrationNotAllowed(err):
ctx.Error(http.StatusUnprocessableEntity, "", err)
default:
err = util.URLSanitizedError(err, remoteAddr)
if strings.Contains(err.Error(), "Authentication failed") ||
Expand Down
4 changes: 4 additions & 0 deletions routers/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/external"
repo_migrations "code.gitea.io/gitea/modules/migrations"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/setting"
Expand Down Expand Up @@ -201,6 +202,9 @@ func GlobalInit(ctx context.Context) {
if err := task.Init(); err != nil {
log.Fatal("Failed to initialize task scheduler: %v", err)
}
if err := repo_migrations.Init(); err != nil {
log.Fatal("Failed to initialize repository migrations: %v", err)
}
eventsource.GetManager().Init()

if setting.EnableSQLite3 {
Expand Down