diff --git a/api/client.go b/api/client.go index a94fad5..c5f6731 100644 --- a/api/client.go +++ b/api/client.go @@ -136,7 +136,7 @@ func call(client *Client, req *http.Request, dst any, opts ...Option) error { } // Handle errors if any - if len(resp.Errors) != 0 { + if resp.Errors != nil { return fmt.Errorf("CTFd responded with errors: %v", resp.Errors) } if !resp.Success { @@ -151,10 +151,10 @@ func call(client *Client, req *http.Request, dst any, opts ...Option) error { } type Response struct { - Success bool `json:"success"` - Data any `json:"data,omitempty"` - Errors []string `json:"errors,omitempty"` - Message *string `json:"message,omitempty"` + Success bool `json:"success"` + Data any `json:"data,omitempty"` + Errors any `json:"errors,omitempty"` // can't type it to []string due to API model instabilities + Message *string `json:"message,omitempty"` } func get(client *Client, edp string, params any, dst any, opts ...Option) error { diff --git a/api/model.go b/api/model.go index 57bfa62..7cff9ed 100644 --- a/api/model.go +++ b/api/model.go @@ -149,23 +149,23 @@ type ( } Team struct { - Bracket *string `json:"bracket"` - Members []int `json:"members,omitempty"` - ID int `json:"id"` - Created string `json:"created"` - Country *string `json:"country"` - Email *string `json:"email"` - Affiliation *string `json:"affiliation"` - CaptainID *int `json:"captain_id"` - Fields []Field `json:"fields"` - Banned bool `json:"banned"` - Website *string `json:"website"` - Hidden bool `json:"hidden"` - Secret *bool `json:"secret"` - Name string `json:"name"` - OauthID *string `json:"oauth_id"` - Place *string `json:"place,omitempty"` - Score *int `json:"score,omitempty"` + Bracket *string `json:"bracket"` + Members []int `json:"members,omitempty"` + ID int `json:"id"` + Created string `json:"created"` + Country *string `json:"country"` + Email *string `json:"email"` + Affiliation *string `json:"affiliation"` + CaptainID *int `json:"captain_id"` + Fields []string `json:"fields"` + Banned bool `json:"banned"` + Website *string `json:"website"` + Hidden bool `json:"hidden"` + Secret *bool `json:"secret"` + Name string `json:"name"` + OauthID *string `json:"oauth_id"` + Place *string `json:"place,omitempty"` + Score *int `json:"score,omitempty"` } User struct { diff --git a/api/setup_test.go b/api/setup_test.go index 013f624..8fd33d1 100644 --- a/api/setup_test.go +++ b/api/setup_test.go @@ -317,3 +317,132 @@ func Test_F_AdvancedSetup(t *testing.T) { return } } + +func Test_F_UsersAndTeams(t *testing.T) { + // Scenario: + // + // As an Ops, your job is to import all the registered users and teams + // before the event such that at the very beginning you are sure no one + // is lost. + + assert := assert.New(t) + + // 1a. Get nonce and session to mock a browser first + nonce, session, err := api.GetNonceAndSession(CTFD_URL) + if !assert.Nil(err, "got error: %s", err) { + return + } + client := api.NewClient(CTFD_URL, nonce, session, "") + + t.Cleanup(func() { + _ = client.Reset(&api.ResetParams{ + Accounts: ptr("y"), + Submissions: ptr("y"), + Challenges: ptr("y"), + Pages: ptr("y"), + Notifications: ptr("y"), + }) + }) + + // 1b. Configure the CTF + err = client.Setup(&api.SetupParams{ + CTFName: "CTFer", + CTFDescription: "Ephemeral CTFd running for API tests purposes.", + UserMode: "teams", + Name: "ctfer", + Email: "ctfer-io@protonmail.com", + Password: "password", // This is not real, don't bother trying x) + ChallengeVisibility: "admins", + AccountVisibility: "private", + ScoreVisibility: "hidden", + RegistrationVisibility: "mlc", + VerifyEmails: false, + TeamSize: ptr(4), + CTFLogo: nil, + CTFBanner: nil, + CTFSmallIcon: nil, + CTFTheme: "core", + ThemeColor: "", + Start: "", + End: "", + Nonce: nonce, + }) + if !assert.Nil(err, "got error: %s", err) { + return + } + + // 1c. Create an API Key to avoid session/nonce+cookies dance + token, err := client.PostTokens(&api.PostTokensParams{ + Expiration: "2222-01-01", + Description: "Example API token.", + }) + if !assert.Nil(err, "got error: %s", err) { + return + } + client.SetAPIKey(*token.Value) + + // Define all users and teams + type User struct { + name, email, password string + } + type Team struct { + name, email, password string + users []User + } + var teams = []Team{ + { + name: "MILF CTF Team", + email: "milfctf@example.com", + password: "password", + users: []User{ + { + name: "hashp4", + email: "hashp4@example.com", + password: "password", + }, + // ... + }, + }, + } + + // 2. Create all the users and their teams + for _, team := range teams { + // 2a. Create team + tm, err := client.PostTeams(&api.PostTeamsParams{ + Name: team.name, + Email: team.email, + Password: team.password, + Banned: false, + Hidden: false, + Fields: []api.Field{}, + }) + if !assert.Nil(err, "got error: %s", err) { + return + } + + for _, user := range team.users { + // 2b. Create user + usr, err := client.PostUsers(&api.PostUsersParams{ + Name: user.name, + Email: user.email, + Password: user.password, + Type: "user", + Verified: false, + Hidden: false, + Banned: false, + Fields: []api.Field{}, + }) + if !assert.Nil(err, "got error: %s", err) { + return + } + + // 2c. Join user to team + _, err = client.PostTeamMembers(tm.ID, &api.PostTeamsMembers{ + UserID: usr.ID, + }) + if !assert.Nil(err, "got error: %s", err) { + return + } + } + } +} diff --git a/api/teams.go b/api/teams.go index c2c9ffb..3bcd6a3 100644 --- a/api/teams.go +++ b/api/teams.go @@ -19,12 +19,15 @@ func (client *Client) GetTeams(params *GetTeamsParams, opts ...Option) ([]*Team, } type PostTeamsParams struct { - Name string `json:"name"` - Password string `json:"password"` - Email string `json:"email"` - Banned bool `json:"banned"` - Hidden bool `json:"hidden"` - Fields []string `json:"fields"` + Name string `json:"name"` + Email string `json:"email"` + Password string `json:"password"` + Website *string `json:"website,omitempty"` + Affiliation *string `json:"affiliation,omitempty"` + Country *string `json:"country,omitempty"` + Banned bool `json:"banned"` + Hidden bool `json:"hidden"` + Fields []Field `json:"fields"` } func (client *Client) PostTeams(params *PostTeamsParams, opts ...Option) (*Team, error) { @@ -48,11 +51,16 @@ func (client *Client) DeleteTeamsMe(opts ...Option) error { } type PatchTeamsParams struct { - CaptainID *int `json:"captain_id,omitempty"` - Banned *bool `json:"banned,omitempty"` - Fields []Field `json:"fields,omitempty"` - Hidden *bool `json:"hidden,omitempty"` - Name *string `json:"name,omitempty"` + CaptainID *int `json:"captain_id,omitempty"` + Name *string `json:"name,omitempty"` + Email *string `json:"email,omitempty"` + Password *string `json:"password,omitempty"` + Website *string `json:"website,omitempty"` + Affiliation *string `json:"affiliation,omitempty"` + Country *string `json:"country,omitempty"` + Banned *bool `json:"banned,omitempty"` + Hidden *bool `json:"hidden,omitempty"` + Fields []Field `json:"fields"` } func (client *Client) PatchTeamsMe(params *PatchTeamsParams, opts ...Option) (*Team, error) { @@ -132,12 +140,13 @@ func (client *Client) DeleteTeamMembers(id int, params *DeleteTeamMembersParams, return v, nil } -func (client *Client) PostTeamMembers(id int, params *PostTeamsMembers, opts ...Option) (*Team, error) { - team := &Team{} +func (client *Client) PostTeamMembers(id int, params *PostTeamsMembers, opts ...Option) (int, error) { + // Use slice as a workaround due to API instabilities + var team []int if err := post(client, fmt.Sprintf("/teams/%d/members", id), params, &team, opts...); err != nil { - return nil, err + return 0, err } - return team, nil + return team[0], nil } func (client *Client) GetTeamAwards(id int, opts ...Option) ([]*Award, error) { diff --git a/api/users.go b/api/users.go index 6f98e39..cd294fc 100644 --- a/api/users.go +++ b/api/users.go @@ -20,14 +20,18 @@ func (client *Client) GetUsers(params *GetUsersParams, opts ...Option) ([]*User, } type PostUsersParams struct { - Name string `json:"name"` - Password string `json:"password"` - Email string `json:"email"` - Type string `json:"type"` - Banned bool `json:"banned"` - Hidden bool `json:"hidden"` - Verified bool `json:"verified"` - Fields []string `json:"fields"` + Name string `json:"name"` + Email string `json:"email"` + Language *string `json:"language,omitempty"` + Password string `json:"password"` + Website *string `json:"website,omitempty"` + Affiliation *string `json:"affiliation,omitempty"` + Country *string `json:"country,omitempty"` + Type string `json:"type"` // "user" or "admin" + Verified bool `json:"verified"` + Hidden bool `json:"hidden"` + Banned bool `json:"banned"` + Fields []Field `json:"fields"` } func (client *Client) PostUsers(params *PostUsersParams, opts ...Option) (*User, error) { @@ -49,7 +53,15 @@ func (client *Client) GetUsersMe(opts ...Option) (*User, error) { type PatchUsersParams struct { Name string `json:"name"` Email string `json:"email"` - Affiliation string `json:"affiliation"` + Language *string `json:"language,omitempty"` + Password *string `json:"password,omitempty"` + Website *string `json:"website,omitempty"` + Affiliation *string `json:"affiliation,omitempty"` + Country *string `json:"country,omitempty"` + Type string `json:"type"` + Verified bool `json:"verified"` + Hidden bool `json:"hidden"` + Banned bool `json:"banned"` Fields []Field `json:"fields"` }