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

multi-category challenge / tags support #122

Closed
noraj opened this issue Sep 24, 2017 · 15 comments
Closed

multi-category challenge / tags support #122

noraj opened this issue Sep 24, 2017 · 15 comments
Milestone

Comments

@noraj
Copy link
Contributor

noraj commented Sep 24, 2017

Issue type : enhancement

I know this is may be a breaking feature but it would be nice to support multi-category challenge (or tags support).

For example some challenge are 100% crypto, but some others can be:

  • Network / Forensics
  • Web / Crypto
  • Misc / Network
  • reverse / ppc

This involve change in the database but also in the way you display challenge. This won't be possible to display each challenge in only one category but instead a view like that is needed :

ASIS CTF 2017:

ASIS CTF 2014:

@rbclark
Copy link
Contributor

rbclark commented Sep 24, 2017

This would require a pretty decent change to the core structure of how the scoreboard is structured. We normally just choose the category that seems most relevant and mention in the description if it overlaps with another category. The easiest way to do this would probably be just allow a challenge to have many categories. That being said I think this is a bit out of scope of how we normally use the scoreboard, not to say we wouldn't consider a PR for this.

@noraj
Copy link
Contributor Author

noraj commented Sep 25, 2017

Maybe an in between solution with fewer changes is:

  • keep 1 challenge = 1 category
  • keep the current scoreboard
  • add tags for challenges
  • add an alternative scoreboard view where challenges can be displayed by tags instead of by category

@rbclark
Copy link
Contributor

rbclark commented Sep 25, 2017

I am not personally opposed to just changing the default view of the scoreboard to have an overall more descriptive design such as the one from the 2014 ASIS CTF. That is probably the easiest route to take since it really requires minimal backend work and a frontend redesign. Redesigning the table to show a title, point value and tags is a pretty decent option.

@noraj
Copy link
Contributor Author

noraj commented Sep 25, 2017

Tags should work exactly the same way as categories but with multiple value support. And yes changes in the game view. I'm afraid I won't be helpful as I'm unskilled in Rails or web app design right now. This may change in the future when I'll begin to learn RoR. PS : let me know if I can help in any way.

@noraj
Copy link
Contributor Author

noraj commented Feb 13, 2018

I find a way to easy do that with filtrify.

  • Server side
    • store a category type tag in BDD (web, forensics, reverse, etc... let admin free to create them) + store a difficulty tag (easy, medium, expert, etc... met admin free to create them)
    • inject category type and difficulty tags in the template
  • Client side
    • use filtrify

That sounds clean and easy.

@noraj
Copy link
Contributor Author

noraj commented Oct 2, 2018

categories are already in a separate table, so challenges refer to them with an id in column category_id. Right now it is only mono value, there is just to create a new table challenges_categories and remove category_id in table challenges. That is the only change needed on the Database side.

One item is going to have many tags. And one tag will belong to many items. This implies to me that you'll quite possibly need an intermediary table to overcome the many-to-many obstacle.

Reference

Table: challenges
Columns: id, name, description, point_value, achievement_name, created_at, updated_at, state

Table: categories
Columns: id, name, game_id, created_at, updated_at

Table: challenges_categories
Columns: challenge_id, category_id, created_at, updated_at

For challenges_categories, make challenge_id, category_id couple as primary key and challenges_categories.challenge_id foreign key of challenges.id and challenges_categories.category_id foreign key of categories.id.

This table schema is compatible with both single category and multiple categories view.

Also I noticed that actually challenges.category_id is not a foreign key of categories.id

Then it needs small changes on models and views.

Lemme initiate the changes on schema.rb but I'm quite lost for views and models.

@noraj
Copy link
Contributor Author

noraj commented Oct 2, 2018

Running bundle exec rake db:migrate add

  create_table "challenges_categories", primary_key: ["challenge_id", "category_id"], force: :cascade do |t|
    t.integer "challenge_id", null: false
    t.integer "category_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

in schema.rb.

But I see that active record is not automatically adding foreign keys.

I'm not sure how to add them. https://edgeguides.rubyonrails.org/active_record_migrations.html#foreign-keys

I really want to help for this issue but I'm not a dev, so if someone can help me?

@rbclark
Copy link
Contributor

rbclark commented Oct 3, 2018

Rails generators are probably your friend here: https://stackoverflow.com/questions/39937839/add-a-reference-column-migration-in-rails-5/39937840

Also if you look through our old migrations, some will probably come in handy: https://github.com/mitre-cyber-academy/ctf-scoreboard/blob/master/db/migrate/20170510052137_add_division_to_teams.rb

@noraj
Copy link
Contributor Author

noraj commented Oct 21, 2018

I ran bundle exec rails g migration CreateChallengesCategories challenges_categories:references

so it created db/migrate/20181021171502_create_challenges_categories.rb.

As I see in 20170510052137_add_division_to_teams.rb only the table name is stated not the column name, so I guess the association is done automatically, ex: challenges_categories.challenge_id <=> challenges.id, based on column name.

That was later confirmed:

This adds a new foreign key to the author_id column of the articles table. The key references the id column of the authors table.

So I modified the migration in:

class CreateChallengesCategories < ActiveRecord::Migration[5.1]
  def change
    create_table :challenges_categories, primary_key: ["challenge_id", "category_id"], force: :cascade do |t|
      t.integer :challenge_id, null: false
      t.integer :category_id, null: false
      t.datetime :created_at, null: false
      t.datetime :updated_at, null: false
      t.references :challenges_categories, :challenges, foreign_key: true
      t.references :challenges_categories, :categories, foreign_key: true
    end
  end
end

Then I had error running bundle exec rake db:migrate so I understood that I need to put the references in challenges and categories tables, not in challenges_categories table.

Then I saw that bundle exec rake db:migrate was only reading 20181021171502 CreateChallengesCategories so I guessed that I need to update previous migrations with a rails command.

Then I read https://edgeguides.rubyonrails.org/active_record_migrations.html#changing-existing-migrations. So instead of changing existing migrations, I'll create a new one to do the change.

So I followed https://edgeguides.rubyonrails.org/active_record_migrations.html#foreign-keys

So I ran bundle exec rails g migration AddChallengeRefToChallengesCategories challenges:references that generated db/migrate/20181021174059_add_challenge_ref_to_challenges_categories.rb

class AddChallengeRefToChallengesCategories < ActiveRecord::Migration[5.1]
  def change
    add_reference :challenges_categories, :challenges, foreign_key: true
  end
end

and bundle exec rails g migration AddCategoryRefToChallengesCategories categories:references that generated db/migrate/20181021174246_add_category_ref_to_challenges_categories.rb

class AddCategoryRefToChallengesCategories < ActiveRecord::Migration[5.1]
  def change
    add_reference :challenges_categories, :categories, foreign_key: true
  end
end

Finally I ran bundle exec rails g migration RemoveCategoryIdFromChallenges category_id:int4 that generated `db/migrate/20181021174826_remove_category_id_from_challenges.rb

class RemoveCategoryIdFromChallenges < ActiveRecord::Migration[5.1]
  def change
    remove_column :challenges, :category_id, :int4
  end
end
$ bundle exec rake db:migrate
== 20181021171502 CreateChallengesCategories: migrating =======================
-- create_table(:challenges_categories, {:primary_key=>["challenge_id", "category_id"], :force=>:cascade})
   -> 0.3052s
== 20181021171502 CreateChallengesCategories: migrated (0.3053s) ==============

$ bundle exec rake db:migrate                                                              
== 20181021174059 AddChallengeRefToChallengesCategories: migrating ============
-- add_reference(:challenges_categories, :challenges, {:foreign_key=>true})
   -> 0.4305s
== 20181021174059 AddChallengeRefToChallengesCategories: migrated (0.4306s) ===

== 20181021174246 AddCategoryRefToChallengesCategories: migrating =============
-- add_reference(:challenges_categories, :categories, {:foreign_key=>true})
   -> 0.2056s
== 20181021174246 AddCategoryRefToChallengesCategories: migrated (0.2058s) ====

== 20181021174826 RemoveCategoryIdFromChallenges: migrating ===================
-- remove_column(:challenges, :category_id, :int4)
   -> 0.0071s
== 20181021174826 RemoveCategoryIdFromChallenges: migrated (0.0072s) ==========

Finally thanks to DBeaver I checked and saw that the proper FK were added to challenges_categories.

So checking again schema.db here are the changes:

  create_table "challenges_categories", primary_key: ["challenge_id", "category_id"], force: :cascade do |t|
    t.integer "challenge_id", null: false
    t.integer "category_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.bigint "challenges_id"
    t.bigint "categories_id"
    t.index ["categories_id"], name: "index_challenges_categories_on_categories_id"
    t.index ["challenges_id"], name: "index_challenges_categories_on_challenges_id"
  end

[...]

  add_foreign_key "challenges_categories", "categories", column: "categories_id"
  add_foreign_key "challenges_categories", "challenges", column: "challenges_id"

But that's wrong because rails is unaware of plurals between category and categories so it created new columns.

So from here https://edgeguides.rubyonrails.org/active_record_migrations.html#changing-existing-migrations, I saw I add to run rails db:rollback, so I ran it 3 times:

$ rails db:rollback
== 20181021174826 RemoveCategoryIdFromChallenges: reverting ===================
-- add_column(:challenges, :category_id, :int4)
   -> 0.0008s
== 20181021174826 RemoveCategoryIdFromChallenges: reverted (0.0202s) ==========

$ rails db:rollback
== 20181021174246 AddCategoryRefToChallengesCategories: reverting =============
-- remove_reference(:challenges_categories, :categories, {:foreign_key=>true})
   -> 0.0043s
== 20181021174246 AddCategoryRefToChallengesCategories: reverted (0.0067s) ====

$ rails db:rollback
== 20181021174059 AddChallengeRefToChallengesCategories: reverting ============
-- remove_reference(:challenges_categories, :challenges, {:foreign_key=>true})
   -> 0.0038s
== 20181021174059 AddChallengeRefToChallengesCategories: reverted (0.0060s) ===

Then I manually modified the two wrong migrations. Thx to teh doc:

If the column names can not be derived from the table names, you can use the :column and :primary_key options.

I had to read https://api.rubyonrails.org/v5.1/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference

So I tried again with:

class AddChallengeRefToChallengesCategories < ActiveRecord::Migration[5.1]
  def change
    add_reference :challenges_categories, :challenge, foreign_key: {to_table: :challenges}
  end
end

and

class AddCategoryRefToChallengesCategories < ActiveRecord::Migration[5.1]
  def change
    add_reference :challenges_categories, :category, foreign_key: {to_table: :categories}
  end
end

But I get PG::DuplicateColumn: ERROR: column "challenge_id" of relation "challenges_categories" already exists when running bundle exec rake db:migrate.

So I need to 1) add only a reference only, not create the column and the reference or 2) rollback again and modify my challenges_categories creation.

I did a mix of both, I rollbacked, create normal columns, and then used add_foreign_key with :column instead of add_reference. https://api.rubyonrails.org/v5.1/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key

Then with dbeaver I generated mock data randomly and they all respected the constraints. I think I'm good to publish the database change for this feature.

noraj added a commit to noraj/ctf-scoreboard-1 that referenced this issue Oct 21, 2018
I created the new rails migrations to reflect the database changes needed
for the multi-category feature
see mitre-cyber-academy#122
noraj added a commit to noraj/ctf-scoreboard-1 that referenced this issue Oct 21, 2018
I created the new rails migrations to reflect the database changes needed
for the multi-category feature
see mitre-cyber-academy#122
@noraj
Copy link
Contributor Author

noraj commented Oct 21, 2018

@rbclark Check my PR #163 and tell me if it is good. Then, please, help me for the front-end and back-end part.

@noraj
Copy link
Contributor Author

noraj commented Jan 27, 2019

@rbclark bump

@rbclark
Copy link
Contributor

rbclark commented Jan 28, 2019

I'm not really sure the vision is clear here, you originally mentioned wanting to add multi tag support but all of your changes are focused around changing the actual backing data. I'd argue that it is more important to figure out what the frontend should look like and then shape the data based on that, not try to design the backend first. Once the frontend look is more figured out the backend redesign should be a lot easier. The backend is probably close enough for now.

@noraj
Copy link
Contributor Author

noraj commented Jan 28, 2019

I thought about:

  • 2 scoreboard design: the actual classic one, and the tag based new one
  • on the top of the scoreboard page add a switch allowing to switch from one view to the other
  • tag based scoreboard: ignore the category of the challenge, one challenge could have one or more tags, display them by tag (for example alphabetically or an arbitrary convention) something like the below screenshot

So now we have something looking like ASIS 2014, we can add a client-side javascript filter like filtryfy or any other lib doing the same thing, to allow user to display only challenges they want to see. See this demo. The Multiple categories filter can be like that:

  • 1st category: filter by tag where the user can select one or several type of challenge
  • 2nd category: challenge that the team flagged or not
  • 3rd (optional): we can use two type of tags (type of challenges and difficulty): so same as 1st category but for difficulty

I would already have developed all of that if I was capable of but I'm not a web dev (nor a dev at all) and I never touched MVC. And even before developing I need that some from the team validate the idea.

@rbclark
Copy link
Contributor

rbclark commented Jan 28, 2019

I think if we're going to change the design of the scoreboard we might as well just change the design for everyone. I would be fine with just changing the scoreboard over to a design with more information provided up front and getting rid the category columns in favor of showing them above each challenge with tags.

@rbclark
Copy link
Contributor

rbclark commented Jul 1, 2020

This feature has been incorporated into the scoreboard, and multiple categories are now supported. Please feel free to contribute a layout if none of the built in ones are similar to what you are looking for.

@rbclark rbclark closed this as completed Jul 1, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants