-
Notifications
You must be signed in to change notification settings - Fork 31
Add per-leaderboard allowed_users gating for private hackathons #454
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # Private Hackathon Whitelist (Quick Ops) | ||
|
|
||
| Use `allowed_users` to make a leaderboard opt-in. | ||
|
|
||
| ## Semantics | ||
|
|
||
| - `allowed_users = null` → leaderboard is open to everyone | ||
| - `allowed_users = []` → whitelist is enabled but empty (nobody can submit) | ||
| - `allowed_users = ["alice", "bob"]` → only those usernames can submit | ||
|
|
||
| ## Setup | ||
|
|
||
| ```bash | ||
| export API_URL="https://discord-cluster-manager-1f6c4782e60a.herokuapp.com" | ||
| export TOKEN="<admin-token>" | ||
| ``` | ||
|
|
||
| ## Commands | ||
|
|
||
| ### View current whitelist | ||
|
|
||
| ```bash | ||
| curl -s "$API_URL/admin/leaderboards/helion-matmul/allowed-users" \ | ||
| -H "Authorization: Bearer $TOKEN" | python3 -m json.tool | ||
| ``` | ||
|
|
||
| ### Replace full whitelist | ||
|
|
||
| ```bash | ||
| curl -X PUT "$API_URL/admin/leaderboards/helion-matmul/allowed-users" \ | ||
| -H "Authorization: Bearer $TOKEN" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"usernames": ["alice", "bob", "charlie"]}' | ||
| ``` | ||
|
|
||
| ### Append users | ||
|
|
||
| ```bash | ||
| curl -X POST "$API_URL/admin/leaderboards/helion-matmul/allowed-users" \ | ||
| -H "Authorization: Bearer $TOKEN" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"usernames": ["dave", "eve"]}' | ||
| ``` | ||
|
|
||
| ### Remove users | ||
|
|
||
| ```bash | ||
| curl -X DELETE "$API_URL/admin/leaderboards/helion-matmul/allowed-users" \ | ||
| -H "Authorization: Bearer $TOKEN" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"usernames": ["eve"]}' | ||
| ``` | ||
|
|
||
| ### Re-open leaderboard (disable whitelist) | ||
|
|
||
| ```bash | ||
| curl -X PUT "$API_URL/admin/leaderboards/helion-matmul/allowed-users" \ | ||
| -H "Authorization: Bearer $TOKEN" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"usernames": null}' | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -588,7 +588,7 @@ def get_leaderboard_templates(self, leaderboard_name: str) -> Dict[str, str]: | |
| def get_leaderboard(self, leaderboard_name: str) -> "LeaderboardItem": | ||
| self.cursor.execute( | ||
| """ | ||
| SELECT id, name, deadline, task, creator_id, forum_id, secret_seed, description | ||
| SELECT id, name, deadline, task, creator_id, forum_id, secret_seed, description, allowed_users | ||
| FROM leaderboard.leaderboard | ||
| WHERE name = %s | ||
| """, | ||
|
|
@@ -599,7 +599,7 @@ def get_leaderboard(self, leaderboard_name: str) -> "LeaderboardItem": | |
|
|
||
| if res: | ||
| task = LeaderboardTask.from_dict(res[3]) | ||
| return LeaderboardItem( | ||
| item = LeaderboardItem( | ||
| id=res[0], | ||
| name=res[1], | ||
| deadline=res[2], | ||
|
|
@@ -610,9 +610,76 @@ def get_leaderboard(self, leaderboard_name: str) -> "LeaderboardItem": | |
| gpu_types=self.get_leaderboard_gpu_types(res[1]), | ||
| description=res[7], | ||
| ) | ||
| if res[8] is not None: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make an explicit get function and fetch this only on-demand? |
||
| item["allowed_users"] = list(res[8]) | ||
| return item | ||
| else: | ||
| raise LeaderboardDoesNotExist(leaderboard_name) | ||
|
|
||
| def set_allowed_users(self, leaderboard_name: str, usernames: Optional[List[str]]) -> None: | ||
| self.cursor.execute( | ||
| """ | ||
| UPDATE leaderboard.leaderboard | ||
| SET allowed_users = %s | ||
| WHERE name = %s | ||
| """, | ||
| (usernames, leaderboard_name), | ||
| ) | ||
| if self.cursor.rowcount == 0: | ||
| raise LeaderboardDoesNotExist(leaderboard_name) | ||
| self.connection.commit() | ||
|
|
||
| def append_allowed_users(self, leaderboard_name: str, usernames: List[str]) -> List[str]: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fancy SQL vs just processing this on the python side? |
||
| self.cursor.execute( | ||
| """ | ||
| UPDATE leaderboard.leaderboard | ||
| SET allowed_users = CASE | ||
| WHEN allowed_users IS NULL THEN %s | ||
| ELSE ( | ||
| SELECT ARRAY( | ||
| SELECT DISTINCT elem | ||
| FROM unnest(allowed_users || %s::text[]) AS elem | ||
| ORDER BY elem | ||
| ) | ||
| ) | ||
| END | ||
| WHERE name = %s | ||
| RETURNING allowed_users | ||
| """, | ||
| (usernames, usernames, leaderboard_name), | ||
| ) | ||
| res = self.cursor.fetchone() | ||
| if res is None: | ||
| raise LeaderboardDoesNotExist(leaderboard_name) | ||
| self.connection.commit() | ||
| return list(res[0]) if res[0] is not None else [] | ||
|
|
||
| def remove_allowed_users(self, leaderboard_name: str, usernames: List[str]) -> List[str]: | ||
| self.cursor.execute( | ||
| """ | ||
| UPDATE leaderboard.leaderboard | ||
| SET allowed_users = CASE | ||
| WHEN allowed_users IS NULL THEN NULL | ||
| ELSE ( | ||
| SELECT ARRAY( | ||
| SELECT elem | ||
| FROM unnest(allowed_users) AS elem | ||
| WHERE NOT (elem = ANY(%s::text[])) | ||
| ORDER BY elem | ||
| ) | ||
| ) | ||
| END | ||
| WHERE name = %s | ||
| RETURNING allowed_users | ||
| """, | ||
| (usernames, leaderboard_name), | ||
| ) | ||
| res = self.cursor.fetchone() | ||
| if res is None: | ||
| raise LeaderboardDoesNotExist(leaderboard_name) | ||
| self.connection.commit() | ||
| return list(res[0]) if res[0] is not None else None | ||
|
|
||
| def get_leaderboard_submissions( | ||
| self, | ||
| leaderboard_name: str, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| """ | ||
| add-allowed-users | ||
| """ | ||
|
|
||
| from yoyo import step | ||
|
|
||
| __depends__ = {"20260226_01_WgYAV-queryindex"} | ||
|
|
||
|
|
||
| steps = [ | ||
| step( | ||
| """ | ||
| ALTER TABLE leaderboard.leaderboard | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whats the design trade-off here on having a column vs having an extra table?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Column approach (TEXT[]) is simpler for this use case: the allowlist is always read/written as a whole unit, the list is small (tens of users max), and it avoids the extra join on every submission check. A separate table would make sense if we needed per-user metadata (e.g., added_by, added_at), needed to query across leaderboards ("which leaderboards is alice on?"), or if the lists grew large. For now the column keeps things simple. |
||
| ADD COLUMN allowed_users TEXT[] DEFAULT NULL; | ||
| """, | ||
| """ | ||
| ALTER TABLE leaderboard.leaderboard | ||
| DROP COLUMN IF EXISTS allowed_users; | ||
| """, | ||
| ), | ||
| ] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
slop; that's the helper above