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

feat: Add file path protection to rulesets #2415

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ export GITHUB_OWNER=
# enable testing of enterprise appliances
export GITHUB_BASE_URL=

# enable testing of GitHub Paid features, these normally also require an organization e.g. repository push rulesets
export GITHUB_PAID_FEATURES=true

# leverage helper accounts for tests requiring them
# examples include:
# - https://github.com/github-terraform-test-user
Expand Down
1 change: 1 addition & 0 deletions github/provider_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

var testCollaborator = os.Getenv("GITHUB_TEST_COLLABORATOR")
var isEnterprise = os.Getenv("ENTERPRISE_ACCOUNT")
var isPaidPlan = os.Getenv("GITHUB_PAID_FEATURES")
var testEnterprise = os.Getenv("ENTERPRISE_SLUG")
var testOrganization = testOrganizationFunc()
var testOwner = os.Getenv("GITHUB_OWNER")
Expand Down
57 changes: 55 additions & 2 deletions github/resource_github_repository_ruleset.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ func resourceGithubRepositoryRuleset() *schema.Resource {
"target": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{"branch", "tag"}, false),
Description: "Possible values are `branch` and `tag`.",
ValidateFunc: validation.StringInSlice([]string{"branch", "push", "tag"}, false),
Description: "Possible values are `branch`, `push` and `tag`.",
},
"repository": {
Type: schema.TypeString,
Expand Down Expand Up @@ -444,6 +444,59 @@ func resourceGithubRepositoryRuleset() *schema.Resource {
},
},
},
"file_path_restriction": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Description: "Prevent commits that include changes in specified file paths from being pushed to the commit graph.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"restricted_file_paths": {
Type: schema.TypeList,
MinItems: 1,
Required: true,
Description: "The file paths that are restricted from being pushed to the commit graph.",
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
},
},
"max_file_size": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Description: "Prevent pushes based on file size.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"max_file_size": {
Type: schema.TypeInt,
Required: true,
Description: "The maximum allowed size of a file in bytes.",
},
},
},
},
"file_extension_restriction": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Description: "Prevent pushes based on file extensions.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"restricted_file_extensions": {
Type: schema.TypeSet,
MinItems: 1,
Required: true,
Description: "A list of file extensions.",
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
},
},
},
},
},
Expand Down
76 changes: 76 additions & 0 deletions github/resource_github_repository_ruleset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,82 @@ func TestGithubRepositoryRulesets(t *testing.T) {
})

})
t.Run("Creates repository rulesets with paid features without errors", func(t *testing.T) {
if isPaidPlan != "true" {
t.Skip("Skipping because `GITHUB_PAID_FEATURES` is not set to true")
}
config := fmt.Sprintf(`
resource "github_repository" "test" {
name = "tf-acc-test-%s"
auto_init = false
visibility = "internal"
vulnerability_alerts = true
}
resource "github_repository_ruleset" "test_push" {
name = "test-push"
repository = github_repository.test.id
target = "push"
enforcement = "active"
rules {
file_path_restriction {
restricted_file_paths = ["test.txt"]
}
max_file_size {
max_file_size = 1048576
}
file_extension_restriction {
restricted_file_extensions = ["*.zip"]
}
}
}
`, randomID)
check := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"github_repository_ruleset.test_push", "name",
"test-push",
),
resource.TestCheckResourceAttr(
"github_repository_ruleset.test_push", "target",
"push",
),
resource.TestCheckResourceAttr(
"github_repository_ruleset.test_push", "rules.0.file_path_restriction.0.restricted_file_paths.0",
"test.txt",
),
resource.TestCheckResourceAttr(
"github_repository_ruleset.test_push", "rules.0.max_file_size.0.max_file_size",
"1048576",
),
resource.TestCheckResourceAttr(
"github_repository_ruleset.test_push", "rules.0.file_extension_restriction.0.restricted_file_extensions.0",
"*.zip",
),
)
testCase := func(t *testing.T, mode string) {
resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessMode(t, mode) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: config,
Check: check,
},
},
})
}
t.Run("with an anonymous account", func(t *testing.T) {
t.Skip("anonymous account not supported for this operation")
})
t.Run("with an individual account", func(t *testing.T) {
t.Skip("individual account not supported for this operation")
})
t.Run("with a paid plan in an organization", func(t *testing.T) {
testCase(t, organization)
})
})

}

Expand Down
48 changes: 48 additions & 0 deletions github/respository_rules_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,43 @@ func expandRules(input []interface{}, org bool) []*github.RepositoryRule {
rulesSlice = append(rulesSlice, github.NewRequiredCodeScanningRule(params))
}

// file_path_restriction rule
if v, ok := rulesMap["file_path_restriction"].([]interface{}); ok && len(v) != 0 {
filePathRestrictionMap := v[0].(map[string]interface{})
restrictedFilePaths := make([]string, 0)
for _, path := range filePathRestrictionMap["restricted_file_paths"].([]interface{}) {
restrictedFilePaths = append(restrictedFilePaths, path.(string))
}
params := &github.RuleFileParameters{
RestrictedFilePaths: &restrictedFilePaths,
}
rulesSlice = append(rulesSlice, github.NewFilePathRestrictionRule(params))
}

// max_file_size rule
if v, ok := rulesMap["max_file_size"].([]interface{}); ok && len(v) != 0 {
maxFileSizeMap := v[0].(map[string]interface{})
maxFileSize := int64(maxFileSizeMap["max_file_size"].(float64))
params := &github.RuleMaxFileSizeParameters{
MaxFileSize: maxFileSize,
}
rulesSlice = append(rulesSlice, github.NewMaxFileSizeRule(params))

}

// file_extension_restriction rule
if v, ok := rulesMap["file_extension_restriction"].([]interface{}); ok && len(v) != 0 {
fileExtensionRestrictionMap := v[0].(map[string]interface{})
restrictedFileExtensions := make([]string, 0)
for _, extension := range fileExtensionRestrictionMap["restricted_file_extensions"].([]interface{}) {
restrictedFileExtensions = append(restrictedFileExtensions, extension.(string))
}
params := &github.RuleFileExtensionRestrictionParameters{
RestrictedFileExtensions: restrictedFileExtensions,
}
rulesSlice = append(rulesSlice, github.NewFileExtensionRestrictionRule(params))
}

return rulesSlice
}

Expand Down Expand Up @@ -504,6 +541,17 @@ func flattenRules(rules []*github.RepositoryRule, org bool) []interface{} {
rule["required_check"] = requiredStatusChecksSlice
rule["strict_required_status_checks_policy"] = params.StrictRequiredStatusChecksPolicy
rulesMap[v.Type] = []map[string]interface{}{rule}

case "file_path_restriction":
var params github.RuleFileParameters
err := json.Unmarshal(*v.Parameters, &params)
if err != nil {
log.Printf("[INFO] Unexpected error unmarshalling rule %s with parameters: %v",
v.Type, v.Parameters)
}
rule := make(map[string]interface{})
rule["restricted_file_paths"] = params.GetRestrictedFilePaths()
rulesMap[v.Type] = []map[string]interface{}{rule}
}
}

Expand Down
16 changes: 16 additions & 0 deletions website/docs/r/repository_ruleset.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ The `rules` block supports the following:

* `required_code_scanning` - (Optional) (Block List, Max: 1) Define which tools must provide code scanning results before the reference is updated. When configured, code scanning must be enabled and have results for both the commit and the reference being updated. Multiple code scanning tools can be specified. (see [below for nested schema](#rules.required_code_scanning))

* `file_path_restriction` - (Optional) (Block List, Max 1) Parameters to be used for the file_path_restriction rule. When enabled restricts access to files within the repository. (See [below for nested schema](#rules.file_path_restriction))

* `max_file_size` - (Optional) (Block List, Max 1) Parameters to be used for the max_file_size rule. When enabled restricts the maximum size of a file that can be pushed to the repository. (See [below for nested schema](#rules.max_file_size))

* `update` - (Optional) (Boolean) Only allow users with bypass permission to update matching refs.

* `update_allows_fetch_and_merge` - (Optional) (Boolean) Branch can pull changes from its upstream repository. This is only applicable to forked repositories. Requires `update` to be set to `true`. Note: behaviour is affected by a known bug on the GitHub side which may cause issues when using this parameter.
Expand Down Expand Up @@ -203,6 +207,18 @@ The `rules` block supports the following:

* `tool` - (Required) (String) The name of a code scanning tool.

#### rules.file_path_restriction ####

* `restricted_file_paths` - (Required) (Block Set, Min: 1) The file paths that are restricted from being pushed to the commit graph.

#### rules.max_file_size ####

* `max_file_size` - (Required) (Integer) The maximum allowed size, in bytes, of a file.

#### rules.file_extension_restriction ####

* `restricted_file_extensions` - (Required) (Block Set, Min: 1) The file extensions that are restricted from being pushed to the commit graph.

#### bypass_actors ####

* `actor_id` - (Required) (Number) The ID of the actor that can bypass a ruleset. If `actor_type` is `Integration`, `actor_id` is a GitHub App ID. App ID can be obtained by following instructions from the [Get an App API docs](https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-app)
Expand Down
Loading