From fb85b2970ca0644ffea4b11c9ed3779cd5307317 Mon Sep 17 00:00:00 2001 From: Lucas Tesson Date: Mon, 8 Sep 2025 11:31:16 +0200 Subject: [PATCH] feat: bump CTFd to 3.8.0, add configuration attributes --- .github/workflows/ci.yaml | 2 +- action.yaml | 22 ++++++++++++++ cmd/ctfd-setup/main.go | 61 ++++++++++++++++++++++++++++++++++++++- config.go | 29 +++++++++++++++++++ go.mod | 2 +- go.sum | 4 +-- setup.go | 7 +++++ 7 files changed, 122 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7f26309..3683ced 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest services: ctfd: - image: ctfd/ctfd:3.7.7@sha256:9847758cdafc5ab86bdc121353dcf5a27a29ce313587270ee90a71bfbda2b910 + image: ctfd/ctfd:3.8.0@sha256:61712c4af6e9cddccfafc4503484c8091f6325be286d407595b922dc9552b5ae ports: - 8000:8000 steps: diff --git a/action.yaml b/action.yaml index 46a3ff3..afdd8db 100644 --- a/action.yaml +++ b/action.yaml @@ -48,6 +48,8 @@ inputs: description: 'Whether to allow team creation by players or not.' accounts_team_size: description: 'Maximum size (number of players) in a team.' + accounts_password_min_length: + description: 'Minimal length of password.' accounts_num_teams: description: 'The total number of teams allowed.' accounts_num_users: @@ -58,6 +60,17 @@ inputs: description: 'Maximum number of invalid submissions per minute (per user/team). We suggest you use it as part of an anti-brute-force strategy (rate limiting).' accounts_name_changes: description: 'Whether a user can change its name or not.' + # Challenges + challenges_view_self_submissions: + description: 'Whether a player can see itw own previous submissions.' + challenges_max_attempts_behavior: + description: 'The behavior to adopt in case a player reached the submission rate limiting.' + challenges_max_attempts_timeout: + description: 'The duration of the submission rate limit for further submissions.' + challenges_hints_free_public_access: + description: 'Control whether users must be logged in to see free hints.' + challenges_challenge_ratings: + description: 'Who can see and submit challenge ratings.' # Pages pages_robots_txt: description: 'Define the /robots.txt file content, for web crawlers indexing.' @@ -133,6 +146,8 @@ inputs: # Social social_shares: description: 'Whether to enable users share they solved a challenge or not.' + social_template: + description: 'A template for social shares. Provide a path to a locally-accessible file.' # Legal legal_tos_url: description: 'The Terms of Services URL.' @@ -175,11 +190,17 @@ runs: ACCOUNTS_VERIFY_EMAILS: ${{ inputs.accounts_verify_emails }} ACCOUNTS_TEAM_CREATION: ${{ inputs.accounts_team_creation }} ACCOUNTS_TEAM_SIZE: ${{ inputs.accounts_team_size }} + ACCOUNTS_PASSWORD_MIN_LENGTH: ${{ inputs.accounts_password_min_length }} ACCOUNTS_NUM_TEAMS: ${{ inputs.accounts_num_teams }} ACCOUNTS_NUM_USERS: ${{ inputs.accounts_num_users }} ACCOUNTS_TEAM_DISBANDING: ${{ inputs.accounts_team_disbanding }} ACCOUNTS_INCORRECT_SUBMISSIONS_PER_MINUTE: ${{ inputs.accounts_incorrect_submissions_per_minute }} ACCOUNTS_NAME_CHANGES: ${{ inputs.accounts_name_changes }} + CHALLENGES_VIEW_SELF_SUBMISSIONS: ${{ inputs.challenges_view_self_submissions }} + CHALLENGES_MAX_ATTEMPTS_BEHAVIOR: ${{ inputs.challenges_max_attempts_behavior }} + CHALLENGES_MAX_ATTEMPTS_TIMEOUT: ${{ inputs.challenges_max_attempts_timeout }} + CHALLENGES_HINTS_FREE_PUBLIC_ACCESS: ${{ inputs.challenges_hints_free_public_access }} + CHALLENGES_CHALLENGE_RATINGS: ${{ inputs.challenges_challenge_ratings }} PAGES_ROBOTS_TXT: ${{ inputs.pages_robots_txt }} MAJOR_LEAGUE_CYBER_CLIENT_ID: ${{ inputs.major_league_cyber_client_id }} MAJOR_LEAGUE_CYBER_CLIENT_SECRET: ${{ inputs.major_league_cyber_client_secret }} @@ -212,6 +233,7 @@ runs: TIME_FREEZE: ${{ inputs.time_freeze }} TIME_VIEW_AFTER: ${{ inputs.time_view_after }} SOCIAL_SHARES: ${{ inputs.social_shares }} + SOCIAL_TEMPLATE: ${{ inputs.social_template }} LEGAL_TOS_URL: ${{ inputs.legal_tos_url }} LEGAL_TOS_CONTENT: ${{ inputs.legal_tos_content }} LEGAL_PRIVACY_POLICY_URL: ${{ inputs.legal_privacy_policy_url }} diff --git a/cmd/ctfd-setup/main.go b/cmd/ctfd-setup/main.go index 03bd58c..83b95ba 100644 --- a/cmd/ctfd-setup/main.go +++ b/cmd/ctfd-setup/main.go @@ -155,6 +155,12 @@ func main() { Sources: cli.EnvVars("ACCOUNTS_TEAM_SIZE", "PLUGIN_ACCOUNTS_TEAM_SIZE"), Category: configuration, }, + &cli.IntFlag{ + Name: "accounts.password_min_length", + Usage: "Minimal length of password.", + Sources: cli.EnvVars("ACCOUNTS_PASSWORD_MIN_LENGTH", "PLUGIN_ACCOUNTS_PASSWORD_MIN_LENGTH"), + Category: configuration, + }, &cli.IntFlag{ Name: "accounts.num_teams", Usage: "The total number of teams allowed.", @@ -185,6 +191,39 @@ func main() { Sources: cli.EnvVars("ACCOUNTS_NAME_CHANGES", "PLUGIN_ACCOUNTS_NAME_CHANGES"), Category: configuration, }, + // => Challenges + &cli.BoolFlag{ + Name: "challenges.view_self_submissions", + Usage: "Whether a player can see itw own previous submissions.", + Sources: cli.EnvVars("CHALLENGES_VIEW_SELF_SUBMISSIONS", "PLUGIN_CHALLENGES_VIEW_SELF_SUBMISSIONS"), + Category: configuration, + }, + &cli.StringFlag{ + Name: "challenges.max_attempts_behavior", + Usage: "The behavior to adopt in case a player reached the submission rate limiting.", + Value: "lockout", + Sources: cli.EnvVars("CHALLENGES_MAX_ATTEMPTS_BEHAVIOR", "PLUGIN_CHALLENGES_MAX_ATTEMPTS_BEHAVIOR"), + Category: configuration, + }, + &cli.IntFlag{ + Name: "challenges.max_attempts_timeout", + Usage: "The duration of the submission rate limit for further submissions.", + Sources: cli.EnvVars("CHALLENGES_MAX_ATTEMPTS_TIMEOUT", "PLUGIN_CHALLENGES_MAX_ATTEMPTS_TIMEOUT"), + Category: configuration, + }, + &cli.BoolFlag{ + Name: "challenges.hints_free_public_access", + Usage: "Control whether users must be logged in to see free hints.", + Sources: cli.EnvVars("CHALLENGES_HINTS_FREE_PUBLIC_ACCESS", "PUBLIC_CHALLENGES_HINTS_FREE_PUBLIC_ACCESS"), + Category: configuration, + }, + &cli.StringFlag{ + Name: "challenges.challenge_ratings", + Usage: "Who can see and submit challenge ratings.", + Value: "public", + Sources: cli.EnvVars("CHALLENGES_CHALLENGE_RATINGS", "PUBLIC_CHALLENGES_CHALLENGE_RATINGS"), + Category: configuration, + }, // => Pages &cli.StringFlag{ Name: "pages.robots_txt", @@ -388,6 +427,12 @@ func main() { Sources: cli.EnvVars("SOCIAL_SHARES", "PLUGIN_SOCIAL_SHARES"), Category: configuration, }, + &cli.StringFlag{ + Name: "social.template", + Usage: "A template for social shares. Provide a path to a locally-accessible file.", + Sources: cli.EnvVars("SOCIAL_TEMPLATE", "PUBLIC_SOCIAL_TEMPLATE"), + Category: configuration, + }, // => Legal &cli.StringFlag{ Name: "legal.tos.url", @@ -516,6 +561,11 @@ func run(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } + socialTpl, err := filePtr(cmd, "social.template") + if err != nil { + return err + } + tos, err := filePtr(cmd, "legal.tos.content") if err != nil { return err @@ -545,12 +595,20 @@ func run(ctx context.Context, cmd *cli.Command) error { VerifyEmails: cmd.Bool("accounts.verify_emails"), TeamCreation: boolPtr(cmd, "accounts.team_creation"), TeamSize: intPtr(cmd, "accounts.team_size"), + PasswordMinLength: intPtr(cmd, "accounts.password_min_length"), NumTeams: intPtr(cmd, "accounts.num_teams"), NumUsers: intPtr(cmd, "accounts.num_users"), TeamDisbanding: stringPtr(cmd, "accounts.team_disbanding"), IncorrectSubmissionsPerMinute: intPtr(cmd, "accounts.incorrect_submissions_per_minute"), NameChanges: boolPtr(cmd, "accounts.name_changes"), }, + Challenges: &ctfdsetup.Challenges{ + ViewSelfSubmission: cmd.Bool("challenges.view_self_submissions"), + MaxAttemptsBehavior: cmd.String("challenges.max_attempts_behavior"), + MaxAttemptsTimeout: cmd.Int("challenges.max_attempts_timeout"), + HintsFreePublicAccess: cmd.Bool("challenges.hints_free_public_access"), + ChallengeRatings: cmd.String("challenges.challenge_ratings"), + }, Pages: &ctfdsetup.Pages{ RobotsTxt: robotsTxt, }, @@ -603,7 +661,8 @@ func run(ctx context.Context, cmd *cli.Command) error { ViewAfter: boolPtr(cmd, "time.view_after"), }, Social: &ctfdsetup.Social{ - Shares: boolPtr(cmd, "social.shares"), + Shares: boolPtr(cmd, "social.shares"), + Template: socialTpl, }, Legal: &ctfdsetup.Legal{ TOS: ctfdsetup.ExternalReference{ diff --git a/config.go b/config.go index e7c0a4b..944bd1b 100644 --- a/config.go +++ b/config.go @@ -17,6 +17,7 @@ type ( Appearance Appearance `yaml:"appearance" json:"appearance" jsonschema:"required"` Theme *Theme `yaml:"theme,omitempty" json:"theme,omitempty"` Accounts *Accounts `yaml:"accounts,omitempty" json:"accounts,omitempty"` + Challenges *Challenges `yaml:"challenges,omitempty" json:"challenges,omitempty"` Pages *Pages `yaml:"pages,omitempty" json:"pages,omitempty"` MajorLeagueCyber *MajorLeagueCyber `yaml:"major_league_cyber,omitempty" json:"major_league_cyber,omitempty"` Settings *Settings `yaml:"settings,omitempty" json:"settings,omitempty"` @@ -88,6 +89,9 @@ type ( // Maximum size (number of players) in a team. TeamSize *int `yaml:"team_size,omitempty" json:"team_size,omitempty"` + // Minimal length of passwords. + PasswordMinLength *int `yaml:"password_min_length,omitempty" json:"password_min_length,omitempty"` + // The total number of teams allowed. NumTeams *int `yaml:"num_teams,omitempty" json:"num_teams,omitempty"` @@ -104,6 +108,24 @@ type ( NameChanges *bool `yaml:"name_changes,omitempty" json:"name_changes,omitempty"` } + // Challenge-related configurations. + Challenges struct { + // Whether a player can see itw own previous submissions. + ViewSelfSubmission bool `yaml:"view_self_submissions" json:"view_self_submissions"` + + // The behavior to adopt in case a player reached the submission rate limiting. + MaxAttemptsBehavior string `yaml:"max_attempts_behavior" json:"max_attempts_behavior" jsonschema:"enum=lockout,enum=timeout,default=lockout"` + + // The duration of the submission rate limit for further submissions. + MaxAttemptsTimeout int `yaml:"max_attempts_timeout" json:"max_attempts_timeout"` + + // Control whether users must be logged in to see free hints. + HintsFreePublicAccess bool `yaml:"hints_free_public_access" json:"hints_free_public_access"` + + // Who can see and submit challenge ratings. + ChallengeRatings string `yaml:"challenge_ratings" json:"challenge_ratings" jsonschema:"enum=public,enum=private,enum=disabled,default=public"` + } + // Pages global configuration. Pages struct { // Define the /robots.txt file content, for web crawlers indexing. @@ -238,6 +260,9 @@ type ( Social struct { // Whether to enable users share they solved a challenge or not. Shares *bool `yaml:"shares,omitempty" json:"shares,omitempty"` + + // A template for social shares. + Template *File `yaml:"template,omitempty" json:"template,omitempty"` } // Legal contents for players. @@ -293,6 +318,10 @@ func NewConfig() *Config { Settings: &File{}, }, Accounts: &Accounts{}, + Challenges: &Challenges{ + MaxAttemptsBehavior: "lockout", + ChallengeRatings: "public", + }, Pages: &Pages{ RobotsTxt: &File{}, }, diff --git a/go.mod b/go.mod index 9be37dc..e7fe783 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/ctfer-io/ctfd-setup go 1.22.2 require ( - github.com/ctfer-io/go-ctfd v0.14.0 + github.com/ctfer-io/go-ctfd v0.15.0 github.com/invopop/jsonschema v0.13.0 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index acfdf4b..aa71cc7 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/ctfer-io/go-ctfd v0.14.0 h1:yEETB4oMWzvEJjyWwQPt/rUrNKyLBFx7NB6e2/IcgII= -github.com/ctfer-io/go-ctfd v0.14.0/go.mod h1:ebgSW8LdP/qtRCpglK4djBp+g6kU5YM98XBZKowiCeY= +github.com/ctfer-io/go-ctfd v0.15.0 h1:gdZK83cOIWassyW+PsyMqlSBFxHhsKh+yjU+fsc5A58= +github.com/ctfer-io/go-ctfd v0.15.0/go.mod h1:cUAbm6dv5OhjR3E3/QaJBGTJ/GJ7vsXdL92SmyN/aQo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/setup.go b/setup.go index ea03c1a..17ba7c6 100644 --- a/setup.go +++ b/setup.go @@ -144,7 +144,13 @@ func updateSetup(ctx context.Context, client *api.Client, conf *Config) error { TeamCreation: conf.Accounts.TeamCreation, TeamDisbanding: conf.Accounts.TeamDisbanding, TeamSize: conf.Accounts.TeamSize, + PasswordMinLength: conf.Accounts.PasswordMinLength, VerifyEmails: &conf.Accounts.VerifyEmails, + ViewSelfSubmission: conf.Challenges.ViewSelfSubmission, + MaxAttemptsBehavior: conf.Challenges.MaxAttemptsBehavior, + MaxAttemptsTimeout: conf.Challenges.MaxAttemptsTimeout, + HintsFreePublicAccess: conf.Challenges.HintsFreePublicAccess, + ChallengeRatings: conf.Challenges.ChallengeRatings, RobotsTxt: ptr(string(conf.Pages.RobotsTxt.Content)), OauthClientID: conf.MajorLeagueCyber.ClientID, OauthClientSecret: conf.MajorLeagueCyber.ClientSecret, @@ -180,6 +186,7 @@ func updateSetup(ctx context.Context, client *api.Client, conf *Config) error { Freeze: conf.Time.Freeze, ViewAfterCTF: conf.Time.ViewAfter, SocialShares: conf.Social.Shares, + SocialSharesTemplate: string(conf.Social.Template.Content), PrivacyURL: conf.Legal.PrivacyPolicy.URL, PrivacyText: ptr(string(conf.Legal.PrivacyPolicy.Content.Content)), TOSURL: conf.Legal.TOS.URL,