diff --git a/Makefile b/Makefile index 55ddb1d..66f2829 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: test-acc test-acc: TF_ACC=1 \ - go test ./provider/ -v -run=^TestAcc_ -count=1 -coverprofile=cov.out -coverpkg "github.com/ctfer-io/terraform-provider-ctfd/provider,github.com/ctfer-io/terraform-provider-ctfd/provider/challenge,github.com/ctfer-io/terraform-provider-ctfd/provider/utils,github.com/ctfer-io/terraform-provider-ctfd/provider/validators" + go test ./provider/ -v -run=^TestAcc_ -count=1 -coverprofile=cov.out -coverpkg "github.com/ctfer-io/terraform-provider-ctfd/provider,github.com/ctfer-io/terraform-provider-ctfd/provider/utils,github.com/ctfer-io/terraform-provider-ctfd/provider/validators" .PHONY: docs docs: diff --git a/docs/data-sources/challenges.md b/docs/data-sources/challenges.md index 6a847c2..3d28791 100644 --- a/docs/data-sources/challenges.md +++ b/docs/data-sources/challenges.md @@ -29,10 +29,7 @@ Read-Only: - `connection_info` (String) Connection Information to connect to the challenge instance, useful for pwn or web pentest. - `decay` (Number) - `description` (String) Description of the challenge, consider using multiline descriptions for better style. -- `files` (Attributes List) List of files given to players to flag the challenge. (see [below for nested schema](#nestedatt--challenges--files)) -- `flags` (Attributes List) List of challenge flags that solves it. (see [below for nested schema](#nestedatt--challenges--flags)) - `function` (String) Decay function to define how the challenge value evolve through solves, either linear or logarithmic. -- `hints` (Attributes List) List of hints about the challenge displayed to the end-user. (see [below for nested schema](#nestedatt--challenges--hints)) - `id` (String) Identifier of the challenge. - `max_attempts` (Number) Maximum amount of attempts before being unable to flag the challenge. - `minimum` (Number) @@ -45,40 +42,6 @@ Read-Only: - `type` (String) Type of the challenge defining its layout, either standard or dynamic. - `value` (Number) - -### Nested Schema for `challenges.files` - -Read-Only: - -- `content` (String) -- `contentb64` (String) -- `id` (String) -- `location` (String) -- `name` (String) - - - -### Nested Schema for `challenges.flags` - -Read-Only: - -- `content` (String) -- `data` (String) -- `id` (String) -- `type` (String) - - - -### Nested Schema for `challenges.hints` - -Read-Only: - -- `content` (String) -- `cost` (Number) -- `id` (String) -- `requirements` (List of String) - - ### Nested Schema for `challenges.requirements` diff --git a/docs/resources/challenge.md b/docs/resources/challenge.md index 156b222..86ac1f0 100644 --- a/docs/resources/challenge.md +++ b/docs/resources/challenge.md @@ -26,10 +26,6 @@ resource "ctfd_challenge" "http" { state = "visible" function = "logarithmic" - flags = [{ - content = "CTF{some_flag}" - }] - topics = [ "Misc" ] @@ -37,19 +33,30 @@ resource "ctfd_challenge" "http" { "misc", "basic" ] +} - hints = [{ - content = "Some super-helpful hint" - cost = 50 - }, { - content = "Even more helpful hint !" - cost = 50 - }] - - files = [{ - name = "image.png" - contentb64 = filebase64(".../image.png") - }] +resource "ctfd_flag" "http_flag" { + challenge_id = ctfd_challenge.http.id + content = "CTF{some_flag}" +} + +resource "ctfd_hint" "http_hint_1" { + challenge_id = ctfd_challenge.http.id + content = "Some super-helpful hint" + cost = 50 +} + +resource "ctfd_hint" "http_hint_2" { + challenge_id = ctfd_challenge.http.id + content = "Even more helpful hint !" + cost = 50 + requirements = [ctfd_hint.http_hint_1.id] +} + +resource "ctfd_file" "http_file" { + challenge_id = ctfd_challenge.http.id + name = "image.png" + contentb64 = filebase64(".../image.png") } ``` @@ -67,10 +74,7 @@ resource "ctfd_challenge" "http" { - `connection_info` (String) Connection Information to connect to the challenge instance, useful for pwn, web and infrastructure pentests. - `decay` (Number) The decay defines from each number of solves does the decay function triggers until reaching minimum. This function is defined by CTFd and could be configured through `.function`. -- `files` (Attributes List) List of files given to players to flag the challenge. (see [below for nested schema](#nestedatt--files)) -- `flags` (Attributes List) List of challenge flags that solves it. (see [below for nested schema](#nestedatt--flags)) - `function` (String) Decay function to define how the challenge value evolve through solves, either linear or logarithmic. -- `hints` (Attributes List) List of hints about the challenge displayed to the end-user. (see [below for nested schema](#nestedatt--hints)) - `max_attempts` (Number) Maximum amount of attempts before being unable to flag the challenge. - `minimum` (Number) The minimum points for a dynamic-score challenge to reach with the decay function. Once there, no solve could have more value. - `next` (Number) Suggestion for the end-user as next challenge to work on. @@ -84,59 +88,6 @@ resource "ctfd_challenge" "http" { - `id` (String) Identifier of the challenge. - -### Nested Schema for `files` - -Required: - -- `name` (String) Name of the file as displayed to end-users. - -Optional: - -- `content` (String, Sensitive) Raw content of the file, perfectly fit the use-cases of a .txt document or anything with a simple binary content. You could provide it from the file-system using `file("${path.module}/...")`. -- `contentb64` (String, Sensitive) Base 64 content of the file, perfectly fit the use-cases of complex binaries. You could provide it from the file-system using `filebase64("${path.module}/...")`. -- `location` (String) Location where the file is stored on the CTFd instance, for download purposes. - -Read-Only: - -- `id` (String) Identifier of the file, used internally to handle the CTFd corresponding object. -- `sha1sum` (String) The sha1 sum of the file. - - - -### Nested Schema for `flags` - -Required: - -- `content` (String, Sensitive) The actual flag to match. Consider using the convention `MYCTF{value}` with `MYCTF` being the shortcode of your event's name and `value` depending on each challenge. - -Optional: - -- `data` (String) The flag sensitivity information, either case_sensitive or case_insensitive -- `type` (String) The type of the flag, could be either static or regex - -Read-Only: - -- `id` (String) Identifier of the flag, used internally to handle the CTFd corresponding object. - - - -### Nested Schema for `hints` - -Required: - -- `content` (String) Content of the hint as displayed to the end-user. - -Optional: - -- `cost` (Number) Cost of the hint, and if any specified, the end-user will consume its own (or team) points to get it. -- `requirements` (List of String) Other hints required to be consumed before getting this one. Useful for cost-increasing hint strategies with more and more help. - -Read-Only: - -- `id` (String) Identifier of the hint, used internally to handle the CTFd corresponding object. - - ### Nested Schema for `requirements` diff --git a/docs/resources/file.md b/docs/resources/file.md new file mode 100644 index 0000000..680608c --- /dev/null +++ b/docs/resources/file.md @@ -0,0 +1,59 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "ctfd_file Resource - terraform-provider-ctfd" +subcategory: "" +description: |- + A CTFd file for a challenge. +--- + +# ctfd_file (Resource) + +A CTFd file for a challenge. + +## Example Usage + +```terraform +resource "ctfd_challenge" "http" { + name = "My Challenge" + category = "misc" + description = "..." + value = 500 + decay = 100 + minimum = 50 + state = "visible" + function = "logarithmic" + + topics = [ + "Misc" + ] + tags = [ + "misc", + "basic" + ] +} + +resource "ctfd_file" "http_file" { + challenge_id = ctfd_challenge.http.id + name = "image.png" + contentb64 = filebase64(".../image.png") +} +``` + + +## Schema + +### Required + +- `challenge_id` (String) Challenge of the file. +- `name` (String) Name of the file as displayed to end-users. + +### Optional + +- `content` (String, Sensitive) Raw content of the file, perfectly fit the use-cases of a .txt document or anything with a simple binary content. You could provide it from the file-system using `file("${path.module}/...")`. +- `contentb64` (String, Sensitive) Base 64 content of the file, perfectly fit the use-cases of complex binaries. You could provide it from the file-system using `filebase64("${path.module}/...")`. +- `location` (String) Location where the file is stored on the CTFd instance, for download purposes. + +### Read-Only + +- `id` (String) Identifier of the file, used internally to handle the CTFd corresponding object. WARNING: updating this file does not work, requires full replacement. +- `sha1sum` (String) The sha1 sum of the file. diff --git a/docs/resources/flag.md b/docs/resources/flag.md new file mode 100644 index 0000000..97a8077 --- /dev/null +++ b/docs/resources/flag.md @@ -0,0 +1,56 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "ctfd_flag Resource - terraform-provider-ctfd" +subcategory: "" +description: |- + A flag to solve the challenge. +--- + +# ctfd_flag (Resource) + +A flag to solve the challenge. + +## Example Usage + +```terraform +resource "ctfd_challenge" "http" { + name = "My Challenge" + category = "misc" + description = "..." + value = 500 + decay = 100 + minimum = 50 + state = "visible" + function = "logarithmic" + + topics = [ + "Misc" + ] + tags = [ + "misc", + "basic" + ] +} + +resource "ctfd_flag" "http_flag" { + challenge_id = ctfd_challenge.http.id + content = "CTF{some_flag}" +} +``` + + +## Schema + +### Required + +- `challenge_id` (String) Challenge of the flag. +- `content` (String, Sensitive) The actual flag to match. Consider using the convention `MYCTF{value}` with `MYCTF` being the shortcode of your event's name and `value` depending on each challenge. + +### Optional + +- `data` (String) The flag sensitivity information, either case_sensitive or case_insensitive +- `type` (String) The type of the flag, could be either static or regex + +### Read-Only + +- `id` (String) Identifier of the flag, used internally to handle the CTFd corresponding object. diff --git a/docs/resources/hint.md b/docs/resources/hint.md new file mode 100644 index 0000000..b41fdf5 --- /dev/null +++ b/docs/resources/hint.md @@ -0,0 +1,69 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "ctfd_hint Resource - terraform-provider-ctfd" +subcategory: "" +description: |- + A hint for a challenge to help players solve it. +--- + +# ctfd_hint (Resource) + +A hint for a challenge to help players solve it. + +## Example Usage + +```terraform +resource "ctfd_challenge" "http" { + name = "My Challenge" + category = "misc" + description = "..." + value = 500 + decay = 100 + minimum = 50 + state = "visible" + function = "logarithmic" + + topics = [ + "Misc" + ] + tags = [ + "misc", + "basic" + ] +} + +resource "ctfd_flag" "http_flag" { + challenge_id = ctfd_challenge.http.id + content = "CTF{some_flag}" +} + +resource "ctfd_hint" "http_hint" { + challenge_id = ctfd_challenge.http.id + content = "Some super-helpful hint" + cost = 50 +} + +resource "ctfd_hint" "http_hint_2" { + challenge_id = ctfd_challenge.http.id + content = "Even more helpful hint !" + cost = 50 + requirements = [ctfd_hint.http_hint_1.id] +} +``` + + +## Schema + +### Required + +- `challenge_id` (String) Challenge of the hint. +- `content` (String) Content of the hint as displayed to the end-user. + +### Optional + +- `cost` (Number) Cost of the hint, and if any specified, the end-user will consume its own (or team) points to get it. +- `requirements` (List of String) List of the other hints it depends on. + +### Read-Only + +- `id` (String) Identifier of the hint, used internally to handle the CTFd corresponding object. diff --git a/examples/provider-install-verification/main.tf b/examples/provider-install-verification/main.tf index a812404..dbb1450 100644 --- a/examples/provider-install-verification/main.tf +++ b/examples/provider-install-verification/main.tf @@ -10,6 +10,8 @@ provider "ctfd" { url = "http://localhost:8080" } + + resource "ctfd_challenge" "http" { name = "HTTP Authentication" category = "network" @@ -27,10 +29,6 @@ resource "ctfd_challenge" "http" { state = "visible" function = "logarithmic" - flags = [{ - content = "24HIUT{Http_1s_n0t_s3cuR3}" - }] - topics = [ "Network" ] @@ -38,21 +36,34 @@ resource "ctfd_challenge" "http" { "network", "http" ] +} - hints = [{ - content = "Les flux http ne sont pas chiffrés" - cost = 50 - }, { - content = "Les informations sont POSTées en HTTP :)" - cost = 50 - }] +resource "ctfd_flag" "http_flag" { + challenge_id = ctfd_challenge.http.id + content = "24HIUT{Http_1s_n0t_s3cuR3}" +} - files = [{ - name = "capture.pcapng" - contentb64 = filebase64("${path.module}/capture.pcapng") - }] +resource "ctfd_hint" "http_hint_1" { + challenge_id = ctfd_challenge.http.id + content = "Les flux http ne sont pas chiffrés" + cost = 50 } +resource "ctfd_hint" "http_hint_2" { + challenge_id = ctfd_challenge.http.id + content = "Les informations sont POSTées en HTTP :)" + cost = 50 + requirements = [ctfd_hint.http_hint_1.id] +} + +resource "ctfd_file" "http_file" { + challenge_id = ctfd_challenge.http.id + name = "capture.pcapng" + contentb64 = filebase64("${path.module}/capture.pcapng") +} + + + resource "ctfd_challenge" "icmp" { name = "Stealing data" category = "network" @@ -85,17 +96,28 @@ resource "ctfd_challenge" "icmp" { "network", "icmp" ] +} - hints = [{ - content = "Vous ne trouvez pas qu'il ya beaucoup de requêtes ICMP ?" - cost = 50 - }, { - content = "Pour l'exo, le ttl a été modifié, tente un `ip.ttl<=20`" - cost = 50 - }] +resource "ctfd_flag" "icmp_flag" { + challenge_id = ctfd_challenge.icmp.id + content = "24HIUT{IcmpExfiltrationIsEasy}" +} - files = [{ - name = "icmp.pcap" - contentb64 = filebase64("${path.module}/icmp.pcap") - }] +resource "ctfd_hint" "icmp_hint_1" { + challenge_id = ctfd_challenge.icmp.id + content = "Vous ne trouvez pas qu'il ya beaucoup de requêtes ICMP ?" + cost = 50 +} + +resource "ctfd_hint" "icmp_hint_2" { + challenge_id = ctfd_challenge.icmp.id + content = "Pour l'exo, le ttl a été modifié, tente un `ip.ttl<=20`" + cost = 50 + requirements = [ctfd_hint.icmp_hint_2.id] +} + +resource "ctfd_file" "icmp_file" { + challenge_id = ctfd_challenge.icmp.id + name = "icmp.pcap" + contentb64 = filebase64("${path.module}/icmp.pcap") } diff --git a/examples/resources/ctfd_challenge/resource.tf b/examples/resources/ctfd_challenge/resource.tf index d74d52b..e698b07 100644 --- a/examples/resources/ctfd_challenge/resource.tf +++ b/examples/resources/ctfd_challenge/resource.tf @@ -8,10 +8,6 @@ resource "ctfd_challenge" "http" { state = "visible" function = "logarithmic" - flags = [{ - content = "CTF{some_flag}" - }] - topics = [ "Misc" ] @@ -19,17 +15,28 @@ resource "ctfd_challenge" "http" { "misc", "basic" ] +} + +resource "ctfd_flag" "http_flag" { + challenge_id = ctfd_challenge.http.id + content = "CTF{some_flag}" +} - hints = [{ - content = "Some super-helpful hint" - cost = 50 - }, { - content = "Even more helpful hint !" - cost = 50 - }] +resource "ctfd_hint" "http_hint_1" { + challenge_id = ctfd_challenge.http.id + content = "Some super-helpful hint" + cost = 50 +} + +resource "ctfd_hint" "http_hint_2" { + challenge_id = ctfd_challenge.http.id + content = "Even more helpful hint !" + cost = 50 + requirements = [ctfd_hint.http_hint_1.id] +} - files = [{ - name = "image.png" - contentb64 = filebase64(".../image.png") - }] +resource "ctfd_file" "http_file" { + challenge_id = ctfd_challenge.http.id + name = "image.png" + contentb64 = filebase64(".../image.png") } diff --git a/examples/resources/ctfd_file/resource.tf b/examples/resources/ctfd_file/resource.tf new file mode 100644 index 0000000..bf47e4e --- /dev/null +++ b/examples/resources/ctfd_file/resource.tf @@ -0,0 +1,24 @@ +resource "ctfd_challenge" "http" { + name = "My Challenge" + category = "misc" + description = "..." + value = 500 + decay = 100 + minimum = 50 + state = "visible" + function = "logarithmic" + + topics = [ + "Misc" + ] + tags = [ + "misc", + "basic" + ] +} + +resource "ctfd_file" "http_file" { + challenge_id = ctfd_challenge.http.id + name = "image.png" + contentb64 = filebase64(".../image.png") +} diff --git a/examples/resources/ctfd_flag/resource.tf b/examples/resources/ctfd_flag/resource.tf new file mode 100644 index 0000000..4ca13f3 --- /dev/null +++ b/examples/resources/ctfd_flag/resource.tf @@ -0,0 +1,23 @@ +resource "ctfd_challenge" "http" { + name = "My Challenge" + category = "misc" + description = "..." + value = 500 + decay = 100 + minimum = 50 + state = "visible" + function = "logarithmic" + + topics = [ + "Misc" + ] + tags = [ + "misc", + "basic" + ] +} + +resource "ctfd_flag" "http_flag" { + challenge_id = ctfd_challenge.http.id + content = "CTF{some_flag}" +} diff --git a/examples/resources/ctfd_hint/resource.tf b/examples/resources/ctfd_hint/resource.tf new file mode 100644 index 0000000..94a1c6d --- /dev/null +++ b/examples/resources/ctfd_hint/resource.tf @@ -0,0 +1,36 @@ +resource "ctfd_challenge" "http" { + name = "My Challenge" + category = "misc" + description = "..." + value = 500 + decay = 100 + minimum = 50 + state = "visible" + function = "logarithmic" + + topics = [ + "Misc" + ] + tags = [ + "misc", + "basic" + ] +} + +resource "ctfd_flag" "http_flag" { + challenge_id = ctfd_challenge.http.id + content = "CTF{some_flag}" +} + +resource "ctfd_hint" "http_hint" { + challenge_id = ctfd_challenge.http.id + content = "Some super-helpful hint" + cost = 50 +} + +resource "ctfd_hint" "http_hint_2" { + challenge_id = ctfd_challenge.http.id + content = "Even more helpful hint !" + cost = 50 + requirements = [ctfd_hint.http_hint_1.id] +} diff --git a/go.mod b/go.mod index 44242cb..0c79b23 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/ctfer-io/terraform-provider-ctfd go 1.22 require ( - github.com/ctfer-io/go-ctfd v0.7.0 + github.com/ctfer-io/go-ctfd v0.8.0 github.com/hashicorp/terraform-plugin-docs v0.19.2 github.com/hashicorp/terraform-plugin-framework v1.8.0 github.com/hashicorp/terraform-plugin-go v0.23.0 diff --git a/go.sum b/go.sum index e32ead6..442804b 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZ github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/ctfer-io/go-ctfd v0.7.0 h1:/p8ynT3DTu201DANFIVc0otgE6eViSwcRuosyE9azaQ= -github.com/ctfer-io/go-ctfd v0.7.0/go.mod h1:zOOgs1LmKEVW3rilcog0jT921vjShmR3avJbSMtvNyM= +github.com/ctfer-io/go-ctfd v0.8.0 h1:as4TWW0OFI/4oZnJi8ad1pEeVqcZWVw2HiQl0rMDWNk= +github.com/ctfer-io/go-ctfd v0.8.0/go.mod h1:zOOgs1LmKEVW3rilcog0jT921vjShmR3avJbSMtvNyM= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/provider/challenge/function.go b/provider/challenge/function.go deleted file mode 100644 index 12ea7e7..0000000 --- a/provider/challenge/function.go +++ /dev/null @@ -1,10 +0,0 @@ -// This file is related to the challenge.function attribute - -package challenge - -import "github.com/hashicorp/terraform-plugin-framework/types" - -var ( - FunctionLinear = types.StringValue("linear") - FunctionLogarithmic = types.StringValue("logarithmic") -) diff --git a/provider/challenge/requirements_subresource.go b/provider/challenge/requirements_subresource.go deleted file mode 100644 index 3698bd6..0000000 --- a/provider/challenge/requirements_subresource.go +++ /dev/null @@ -1,36 +0,0 @@ -package challenge - -import ( - "github.com/ctfer-io/terraform-provider-ctfd/provider/utils" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -var ( - BehaviorHidden = types.StringValue("hidden") - BehaviorAnonymized = types.StringValue("anonymized") -) - -type RequirementsSubresourceModel struct { - Behavior types.String `tfsdk:"behavior"` - Prerequisites []types.String `tfsdk:"prerequisites"` -} - -func GetAnon(str types.String) *bool { - switch { - case str.Equal(BehaviorHidden): - return nil - case str.Equal(BehaviorAnonymized): - return utils.Ptr(true) - } - panic("invalid anonymization value: " + str.ValueString()) -} - -func FromAnon(b *bool) types.String { - if b == nil { - return BehaviorHidden - } - if *b { - return BehaviorAnonymized - } - panic("invalid anonymization value, got boolean false") -} diff --git a/provider/file_resource.go b/provider/file_resource.go index 83fc182..afd01b5 100644 --- a/provider/file_resource.go +++ b/provider/file_resource.go @@ -49,7 +49,7 @@ func (r *fileResource) Metadata(ctx context.Context, req resource.MetadataReques func (r *fileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: "", + MarkdownDescription: "A CTFd file for a challenge.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, @@ -61,22 +61,17 @@ func (r *fileResource) Schema(ctx context.Context, req resource.SchemaRequest, r "challenge_id": schema.StringAttribute{ MarkdownDescription: "Challenge of the file.", Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, }, "name": schema.StringAttribute{ MarkdownDescription: "Name of the file as displayed to end-users.", Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, }, "location": schema.StringAttribute{ MarkdownDescription: "Location where the file is stored on the CTFd instance, for download purposes.", Optional: true, Computed: true, PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), stringplanmodifier.RequiresReplace(), }, }, @@ -84,7 +79,7 @@ func (r *fileResource) Schema(ctx context.Context, req resource.SchemaRequest, r MarkdownDescription: "The sha1 sum of the file.", Computed: true, PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), }, }, "content": schema.StringAttribute{ @@ -93,6 +88,7 @@ func (r *fileResource) Schema(ctx context.Context, req resource.SchemaRequest, r Computed: true, Sensitive: true, PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), stringplanmodifier.RequiresReplace(), }, }, @@ -102,6 +98,7 @@ func (r *fileResource) Schema(ctx context.Context, req resource.SchemaRequest, r Computed: true, Sensitive: true, PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), stringplanmodifier.RequiresReplace(), }, }, @@ -191,7 +188,7 @@ func (r *fileResource) Read(ctx context.Context, req resource.ReadRequest, resp data.Name = types.StringValue(filepath.Base(res.Location)) data.Location = types.StringValue(res.Location) data.SHA1Sum = types.StringValue(res.SHA1sum) - data.ChallengeID = lookForChallengeId(ctx, r.client, res.Location, resp.Diagnostics) + data.ChallengeID = lookForChallengeId(ctx, r.client, res.ID, resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -267,14 +264,16 @@ func (data *fileResourceModel) PropagateContent(ctx context.Context, diags diag. } // XXX this helper only exist because CTFd does not return the challenge id of a file if it exist... -func lookForChallengeId(ctx context.Context, client *api.Client, location string, diags diag.Diagnostics) types.String { - challs, err := client.GetChallenges(nil, api.WithContext(ctx)) +func lookForChallengeId(ctx context.Context, client *api.Client, fileID int, diags diag.Diagnostics) types.String { + challs, err := client.GetChallenges(&api.GetChallengesParams{ + View: utils.Ptr("admin"), // required, else CTFd only returns the "visible" challenges + }, api.WithContext(ctx)) if err != nil { diags.AddError( "CTFd Error", fmt.Sprintf("Unable to query challenges, got error: %s", err), ) - return types.StringValue("") + return types.StringNull() } for _, chall := range challs { @@ -284,17 +283,17 @@ func lookForChallengeId(ctx context.Context, client *api.Client, location string "CTFd Error", fmt.Sprintf("Unable to query challenge %d files, got error: %s", chall.ID, err), ) - return types.StringValue("") + return types.StringNull() } for _, file := range files { - if file.Location == location { + if file.ID == fileID { return types.StringValue(strconv.Itoa(chall.ID)) } } } diags.AddError( "Provider Error", - fmt.Sprintf("Unable to find challenge of file at %s", location), + fmt.Sprintf("Unable to find challenge of file %d", fileID), ) - return types.StringValue("") + return types.StringNull() } diff --git a/provider/file_resource_test.go b/provider/file_resource_test.go index fe93641..ba00609 100644 --- a/provider/file_resource_test.go +++ b/provider/file_resource_test.go @@ -13,23 +13,19 @@ func TestAcc_File_Lifecycle(t *testing.T) { // Create and Read testing { Config: providerConfig + ` -#resource "ctfd_challenge" "example" { -# name = "Example challenge" -# category = "test" -# description = "Example challenge description..." -# value = 500 -#} +resource "ctfd_challenge" "example" { + name = "Example challenge" + category = "test" + description = "Example challenge description..." + value = 500 +} resource "ctfd_file" "pouet" { - challenge_id = "1" + challenge_id = ctfd_challenge.example.id name = "pouet.txt" content = "Pouet is a clown cat" } `, - Check: resource.ComposeAggregateTestCheckFunc( - // Verify dynamic values have any value set in the state. - // resource.TestCheckResourceAttr("ctfd_flag.first", "requirements.#", "0"), - ), }, // ImportState testing { @@ -40,19 +36,19 @@ resource "ctfd_file" "pouet" { // Update and Read testing { Config: providerConfig + ` -#resource "ctfd_challenge" "example" { -# name = "Example challenge" -# category = "test" -# description = "Example challenge description..." -# value = 500 -#} + resource "ctfd_challenge" "example" { + name = "Example challenge" + category = "test" + description = "Example challenge description..." + value = 500 + } -resource "ctfd_file" "pouet" { - challenge_id = "1" - name = "pouet.txt" - content = "Pouet the 2nd is the clowniest cat ever" -} -`, + resource "ctfd_file" "pouet" { + challenge_id = ctfd_challenge.example.id + name = "pouet.txt" + content = "Pouet the 2nd is the clowniest cat ever" + } + `, Check: resource.ComposeAggregateTestCheckFunc( // resource.TestCheckResourceAttr("ctfd_flag.first", "requirements.#", "0"), // resource.TestCheckResourceAttr("ctfd_flag.second", "requirements.#", "1"), diff --git a/provider/flag_resource.go b/provider/flag_resource.go index d63a4b3..e588e80 100644 --- a/provider/flag_resource.go +++ b/provider/flag_resource.go @@ -48,7 +48,7 @@ func (r *flagResource) Metadata(ctx context.Context, req resource.MetadataReques func (r *flagResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: "", + MarkdownDescription: "A flag to solve the challenge.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ MarkdownDescription: "Identifier of the flag, used internally to handle the CTFd corresponding object.", diff --git a/provider/provider.go b/provider/provider.go index 9fe5529..8b6ce67 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -5,6 +5,7 @@ import ( "os" "github.com/ctfer-io/go-ctfd/api" + "github.com/ctfer-io/terraform-provider-ctfd/provider/utils" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" @@ -171,9 +172,9 @@ func (p *CTFdProvider) Configure(ctx context.Context, req provider.ConfigureRequ // Instantiate CTFd API client ctx = tflog.SetField(ctx, "ctfd_url", url) - ctx = addSensitive(ctx, "ctfd_session", session) - ctx = addSensitive(ctx, "ctfd_nonce", nonce) - ctx = addSensitive(ctx, "ctfd_api_key", apiKey) + ctx = utils.AddSensitive(ctx, "ctfd_session", session) + ctx = utils.AddSensitive(ctx, "ctfd_nonce", nonce) + ctx = utils.AddSensitive(ctx, "ctfd_api_key", apiKey) tflog.Debug(ctx, "Creating CTFd API client") client := api.NewClient(url, session, nonce, apiKey) diff --git a/provider/utils.go b/provider/utils.go deleted file mode 100644 index 7a5036f..0000000 --- a/provider/utils.go +++ /dev/null @@ -1,12 +0,0 @@ -package provider - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -func addSensitive(ctx context.Context, key string, value any) context.Context { - ctx = tflog.SetField(ctx, key, value) - return tflog.MaskFieldValuesWithFieldKeys(ctx, key) -} diff --git a/provider/utils/utils.go b/provider/utils/utils.go index a64c29e..b16bdc9 100644 --- a/provider/utils/utils.go +++ b/provider/utils/utils.go @@ -1,12 +1,18 @@ package utils import ( + "context" "strconv" - "strings" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" ) +func AddSensitive(ctx context.Context, key string, value any) context.Context { + ctx = tflog.SetField(ctx, key, value) + return tflog.MaskFieldValuesWithFieldKeys(ctx, key) +} + // return a null types.Int64 if pointer is nil, else its value func ToTFInt64(i *int) types.Int64 { if i == nil {