21
53
Fork
You've already forked sciop
23

WIP - scopes refactor for multiple dataset owners #456

Open
goblina wants to merge 37 commits from dataset-scopes into main
pull from: dataset-scopes
merge into: Safeguarding:main
Safeguarding:main
Safeguarding:filter-items
Safeguarding:cli-trackers
Safeguarding:mkdocs-icon-link
Safeguarding:create_time_index
Safeguarding:random-quote-config
Safeguarding:faster-counter
Safeguarding:bugfix-config-defaults
Safeguarding:spotlight-svg-convert
Safeguarding:crawlercatcher
Safeguarding:performance-logging
Safeguarding:allow-replacing-identical-ih-torrents
Safeguarding:disable-animation
Safeguarding:rss-feed-cache-2
Safeguarding:mark-ui
Safeguarding:src-icon
Safeguarding:unapprove
Safeguarding:docs-usage
Safeguarding:footer-md
Safeguarding:rssfeedadditions
Safeguarding:magnet-link-when-collapsed
Safeguarding:hit-counter
Safeguarding:css-wrapping
Safeguarding:config-yaml
Safeguarding:obscure-email
Safeguarding:aboutpage
Safeguarding:more-torrent-feeds
Safeguarding:bugfix-nested-sort
Safeguarding:docs-griffe-pydantic
Safeguarding:rsscache
Safeguarding:bugfix-sort-numbers
Safeguarding:sort-lists
Safeguarding:bump-limits
Safeguarding:magnet-links
Safeguarding:ttl-url-fix
Safeguarding:sort-dataset-parts
Safeguarding:nixify
Safeguarding:docs-no-build-in-prod
Safeguarding:docs-api-data-structures
Safeguarding:docs-webscraping-cleaning
Safeguarding:perf
Safeguarding:logging-400s
Safeguarding:astatide/ISSUE-287-torrent-size-migration
Safeguarding:bugfix-search-syntax
Safeguarding:hotfix-version-uq-bug
Safeguarding:tests-playwright
Safeguarding:tests-warnings-cleanup
Safeguarding:docs-bittorrent
Safeguarding:contact
Safeguarding:request-scope
Safeguarding:ignore-padfiles
Safeguarding:datetime-inputs
Safeguarding:markdown-blockquotes
Safeguarding:lifespan-run-once
Safeguarding:http-scrape
Safeguarding:docs-rss
Safeguarding:continuous-deployment
Safeguarding:dataset-parts-description-fix
Safeguarding:the-most-minor-fix-ever
Safeguarding:css-uploads-table-width
Safeguarding:bugfix-response-type
Safeguarding:site-stats
Safeguarding:patch-null-identifier-uri
Safeguarding:ci-python-matrix
Safeguarding:bugfix-torf-max-size
Safeguarding:docs-index-links
Safeguarding:db-case-insensitive-username
Safeguarding:frontend-limits
Safeguarding:docs-clients
Safeguarding:bugfig-watch-dirs
Safeguarding:docs-bassline
Safeguarding:db-fk-updates
Safeguarding:bittorrent-source-type
Safeguarding:frontpage-beta
Safeguarding:cli-generate
Safeguarding:bugfix-duplicate-torrents
Safeguarding:bugfix-form-bools
Safeguarding:bugfix-pk-migration
Safeguarding:rebuild-search-idx
Safeguarding:cli
Safeguarding:editing
Safeguarding:rm-mimetype-checking
Safeguarding:schoeneh-patch-1
Safeguarding:rm-model-formitem-followup
Safeguarding:bugfix-last-seen-at
Safeguarding:bugfix-upload-torrent
Safeguarding:ci-solaris-runner
Safeguarding:markdown-touchups
Safeguarding:torrent-scrape-servie
Safeguarding:astatide/apscheduler
Safeguarding:error-handling-non-torrent-uploads
Safeguarding:filter-user-page
Safeguarding:frontpage-message
Safeguarding:db-migrations-actually
Safeguarding:delete-buttons
Safeguarding:upload-without-scope
Safeguarding:dont-delete-things-self
Safeguarding:minor-patch
Safeguarding:htmx-source-map
Safeguarding:markdown-descriptions
Safeguarding:front-page-text
Safeguarding:astatide/upload-db-seeders-leechers-update
Safeguarding:astatide/torrent-stat
No description provided.
goblina changed title from (削除) WIP - scops refactor for multiple dataset owners (削除ここまで) to WIP - scopes refactor for multiple dataset owners 2025年08月06日 02:54:58 +02:00
goblina force-pushed dataset-scopes from cf6c42710d
Some checks failed
lint / lint (pull_request) Failing after 12s
tests / tests-python3.11 (pull_request) Failing after 56s
tests / tests-python3.13 (pull_request) Failing after 56s
tests / tests-python3.12 (pull_request) Failing after 59s
tests / tests-slow (pull_request) Successful in 51s
tests / tests-playwright-python3.11 (pull_request) Failing after 1m53s
tests / tests-playwright-python3.13 (pull_request) Failing after 1m57s
tests / tests-playwright-python3.12 (pull_request) Failing after 2m2s
to 9420267920
Some checks failed
lint / lint (pull_request) Failing after 14s
tests / tests-slow (pull_request) Successful in 52s
tests / tests-python3.11 (pull_request) Failing after 1m16s
tests / tests-python3.13 (pull_request) Failing after 1m17s
tests / tests-python3.12 (pull_request) Failing after 1m20s
tests / tests-playwright-python3.11 (pull_request) Failing after 2m4s
tests / tests-playwright-python3.13 (pull_request) Failing after 2m4s
tests / tests-playwright-python3.12 (pull_request) Failing after 2m5s
2025年09月15日 23:15:57 +02:00
Compare
Author
Owner
Copy link

@sneakers-the-rat so my main questions are:

  1. current implementation has 3 scopes for datasets: edit, permissions (for granting/revoking permissions for other users), delete ... any others I should add to the mix?
  2. current implementation replaces dataset.account == current_account with current_account.has_scope for editable/removable_by ... do we want to retain the former as well? in other words, do we want to allow a dataset's creator to have their scopes revoked? (migration creates scopes for dataset creators, so nobody will lose edit/delete access to datasets they created if we keep it as is)
  3. related to 2, in those editable/removable_by checks, I'm looking for the appropriate scope whenever hasattr(self, "dataset_id"), which provides a bit of hierarchy for models which have dataset_id as a foreign key (dataset parts, uploads, etc), so, for example, users with the edit scope for a dataset can also edit that dataset's uploads. for uploads though, I did retain the self.account == current_account so uploaders can still edit/remove their uploads for datasets that they don't have scopes for. not really a question, but wanna make sure that all makes sense.

other than that, the gist is...

  • a new AccountDatasetScopeLinks table, pmuch AccountScopeLinks but with a dataset_id foreign key... i was originally thinking of turning AccountScopeLinks into a more general purpose ScopeLinks with an integer primary key and a unique constraint across account/dataset/upload ID foreign keys, which would each be nullable. but after getting further along it seemed like it'd be a bit over-engineering for a few reasons. and the main thing we're looking for here is multiple dataset owners, so went with this for simplicity. but might be worth doing that if we want scopes to be applied to more models, and it wouldn't be too much to refactor and maybe do part of it as a mixin.
  • a new "collaborators" field in the dataset form, with tokens that look similar to the admin page's account scope rows. but unlike the account scopes which make database changes whenever you click on the scope buttons, changes only happen when you submit the form. but I also included a couple API routes for granting/revoking individual dataset scopes to users.
  • regular users (non-review/admin/root) can only modify permissions that they've already been granted, and can't modify their own permissions (e.g., this screenshot was taken as the rando user)
    Screenshot From 2025年09月27日 15-49-47

other things in this pr that aren't related to multiple dataset owners/scopes stuff, but I included anyways:

  • updated tags autocomplete to exclude already selected tags... also prevented adding tokens with <2 chars on the frontend, since that's the min for the model
  • made it so all form fields have the magenta border, which was only showing for a few fields
  • added a block to pyproject.toml so my editor won't complain about not finding packages and whatnot
  • datasets without a description were displaying "None" because that was the value of the field, so I changed that to display "No description provided"

gotta write tests, but otherwise feel free to review/request changes!

@sneakers-the-rat so my main questions are: 1. current implementation has 3 scopes for datasets: `edit`, `permissions` (for granting/revoking permissions for other users), `delete` ... any others I should add to the mix? 2. current implementation replaces `dataset.account == current_account` with `current_account.has_scope` for editable/removable_by ... do we want to retain the former as well? in other words, do we want to allow a dataset's creator to have their scopes revoked? (migration creates scopes for dataset creators, so nobody will lose edit/delete access to datasets they created if we keep it as is) 3. related to 2, in those editable/removable_by checks, I'm looking for the appropriate scope whenever `hasattr(self, "dataset_id")`, which provides a bit of hierarchy for models which have `dataset_id` as a foreign key (dataset parts, uploads, etc), so, for example, users with the `edit` scope for a dataset can also edit that dataset's uploads. for uploads though, I did retain the `self.account == current_account` so uploaders can still edit/remove their uploads for datasets that they don't have scopes for. not really a question, but wanna make sure that all makes sense. other than that, the gist is... - a new `AccountDatasetScopeLinks` table, pmuch `AccountScopeLinks` but with a `dataset_id` foreign key... i was originally thinking of turning `AccountScopeLinks` into a more general purpose `ScopeLinks` with an integer primary key and a unique constraint across account/dataset/upload ID foreign keys, which would each be nullable. but after getting further along it seemed like it'd be a bit over-engineering for a few reasons. and the main thing we're looking for here is multiple dataset owners, so went with this for simplicity. but might be worth doing that if we want scopes to be applied to more models, and it wouldn't be too much to refactor and maybe do part of it as a mixin. - a new "collaborators" field in the dataset form, with tokens that look similar to the admin page's account scope rows. but unlike the account scopes which make database changes whenever you click on the scope buttons, changes only happen when you submit the form. but I also included a couple API routes for granting/revoking individual dataset scopes to users. - regular users (non-review/admin/root) can only modify permissions that they've already been granted, and can't modify their own permissions (e.g., this screenshot was taken as the `rando` user) ![Screenshot From 2025年09月27日 15-49-47](/attachments/61d5ca5f-d02a-472f-bd81-ef712e970a59) other things in this pr that aren't related to multiple dataset owners/scopes stuff, but I included anyways: - updated tags autocomplete to exclude already selected tags... also prevented adding tokens with <2 chars on the frontend, since that's the min for the model - made it so all form fields have the magenta border, which was only showing for a few fields - added a block to `pyproject.toml` so my editor won't complain about not finding packages and whatnot - datasets without a description were displaying "None" because that was the value of the field, so I changed that to display "No description provided" gotta write tests, but otherwise feel free to review/request changes!

making a couple comments just reading your comment before reading the code since i have a few minutes before monsterdon starts:

permissions (for granting/revoking permissions for other users)

this seems like it could be a little tricky, esp. as an othogonal scope to adding/removing other users from the ownership set (e.g. could i add someone as an owner and then not be able to give them any permissions?). ability to edit permissions is currently done by admin scope which i sort of think is different in kind than other permissions, which are what kind of things you can do, vs. what kind of account you are, so not my favorite, but it's also sort of important for permissions to be able to designate someone as the "primary" on something (esp. while the edit histories are on the fritz so we can't just do nondestructive rollbacks). not rly sure, will think more after reading code.

do we want to allow a dataset's creator to have their scopes revoked?

no i don't think so, unless they give it away or have it revoked by a moderation action

I'm looking for the appropriate scope whenever hasattr(self, "dataset_id")

i'll take a closer look when i read the code, but from the description i'd be a little concerned about having this in the root method implementation, since the odds we forget it's in there and have some other model that has dataset_id in it and give ourselves off target effects seems somewhat high, but lemme read first.

turning AccountScopeLinks into a more general purpose ScopeLinks

I think we'll eventually need to get here, but we may want to move this to graph DB land where this would be easier to do, and to generalize this in sql land should be relatively straightforward to convert the special AccountDatasetScopeLinks table to a more general form. totally reasonable call.

unlike the account scopes which make database changes whenever you click on the scope buttons, changes only happen when you submit the form

makes perfect sense

made it so all form fields have the magenta border,

this was intended to indicate required/not required (purple border == required), but let me see the changes


otherwise looks good! love the frontend screenshot. will give an actual review l8r

making a couple comments just reading your comment before reading the code since i have a few minutes before monsterdon starts: > permissions (for granting/revoking permissions for other users) this seems like it could be a little tricky, esp. as an othogonal scope to adding/removing other users from the ownership set (e.g. could i add someone as an owner and then not be able to give them any permissions?). ability to edit permissions is currently done by `admin` scope which i sort of think is different in kind than other permissions, which are what kind of things you can *do*, vs. what kind of account you *are*, so not my favorite, but it's also sort of important for permissions to be able to designate someone as the "primary" on something (esp. while the edit histories are on the fritz so we can't just do nondestructive rollbacks). not rly sure, will think more after reading code. > do we want to allow a dataset's creator to have their scopes revoked? no i don't think so, unless they give it away or have it revoked by a moderation action > I'm looking for the appropriate scope whenever hasattr(self, "dataset_id") i'll take a closer look when i read the code, but from the description i'd be a little concerned about having this in the root method implementation, since the odds we forget it's in there and have some other model that has `dataset_id` in it and give ourselves off target effects seems somewhat high, but lemme read first. > turning AccountScopeLinks into a more general purpose ScopeLinks I think we'll eventually need to get here, but we may want to move this to graph DB land where this would be easier to do, and to generalize this in sql land should be relatively straightforward to convert the special `AccountDatasetScopeLinks` table to a more general form. totally reasonable call. > unlike the account scopes which make database changes whenever you click on the scope buttons, changes only happen when you submit the form makes perfect sense > made it so all form fields have the magenta border, this was intended to indicate required/not required (purple border == required), but let me see the changes --- otherwise looks good! love the frontend screenshot. will give an actual review l8r
Author
Owner
Copy link

@sneakers-the-rat wrote in #456 (comment):

permissions (for granting/revoking permissions for other users)

this seems like it could be a little tricky, esp. as an othogonal scope to adding/removing other users from the ownership set (e.g. could i add someone as an owner and then not be able to give them any permissions?). ability to edit permissions is currently done by admin scope which i sort of think is different in kind than other permissions, which are what kind of things you can do, vs. what kind of account you are, so not my favorite, but it's also sort of important for permissions to be able to designate someone as the "primary" on something (esp. while the edit histories are on the fritz so we can't just do nondestructive rollbacks). not rly sure, will think more after reading code.

ah, so that's a thing i didn't mention. I put the existing scopes (admin, upload, etc) into a new string enum called AccountScopes and dataset scopes onto one called ItemScopes. the Scopes enum that existed before still exists and combines both, but that's pmuch just used for Scopes table stuff now. for displaying scopes on the admin page, it pulls from AccountScopes, and from ItemScopes in the dataset form. this also is reflected in other places like, for example, the RequireValidScope route dep is now RequireValidAccountScope and RequireValidItemScope for the respective grant/revoke account/dataset scopes routes.

this does complicate the way we think about scopes, but it seemed to me that the functionalities we want to provide for multiple dataset owners (edit, delete, etc) don't map onto existing instance-wide account scopes, and vice versa. and even if we want to just use a single scope that grants full ownership to each "collaborator", trying to use existing scopes for both purposes (e.g., { scope: "admin", dataset: {...}, account: {...} }) is risky wrt future work both because it muddies the water for that scope, and because it opens the door to forgetting about the dataset use case for the scope and introducing bugs where users get abilities they shouldn't have. so separation of concerns wrt scope values seems like a good idea here to me.

for clarity, given the above, you still have to be an admin to modify instance-wide account scopes (upload, submit, etc), but any user can modify users' scopes for a given dataset if they have the permissions scope for that dataset.

also don't think i said that the permissions scope includes the edit scope, similar to how the review/admin/root scopes include the upload and submit scopes. because the "collaborators" scope field is in the dataset form and a permissions scope without an edit scope would be useless.

i also think granular scopes for datasets will be useful for future work, as opposed to granting full ownership to each collaborator. e.g., since we want to enable hidden/dark datasets, we could add a "visible" scope that would allow certain users to see and upload to a dataset without being able to edit or delete it.

do we want to allow a dataset's creator to have their scopes revoked?

no i don't think so, unless they give it away or have it revoked by a moderation action

righto, easy to add that back. the current implementation basically assumes that there's no hierarchy among dataset owners (ie, anyone with the permissions scope can revoke any other user's scopes for that dataset). which yeah, could enable bad actors who gets the right permissions to mess up a dataset, esp without being able to do rollbacks, but the idea is also that it's trusted people (like people affiliated with an org) getting permissions, right? i think a teams feature would also be a useful future feature, so users could create teams and grant permissions to the entire team, and removing someone from the team would result in loss of privs for datasets that the team has scopes for.

I'm looking for the appropriate scope whenever hasattr(self, "dataset_id")

i'll take a closer look when i read the code, but from the description i'd be a little concerned about having this in the root method implementation, since the odds we forget it's in there and have some other model that has dataset_id in it and give ourselves off target effects seems somewhat high, but lemme read first.

def something i was concerned about too... could just override those methods for the dataset class to avoid changing the root method... and could still achieve the hierarchy i described (where dataset owners can edit the dataset's uploads), if desired, by also overriding the methods in classes where we want that. i'll go ahead and do that since it'd be more future proof.

turning AccountScopeLinks into a more general purpose ScopeLinks

I think we'll eventually need to get here, but we may want to move this to graph DB land where this would be easier to do, and to generalize this in sql land should be relatively straightforward to convert the special AccountDatasetScopeLinks table to a more general form. totally reasonable call.

ya know what, i'll go ahead and try out the more general approach and see if my concerns were even valid

made it so all form fields have the magenta border,

this was intended to indicate required/not required (purple border == required), but let me see the changes

ah ok, i actually thought that was the case at first but made the change bc i found a discrepancy between required fields and fields with magenta borders at one point. so i'll remove that change and see if i can i replicate what i'd seen (was a while ago, forgot exactly what it was) in case there's a different fix needed to bring the borders and required into alignment

@sneakers-the-rat wrote in https://codeberg.org/Safeguarding/sciop/pulls/456#issuecomment-7422904: > > permissions (for granting/revoking permissions for other users) > > this seems like it could be a little tricky, esp. as an othogonal scope to adding/removing other users from the ownership set (e.g. could i add someone as an owner and then not be able to give them any permissions?). ability to edit permissions is currently done by `admin` scope which i sort of think is different in kind than other permissions, which are what kind of things you can _do_, vs. what kind of account you _are_, so not my favorite, but it's also sort of important for permissions to be able to designate someone as the "primary" on something (esp. while the edit histories are on the fritz so we can't just do nondestructive rollbacks). not rly sure, will think more after reading code. ah, so that's a thing i didn't mention. I put the existing scopes (admin, upload, etc) into a new string enum called `AccountScopes` and dataset scopes onto one called `ItemScopes`. the `Scopes` enum that existed before still exists and combines both, but that's pmuch just used for Scopes table stuff now. for displaying scopes on the admin page, it pulls from `AccountScopes`, and from `ItemScopes` in the dataset form. this also is reflected in other places like, for example, the `RequireValidScope` route dep is now `RequireValidAccountScope` and `RequireValidItemScope` for the respective grant/revoke account/dataset scopes routes. this does complicate the way we think about scopes, but it seemed to me that the functionalities we want to provide for multiple dataset owners (edit, delete, etc) don't map onto existing instance-wide account scopes, and vice versa. and even if we want to just use a single scope that grants full ownership to each "collaborator", trying to use existing scopes for both purposes (e.g., `{ scope: "admin", dataset: {...}, account: {...} }`) is risky wrt future work both because it muddies the water for that scope, and because it opens the door to forgetting about the dataset use case for the scope and introducing bugs where users get abilities they shouldn't have. so separation of concerns wrt scope values seems like a good idea here to me. for clarity, given the above, you still have to be an admin to modify instance-wide account scopes (upload, submit, etc), but any user can modify users' scopes for a given dataset if they have the `permissions` scope for that dataset. also don't think i said that the permissions scope includes the edit scope, similar to how the review/admin/root scopes include the upload and submit scopes. because the "collaborators" scope field is in the dataset form and a `permissions` scope without an `edit` scope would be useless. i also think granular scopes for datasets will be useful for future work, as opposed to granting full ownership to each collaborator. e.g., since we want to enable hidden/dark datasets, we could add a "visible" scope that would allow certain users to see and upload to a dataset without being able to edit or delete it. > > do we want to allow a dataset's creator to have their scopes revoked? > > no i don't think so, unless they give it away or have it revoked by a moderation action righto, easy to add that back. the current implementation basically assumes that there's no hierarchy among dataset owners (ie, anyone with the permissions scope can revoke any other user's scopes for that dataset). which yeah, could enable bad actors who gets the right permissions to mess up a dataset, esp without being able to do rollbacks, but the idea is also that it's trusted people (like people affiliated with an org) getting permissions, right? i think a teams feature would also be a useful future feature, so users could create teams and grant permissions to the entire team, and removing someone from the team would result in loss of privs for datasets that the team has scopes for. > > I'm looking for the appropriate scope whenever hasattr(self, "dataset_id") > > i'll take a closer look when i read the code, but from the description i'd be a little concerned about having this in the root method implementation, since the odds we forget it's in there and have some other model that has `dataset_id` in it and give ourselves off target effects seems somewhat high, but lemme read first. def something i was concerned about too... could just override those methods for the dataset class to avoid changing the root method... and could still achieve the hierarchy i described (where dataset owners can edit the dataset's uploads), if desired, by also overriding the methods in classes where we want that. i'll go ahead and do that since it'd be more future proof. > > turning AccountScopeLinks into a more general purpose ScopeLinks > > I think we'll eventually need to get here, but we may want to move this to graph DB land where this would be easier to do, and to generalize this in sql land should be relatively straightforward to convert the special `AccountDatasetScopeLinks` table to a more general form. totally reasonable call. > ya know what, i'll go ahead and try out the more general approach and see if my concerns were even valid > > made it so all form fields have the magenta border, > > this was intended to indicate required/not required (purple border == required), but let me see the changes > ah ok, i actually thought that was the case at first but made the change bc i found a discrepancy between required fields and fields with magenta borders at one point. so i'll remove that change and see if i can i replicate what i'd seen (was a while ago, forgot exactly what it was) in case there's a different fix needed to bring the borders and required into alignment
Author
Owner
Copy link

book-length response, sorryyyyy 😜

book-length response, sorryyyyy 😜
sneakers-the-rat left a comment
Copy link

hell yeah. like i said above, this is a ton of work, thank you for hanging in there and finishing this.

Some comments, some are stylistic, some are more substantive, hopefully the content of the comment makes it clear which is which. i think main thing that's needed is tests, and those should probably include some playwright tests since the frontend interaction part is pretty sophisticated for sciop. then we'll want to make sure we cover all the basic stuff like adding a collaborator, adding/removing scopes, and then testing that an account with given scopes can do the things they should be able to do and vice versa re: accounts without scopes.

we have talked about the macro-level design stuff like wanting to have a more general way to apply functionality to all item types rather than having to do them one by one in chat, but i think this is good, and datasets are a bit of a special beast as an abstract reference to an 'item in the world' vs. uploads which are more explicitly 'items created by the account in question'

hell yeah. like i said above, this is a ton of work, thank you for hanging in there and finishing this. Some comments, some are stylistic, some are more substantive, hopefully the content of the comment makes it clear which is which. i think main thing that's needed is tests, and those should probably include some playwright tests since the frontend interaction part is pretty sophisticated for sciop. then we'll want to make sure we cover all the basic stuff like adding a collaborator, adding/removing scopes, and then testing that an account with given scopes can do the things they should be able to do and vice versa re: accounts without scopes. we have talked about the macro-level design stuff like wanting to have a more general way to apply functionality to all item types rather than having to do them one by one in chat, but i think this is good, and datasets are a bit of a special beast as an abstract reference to an 'item in the world' vs. uploads which are more explicitly 'items created by the account in question'
@ -233,6 +234,38 @@ def require_visible_dataset(dataset: RequireDataset, current_account: CurrentAcc
RequireVisibleDataset = Annotated[Dataset, Depends(require_visible_dataset)]
def require_scopable_dataset(

docstring here plz, not immediately clear to me what this is requiring

docstring here plz, not immediately clear to me what this is requiring
goblina marked this conversation as resolved
@ -121,3 +122,3 @@
dataset_slug: str,
dataset_patch: Annotated[DatasetUpdate, Body()],
dataset: RequireVisibleDataset,
dataset: RequireScopableDataset,

seems like this might have overlapping responsibility with the RequireEditableBy dependency?

seems like this might have overlapping responsibility with the `RequireEditableBy` dependency?
goblina marked this conversation as resolved
@ -344,0 +405,4 @@
account: RequireAccount,
session: SessionDep,
):
if not current_account.get_scope("review"):

maybe we need a method like can_grant_scope in the moderable mixin? some way of encapsulating this check for whether the action can be taken so we don't have to repeat it

maybe we need a method like `can_grant_scope` in the moderable mixin? some way of encapsulating this check for whether the action can be taken so we don't have to repeat it
Author
Owner
Copy link

oh yeah, i like that. will do!

oh yeah, i like that. will do!
@ -344,0 +420,4 @@
if username == current_account.username:
raise HTTPException(403, "You cannot grant permissions to yourself.")
if scope := next(

excellent use of the language features

excellent use of the language features
goblina marked this conversation as resolved
@ -82,0 +87,4 @@
if current_account:
scopes.insert(
0,
ItemScopesRead(

usually these *Read models are used to display data publicly, while the *Create models specify the fields needed when creating an item.

usually these `*Read` models are used to display data publicly, while the `*Create` models specify the fields needed when creating an item.

reading further, ya i think this and the other places where there are AccountDatasetScopeLink <-> ItemScopesRead transformations should be methods on ItemScopesRead and that would clean things up a bit

reading further, ya i think this and the other places where there are `AccountDatasetScopeLink` <-> `ItemScopesRead` transformations should be methods on `ItemScopesRead` and that would clean things up a bit
@ -508,0 +539,4 @@
scope_links = [e for e in existing_scopes if (e.account.username, e.scope.value) in new]
revoked_scopes = [e for e in existing_scopes if e not in scope_links]
for scope in revoked_scopes:
session.delete(scope)

I think this function might be doing too much - i see how it's used below, when updating scopes, but the assignment of a list of scopes should handle deleting the items not in the new collection. so if this just returns the links that should exist, subsetting from the ones that already exist, then i think this would be potentially less surprising than the scopes to delete already having been deleted by the time we return from this.

the ListlikeMixin.get_items method might be what you're looking for here

I think this function might be doing too much - i see how it's used below, when updating scopes, but the assignment of a list of scopes should handle deleting the items not in the new collection. so if this just returns the links that *should* exist, subsetting from the ones that already exist, then i think this would be potentially less surprising than the scopes to delete already having been deleted by the time we return from this. the `ListlikeMixin.get_items` method might be what you're looking for here
@ -18,3 +20,3 @@
@autocomplete_router.get("/tags")
@jinja.hx("partials/autocomplete-options.html")
async def tags(tags: str, session: SessionDep) -> list[str]:
async def tags(

nice, love this

nice, love this
goblina marked this conversation as resolved
@ -52,0 +83,4 @@
):
acct = session.exec(
select(Account).where(
Account.username == account_query.strip(), Account != current_account

also Account.is_suspended == False

also `Account.is_suspended == False`
goblina marked this conversation as resolved
@ -0,0 +93,4 @@
op.bulk_insert(Scopes, new_scopes)
conn.execute(
sa.text(
"""

one extra space here re: alignment

one extra space here re: alignment
goblina marked this conversation as resolved
@ -0,0 +94,4 @@
conn.execute(
sa.text(
"""
INSERT INTO account_dataset_scope_links (

dang i know nothing about SQL, this looks like this works but i have never used INSERT INTO before

dang i know nothing about SQL, this looks like this works but i have never used INSERT INTO before
Author
Owner
Copy link

heh, the orm we used at my last job sucked ass and maintenance/refactors rarely got approved, so i had to hand-write way more sql than i would've liked

heh, the orm we used at my last job sucked ass and maintenance/refactors rarely got approved, so i had to hand-write way more sql than i would've liked
goblina marked this conversation as resolved
@ -0,0 +113,4 @@
# ### end Alembic commands ###
def downgrade() -> None:

would also need to delete the new scopes, right?

would also need to delete the new scopes, right?
goblina marked this conversation as resolved
@ -44,4 +32,3 @@
model_config = ConfigDict(ignored_types=(hybrid_method,))
@hybrid_method
def has_scope(self, *args: str | Scopes) -> bool:

can't make multiple comments on the same line, so commenting one line above:

elsewhere, throughout the program, we don't usually use the row ids as identifiers, and even though this is an internal method, we may want to use the dataset slug rather than the id for consistency. it should be indexed so perf should be similar. not a strong opinion.

can't make multiple comments on the same line, so commenting one line above: elsewhere, throughout the program, we don't usually use the row ids as identifiers, and even though this is an internal method, we may want to use the dataset slug rather than the id for consistency. it should be indexed so perf should be similar. not a strong opinion.
@ -45,3 +33,3 @@
@hybrid_method
def has_scope(self, *args: str | Scopes) -> bool:
def has_scope(self, *args: str | Scopes, dataset_id: Optional[int] = None) -> bool:

i think we probably want to make the changes here into a separate method.

so now we have two notions of scopes: scopes for accounts that determine what an account can do globally, and scopes for items x account that determine what an account can do for a specific item. those seem separable enough to me that i think we should probably have a separate has_item_scope() method.

maybe the logic should live on the items not the account? like in the circumstances where we are checking whether an account can do something with an item we should have both the account and the item, so if the method lives on the item then we don't need switching behavior like we would in the account.

let me finish reading and think more about what would be good here

i *think* we probably want to make the changes here into a separate method. so now we have two notions of scopes: scopes for *accounts* that determine what an account can do globally, and scopes for *items x account* that determine what an account can do for a specific item. those seem separable enough to me that i think we should probably have a separate `has_item_scope()` method. maybe the logic should live on the items not the account? like in the circumstances where we are checking whether an account can do something with an item we should have both the account and the item, so if the method lives on the item then we don't need switching behavior like we would in the account. let me finish reading and think more about what would be good here
Author
Owner
Copy link

makes sense to me. i'd actually considered that after i'd already started modifying those methods, and kinda just flipped a coin in my head about it.

makes sense to me. i'd actually considered that after i'd already started modifying those methods, and kinda just flipped a coin in my head about it.
@ -81,0 +68,4 @@
has_scopes = [
scope.scope for scope in self.dataset_scopes if scope.dataset_id == dataset_id
]
if "permissions" in has_scopes:

this seems a touch implicit to me in the same way the existing implementation does, where if one permissions scope implies another then we should probably directly model that. is there ever a time when someone would have permissions without edit? should there be?

this seems a touch implicit to me in the same way the existing implementation does, where if one permissions scope implies another then we should probably directly model that. is there ever a time when someone would have `permissions` without `edit`? should there be?
Author
Owner
Copy link

i def agree, and no, permissions should always include edit because otherwise permissions is useless. i'm curious how you'd model that hierarchy tho, is there another place in the codebase i can peek at to see the pattern you've got in mind?

i def agree, and no, `permissions` should always include `edit` because otherwise `permissions` is useless. i'm curious how you'd model that hierarchy tho, is there another place in the codebase i can peek at to see the pattern you've got in mind?
@ -95,0 +90,4 @@
if dataset_id:
scopes = list(args)
if "permissions" in args and "edit" not in args:

so if i am checking if someone has permissions, if that account has edit but not permissions, wouldn't i get a false positive here? this method works a bit differently than the non-hybrid has_scope method, and i think it should probably work the other way -

if "edit" in args and "permissions" not in args:
 scopes.append("permissions")

so that if we are checking if someone has edit permissions, but they only have permissions permissions, they implicitly are granted edit permissions

so if i am checking if someone has `permissions`, if that account has `edit` but not `permissions`, wouldn't i get a false positive here? this method works a bit differently than the non-hybrid `has_scope` method, and i think it should probably work the other way - ```python if "edit" in args and "permissions" not in args: scopes.append("permissions") ``` so that if we are checking if someone has `edit` permissions, but they only have `permissions` permissions, they implicitly are granted `edit` permissions
Author
Owner
Copy link

ope, you're totally right, my bad!

ope, you're totally right, my bad!
goblina marked this conversation as resolved
@ -254,6 +255,7 @@ class Dataset(DatasetBase, TableMixin, SearchableMixin, EditableMixin, SortMixin
)
account_id: Optional[int] = Field(default=None, foreign_key="accounts.account_id", index=True)
account: Optional["Account"] = Relationship(back_populates="datasets")
account_scopes: list["AccountDatasetScopeLink"] = Relationship(back_populates="dataset")

so now we have both an account and an account_scopes field - what is the account field for now? is that how we indicate a creator of a dataset, even if they are now one of several collaborators?

so now we have both an `account` and an `account_scopes` field - what is the `account` field for now? is that how we indicate a creator of a dataset, even if they are now one of several collaborators?
Author
Owner
Copy link

that's something i was kinda wondering. you said, either above or in signal, that we should display collaborators on the dataset page. so i think account/account_id aren't really doing anything once i do that + make the self page use account_scopes as well

only thing is the change wouldn't be undoable if we ever revert the migration. but i'm cool with that if you are!

that's something i was kinda wondering. you said, either above or in signal, that we should display collaborators on the dataset page. so _i think_ `account`/`account_id` aren't really doing anything once i do that + make the `self` page use `account_scopes` as well only thing is the change wouldn't be undoable if we ever revert the migration. but i'm cool with that if you are!
@ -325,6 +337,17 @@ class DatasetCreate(DatasetBase):
min_length=1,
max_length=512,
)
account_scopes: Optional[list["ItemScopesRead"]] = Field(

i think we need this on DatasetRead too?

i think we need this on `DatasetRead` too?
@ -328,0 +345,4 @@
Hover over permission buttons to see their descriptions.
""",
schema_extra={"json_schema_extra": {"input_type": InputType.account_scopes}},
max_length=512,

the party dataset

the party dataset
Author
Owner
Copy link

to have more than 512 collaborators we'll have to install another fire exit

to have more than 512 collaborators we'll have to install another fire exit
goblina marked this conversation as resolved
@ -355,2 +378,4 @@
for ex in dataset.external_identifiers
],
"account_scopes": [
ItemScopesRead(

seems like we should be using a *Create model for this, just to keep things tidy. (i sort of hate this multiple models for the same thing design, but it is what fits the tools, and there have been plenty of times where we want separate models for each of these contexts)

seems like we should be using a `*Create` model for this, just to keep things tidy. (i sort of hate this multiple models for the same thing design, but it is what fits the tools, and there have been plenty of times where we want separate models for each of these contexts)
goblina marked this conversation as resolved
@ -105,3 +105,3 @@
if account is None:
return False
return self.account == account or account.has_scope("review")
return (

i think this should go in an override of editable_by on the Dataset class - since this only applies to Datasets, let's minimize chance for unexpected behavior on the other items until we do a more general solution.

re: your comment about this extending to dataset parts, should be possible to write it only once by being a separate function that's called by the overridden method, or by assigning to the class object directly, idk i'll mess around with that

i think this should go in an override of `editable_by` on the `Dataset` class - since this only applies to `Dataset`s, let's minimize chance for unexpected behavior on the other items until we do a more general solution. re: your comment about this extending to dataset parts, should be possible to write it only once by being a separate function that's called by the overridden method, or by assigning to the class object directly, idk i'll mess around with that
@ -115,0 +121,4 @@
or_stmts = [account.has_scope("review") == True]
if hasattr(cls, "dataset_id"):

same thing as above, move to override on Dataset class

same thing as above, move to override on `Dataset` class
@ -62,3 +62,3 @@
elif self.is_removed:
return False
elif hasattr(self, "account") and self.account == account:
elif hasattr(self, "dataset_id") and account.has_scope(

same as above, probably in an override on Dataset - the notion of "account" remains true for other item types.

edit: rather than making a comment on each change to mixin methods, i think each of these are overrides. i think of these mixins sort of like a rust trait or a python protocol - these methods say "things that are moderable have some method visible_to that is used to check visibility, and those implementations might vary. and this implementation is just the default one unless they have a more specific criterion for visibility.

same as above, probably in an override on `Dataset` - the notion of "account" remains true for other item types. edit: rather than making a comment on each change to mixin methods, i think each of these are overrides. i think of these mixins sort of like a rust trait or a python protocol - these methods say "things that are moderable have some method `visible_to` that is used to check visibility, and those implementations might vary. and this implementation is just the default one unless they have a more specific criterion for visibility.
@ -0,0 +67,4 @@
scope: Scopes = Field(unique=True)
class ItemScopesRead(BaseModel):

you might consider making some of the nested list comprehensions and whatnot done elsewhere to create these as classmethod creators here - like

@classmethod
def from_links(cls, links: list[AccountDatasetScopeLink]) -> list[ItemScopesRead]: ...
def to_links() -> list[AccountDatasetScopeLink]: ...
you might consider making some of the nested list comprehensions and whatnot done elsewhere to create these as classmethod creators here - like ```python @classmethod def from_links(cls, links: list[AccountDatasetScopeLink]) -> list[ItemScopesRead]: ... def to_links() -> list[AccountDatasetScopeLink]: ... ```
@ -0,0 +68,4 @@
class ItemScopesRead(BaseModel):
"""Aggregated-by-account scopes object returned from API methods"""

I think my comments elsewhere being uneasy about the *Read *Create are just from not having reached this point yet and seeing that it's not a table model, but a convenience container. since there isn't really a notion of *Read (which is mostly used to censor or transform the database form of an object) vs *Create here, you might want to just name this ItemScopes (though i would expect such a class to have an item field, that's just a documentation thing - this is an object used by some item, so the item is implicit whenever this is used)

I think my comments elsewhere being uneasy about the `*Read` `*Create` are just from not having reached this point yet and seeing that it's not a table model, but a convenience container. since there isn't really a notion of `*Read` (which is mostly used to censor or transform the database form of an object) vs `*Create` here, you might want to just name this `ItemScopes` (though i would expect such a class to have an `item` field, that's just a documentation thing - this is an object *used by* some item, so the item is implicit whenever this is used)
@ -0,0 +77,4 @@
class ItemScopesAction(BaseModel):
"""Action object for handling account scope form events"""
action: Literal["load", "add account", "add scope", "remove scope"]

can we make these names underscored like add_account and etc? i am always a bit wary with adding programmatically meaningful tokens as strings with spaces.

can we make these names underscored like `add_account` and etc? i am always a bit wary with adding programmatically meaningful tokens as strings with spaces.
goblina marked this conversation as resolved
@ -7,3 +7,3 @@
padding: 1em;
color: var(--color-text);
border: 1px solid var(--color-primary);
border: 1px solid var(--color-primary) !important;

not having purple border around all elements is intentional - modified by presence of .optional and disabled

not having purple border around all elements is intentional - modified by presence of `.optional` and `disabled`
goblina marked this conversation as resolved
@ -96,3 +96,3 @@
}
&:disabled {
&:disabled, &.disabled {

why disabled as a class as well as the property?

why disabled as a class as well as the property?
goblina marked this conversation as resolved
@ -177,0 +182,4 @@
&.scopes-form {
margin-bottom: 0em;
&.has-entries {

you can just do this with :has(div) or :has(.class) or whatever ya want instead of needing to compute this in the template :)

you can just do this with `:has(div)` or `:has(.class)` or whatever ya want instead of needing to compute this in the template :)
goblina marked this conversation as resolved
@ -58,6 +58,7 @@
}
}
.account-row,

indentation

indentation
Author
Owner
Copy link

think this is correct actually, it's nested within other blocks. unless I'm looking at the wrong thing

think this is correct actually, it's nested within other blocks. unless I'm looking at the wrong thing
@ -105,3 +105,3 @@
// Token input field
function addToken(e){
if (e.key !== "Enter"){ return }
if (e.key !== "Enter" || e.target.value.length < 2){ return }

nice, thanks

nice, thanks
goblina marked this conversation as resolved
@ -1,8 +1,61 @@
{% macro account_row(dataset, current_account, config) %}
{% macro account_row(account, items=None, current_account=None, current_account_scopes=[], edit=False, idx=None) %}

we may want to make this name more specific, but otherwise this is nice.

we may want to make this name more specific, but otherwise this is nice.
goblina marked this conversation as resolved
@ -3,0 +22,4 @@
or (scope_name != "edit" and scope_name not in current_account_scopes)
)
) %}
{% if scope_name in account.scopes or (scope_name == "edit" and "permissions" in account.scopes) %}

could do the same thing you do above and do {% set checked=...%} and avoid duplicating the <a...>

could do the same thing you do above and do `{% set checked=...%}` and avoid duplicating the `<a...>`
goblina marked this conversation as resolved
@ -164,12 +166,47 @@
hx-get="/autocomplete/{{ field_name }}"
hx-target="#{{ parent_id }}-{{ field_name }}-autocomplete"
hx-trigger="input changed delay:250ms"
hx-include="#{{ parent_id }}-{{ field_name }}-tokens"

shouldn't be necessary, each of the token buttons contain a hidden input within the form

shouldn't be necessary, each of the token buttons contain a hidden `input` within the form
Author
Owner
Copy link

nah, it is. by default, hx-get only includes the value of its field, while most (all?) other methods include the entire form. in this hx-include I'm referencing the parent element which contains all of those hidden inputs, which is how they're being included in the request.

that said, I don't think we need the hx-params="*" on the line below. seems like it's not doing anything here. I think that might've gotten left in from when I was considering using a different variable name for the query string so I wouldn't have to do the query = tags[0] on the backend, but that ended up causing more problems than it was worth :P

nah, it is. by default, `hx-get` only includes the value of its field, while most (all?) other methods include the entire form. in this `hx-include` I'm referencing the parent element which contains all of those hidden inputs, which is how they're being included in the request. that said, I don't think we need the `hx-params="*"` on the line below. seems like it's not doing anything here. I think that might've gotten left in from when I was considering using a different variable name for the query string so I wouldn't have to do the `query = tags[0]` on the backend, but that ended up causing more problems than it was worth :P
@ -9,3 +9,3 @@
<h2>Scopes</h2>
{% for scope in account.scopes %}
{% for scope in account.scopes if scope.scope.value in types.AccountScopes.__members__.values() %}

i don't know if this is necessary, invalid scopes should never exist right?

i don't know if this is necessary, invalid scopes should never exist right?
goblina marked this conversation as resolved
@ -285,3 +286,2 @@
class Scopes(StrEnum):
"""Account Permissions"""
class AccountScopes(StrEnum):

the multiple definition of these makes me a bit uneasy, but i'm not sure of a better way to refer to subsets of an enum

the multiple definition of these makes me a bit uneasy, but i'm not sure of a better way to refer to subsets of an enum
Author
Owner
Copy link

yeah i went hunting for a way to do it and couldn't find anything that wasn't super hacky :\

yeah i went hunting for a way to do it and couldn't find anything that wasn't super hacky :\
Author
Owner
Copy link

actually, maybe annotations?

actually, maybe annotations?

one more thing - do we display collaborator usernames on the dataset page? should probably do that.

one more thing - do we display collaborator usernames on the dataset page? should probably do that.

sorry just saw your response after i submitted review. i think i responded to some things after having read the code in comments, but responding to more specific things here:

think a teams feature would also be a useful future feature, so users could create teams and grant permissions to the entire team, and removing someone from the team would result in loss of privs for datasets that the team has scopes for.

agreed that teams/groups should be the eventual form of this. i like having both, where i think teams/groups should basically be addressable as if it was a single account, so e.g. one could have one-off collaborators on some items in addition to it belonging to a group, etc.

i'll go ahead and try out the more general approach and see if my concerns were even valid

if you want to! this is already plenty, so if it's a substantial amount of work to do, then i wouldn't worry about it.

sorry just saw your response after i submitted review. i think i responded to some things after having read the code in comments, but responding to more specific things here: > think a teams feature would also be a useful future feature, so users could create teams and grant permissions to the entire team, and removing someone from the team would result in loss of privs for datasets that the team has scopes for. agreed that teams/groups should be the eventual form of this. i like having both, where i think teams/groups should basically be addressable as if it was a single account, so e.g. one could have one-off collaborators on some items in addition to it belonging to a group, etc. > i'll go ahead and try out the more general approach and see if my concerns were even valid if you want to! this is already plenty, so if it's a substantial amount of work to do, then i wouldn't worry about it.
goblina force-pushed dataset-scopes from 1bfbc1425c
Some checks failed
tests / tests-python3.11 (pull_request) Has started running
tests / tests-python3.12 (pull_request) Has started running
tests / tests-python3.13 (pull_request) Has started running
lint / lint (pull_request) Failing after 12s
tests / tests-slow (pull_request) Failing after 1m26s
tests / tests-playwright-python3.11 (pull_request) Failing after 2m0s
tests / tests-playwright-python3.12 (pull_request) Failing after 2m4s
tests / tests-playwright-python3.13 (pull_request) Failing after 2m5s
to 36eb595be4
Some checks failed
lint / lint (pull_request) Failing after 14s
tests / tests-slow (pull_request) Failing after 1m33s
tests / tests-python3.11 (pull_request) Failing after 1m46s
tests / tests-python3.13 (pull_request) Failing after 1m45s
tests / tests-python3.12 (pull_request) Failing after 1m48s
tests / tests-playwright-python3.11 (pull_request) Failing after 4m0s
tests / tests-playwright-python3.13 (pull_request) Failing after 3m56s
tests / tests-playwright-python3.12 (pull_request) Failing after 4m0s
2026年02月04日 13:28:21 +01:00
Compare
goblina force-pushed dataset-scopes from 36eb595be4
Some checks failed
lint / lint (pull_request) Failing after 14s
tests / tests-slow (pull_request) Failing after 1m33s
tests / tests-python3.11 (pull_request) Failing after 1m46s
tests / tests-python3.13 (pull_request) Failing after 1m45s
tests / tests-python3.12 (pull_request) Failing after 1m48s
tests / tests-playwright-python3.11 (pull_request) Failing after 4m0s
tests / tests-playwright-python3.13 (pull_request) Failing after 3m56s
tests / tests-playwright-python3.12 (pull_request) Failing after 4m0s
to 98a8162d8b
Some checks failed
lint / lint (pull_request) Failing after 15s
tests / tests-python3.11 (pull_request) Failing after 2m9s
tests / tests-python3.13 (pull_request) Failing after 2m11s
tests / tests-slow (pull_request) Successful in 2m6s
tests / tests-python3.12 (pull_request) Failing after 2m18s
tests / tests-playwright-python3.11 (pull_request) Failing after 4m31s
tests / tests-playwright-python3.12 (pull_request) Failing after 4m33s
tests / tests-playwright-python3.13 (pull_request) Failing after 4m33s
2026年02月07日 22:40:49 +01:00
Compare
lint fixes
Some checks failed
tests / tests-python3.11 (pull_request) Failing after 1m59s
tests / tests-python3.12 (pull_request) Failing after 2m9s
tests / tests-python3.13 (pull_request) Failing after 2m10s
tests / tests-slow (pull_request) Successful in 2m4s
tests / tests-playwright-python3.11 (pull_request) Failing after 4m26s
tests / tests-playwright-python3.13 (pull_request) Failing after 4m30s
tests / tests-playwright-python3.12 (pull_request) Failing after 4m33s
lint / lint (pull_request) Failing after 12s
ae71b918e2
use the right scopes type obj
Some checks failed
lint / lint (pull_request) Failing after 16s
tests / tests-python3.11 (pull_request) Failing after 2m18s
tests / tests-slow (pull_request) Successful in 2m11s
tests / tests-python3.13 (pull_request) Failing after 2m26s
tests / tests-python3.12 (pull_request) Failing after 2m31s
tests / tests-playwright-python3.11 (pull_request) Failing after 4m39s
tests / tests-playwright-python3.12 (pull_request) Failing after 4m45s
tests / tests-playwright-python3.13 (pull_request) Failing after 4m46s
ce993e4d9b
Author
Owner
Copy link

@sneakers-the-rat getting back on this finally. writing tests still, but any idea why black is requesting reformatting in the failed lint check for the last commit? locally black is saying those files are good :P

@sneakers-the-rat getting back on this finally. writing tests still, but any idea why black is requesting reformatting in the failed lint check for the last commit? locally black is saying those files are good :P

Oh ya that got me too. Black 26 changed string formatting to make it more compact. I liked the new style so went with that rather than disabling/pinning the version in #481

Also changed the tests so they use the lockfile instead of the most recent versions of packages since this is "an application" after all. If you rebase/merge main and reinstall from the lockfile you should be good

Oh ya that got me too. Black 26 changed string formatting to make it more compact. I liked the new style so went with that rather than disabling/pinning the version in https://codeberg.org/Safeguarding/sciop/pulls/481 Also changed the tests so they use the lockfile instead of the most recent versions of packages since this is "an application" after all. If you rebase/merge main and reinstall from the lockfile you should be good
Author
Owner
Copy link

ah, yeah shoulda thought to try that lol. thanks!

ah, yeah shoulda thought to try that lol. thanks!
reformatting now that black is updated :P
Some checks failed
lint / lint (pull_request) Successful in 13s
tests / tests-python3.11 (pull_request) Failing after 1m59s
tests / tests-python3.13 (pull_request) Failing after 2m6s
tests / tests-python3.12 (pull_request) Failing after 2m6s
tests / tests-slow (pull_request) Successful in 2m0s
tests / tests-playwright-python3.11 (pull_request) Failing after 4m26s
tests / tests-playwright-python3.12 (pull_request) Failing after 4m28s
tests / tests-playwright-python3.13 (pull_request) Failing after 4m27s
85ab775cca
add __name__ back to AccountBase
Some checks failed
lint / lint (pull_request) Successful in 15s
tests / tests-slow (pull_request) Successful in 1m45s
tests / tests-python3.11 (pull_request) Failing after 2m3s
tests / tests-python3.13 (pull_request) Failing after 2m2s
tests / tests-python3.12 (pull_request) Failing after 2m7s
tests / tests-playwright-python3.11 (pull_request) Has been cancelled
tests / tests-playwright-python3.12 (pull_request) Has been cancelled
tests / tests-playwright-python3.13 (pull_request) Has been cancelled
bbc3f7305c
migration fix
Some checks failed
lint / lint (pull_request) Failing after 11s
tests / tests-python3.11 (pull_request) Has been cancelled
tests / tests-python3.13 (pull_request) Has been cancelled
tests / tests-python3.12 (pull_request) Has been cancelled
tests / tests-playwright-python3.13 (pull_request) Has been cancelled
tests / tests-slow (pull_request) Has been cancelled
tests / tests-playwright-python3.12 (pull_request) Has been cancelled
tests / tests-playwright-python3.11 (pull_request) Has been cancelled
7518228d8a
lint fix
Some checks failed
lint / lint (pull_request) Successful in 15s
tests / tests-python3.11 (pull_request) Failing after 2m21s
tests / tests-python3.13 (pull_request) Failing after 2m19s
tests / tests-python3.12 (pull_request) Failing after 2m23s
tests / tests-slow (pull_request) Successful in 2m17s
tests / tests-playwright-python3.11 (pull_request) Failing after 4m19s
tests / tests-playwright-python3.12 (pull_request) Failing after 4m33s
tests / tests-playwright-python3.13 (pull_request) Failing after 4m33s
4459387802
Author
Owner
Copy link

@sneakers-the-rat how do we want to handle "reported account"/report actions for multi-owner datasets?

this method is expecting the report target to have an account link if the target isn't an account itself. I removed the datasets.account_id field because it wasn't being used for anything after I switched the submitted by label on dataset pages to be maintained by and display everyone with scopes for the dataset.

I could add it back for reporting purposes, but the person who submitted the reported dataset might not be the person who violated some site rule if the dataset has multiple owners.

OTOH, if we do want to remove datasets.account_id, then for ReportAction.suspend_remove we have a choice to make - does it remove all datasets the account has scopes for even if some of the datasets have other owners? Do we just skip auto-removing datasets altogether? Do we only auto-remove datasets where the suspended account was the sole owner? For the last option, malicious users would be able to give dataset scopes to random users to prevent the datasets from being automatically removed, but maybe we could just display a table of the account's datasets that have other owners when the person handling the report clicks the "suspend remove" button so they can select which datasets to keep and which to remove.

@sneakers-the-rat how do we want to handle "reported account"/report actions for multi-owner datasets? [this method](https://codeberg.org/Safeguarding/sciop/src/branch/main/src/sciop/models/report.py#L252) is expecting the report target to have an `account` link if the target isn't an account itself. I removed the `datasets.account_id` field because it wasn't being used for anything after I switched the `submitted by` label on dataset pages to be `maintained by` and display everyone with scopes for the dataset. I could add it back for reporting purposes, but the person who submitted the reported dataset might not be the person who violated some site rule if the dataset has multiple owners. OTOH, if we do want to remove `datasets.account_id`, then for `ReportAction.suspend_remove` we have a choice to make - does it remove all datasets the account has scopes for even if some of the datasets have other owners? Do we just skip auto-removing datasets altogether? Do we only auto-remove datasets where the suspended account was the sole owner? For the last option, malicious users would be able to give dataset scopes to random users to prevent the datasets from being automatically removed, but maybe we could just display a table of the account's datasets that have other owners when the person handling the report clicks the "suspend remove" button so they can select which datasets to keep and which to remove.

very good questions:

    1. I think if datasets are truly multi-owner they should behave like it and be modeled like it, so yes i think we shouldn't have a separate field just for reporting purposes. I think the basic behavior we want is "if a dataset with multiple owners is reported and we want to take action against the account as well as the dataset, then moderators should be able to select which of the accounts action is being taken against"
    1. I think for removing, by default we should not remove datasets that are owned by multiple accounts when one of them is suspended. maybe only when all of the accounts are suspended. I like your suggestion to be able to select/deselect datasets to remove, let's go with that. would it be a lot of work to prefill that form with like "all the datasets that are solely owned by an account" already checked?
    1. It's a good point re: abuse and randomly adding other people as co-maintainers. In general i think we want to not have it be possible to people to add other people to things, so ideally that would be a request flow: either the current owner requests to add someone to a dataset and they approve/deny, or someone else who wants to become an owner can request it and the current maintainers can approve/deny. That might be a decent bit of work and this PR is already a ton of work, so we might want to come back to that in another PR.
very good questions: - 1) I think if datasets are truly multi-owner they should behave like it and be modeled like it, so yes i think we shouldn't have a separate field just for reporting purposes. I think the basic behavior we want is "if a dataset with multiple owners is reported and we want to take action against the account as well as the dataset, then moderators should be able to select which of the accounts action is being taken against" - 2) I think for removing, by default we should *not* remove datasets that are owned by multiple accounts when one of them is suspended. maybe only when *all* of the accounts are suspended. I like your suggestion to be able to select/deselect datasets to remove, let's go with that. would it be a lot of work to prefill that form with like "all the datasets that are solely owned by an account" already checked? - 3) It's a good point re: abuse and randomly adding other people as co-maintainers. In general i think we want to not have it be possible to people to add other people to things, so ideally that would be a request flow: either the current owner requests to add someone to a dataset and they approve/deny, or someone else who wants to become an owner can request it and the current maintainers can approve/deny. That might be a decent bit of work and this PR is already a ton of work, so we might want to come back to that in another PR.
Author
Owner
Copy link

that all makes sense to me!

re: prefilling forms with solely-owned datasets, that should be easy. will do!

and yeah, a request flow def sounds like a good idea... and would be good to already have implemented for other stuff like teams. I'll think on that a bit, but yeah, it might deserve its own PR if it should work for different resources like that.

that all makes sense to me! re: prefilling forms with solely-owned datasets, that should be easy. will do! and yeah, a request flow def sounds like a good idea... and would be good to already have implemented for other stuff like teams. I'll think on that a bit, but yeah, it might deserve its own PR if it should work for different resources like that.
Merge branch 'main' into dataset-scopes
Some checks are pending
lint / lint (pull_request) Waiting to run
tests / tests-python3.11 (pull_request) Waiting to run
tests / tests-python3.12 (pull_request) Waiting to run
tests / tests-python3.13 (pull_request) Waiting to run
tests / tests-playwright-python3.11 (pull_request) Waiting to run
tests / tests-playwright-python3.12 (pull_request) Waiting to run
tests / tests-playwright-python3.13 (pull_request) Waiting to run
tests / tests-slow (pull_request) Waiting to run
5732e96780
Some checks are pending
lint / lint (pull_request) Waiting to run
Required
Details
tests / tests-python3.11 (pull_request) Waiting to run
tests / tests-python3.12 (pull_request) Waiting to run
tests / tests-python3.13 (pull_request) Waiting to run
Required
Details
tests / tests-playwright-python3.11 (pull_request) Waiting to run
tests / tests-playwright-python3.12 (pull_request) Waiting to run
tests / tests-playwright-python3.13 (pull_request) Waiting to run
tests / tests-slow (pull_request) Waiting to run
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin dataset-scopes:dataset-scopes
git switch dataset-scopes
Sign in to join this conversation.
No reviewers
Labels
Clear labels
accessibility
backend
bittorrent
component/cli
docs
feature/feeds
feature/moderation
feature/search
feature/services
Scheduled services run by the apscheduler runner
frontend
help wanted
Pitches for ideas for which a PR would be welcome <3. Everything is always help wanted, but these are things that would be discrete features that someone could take and implement on their own
perf
security
size
large
changes to many items, hundreds-thousands of lines, doable in >2 days
size
medium
changes to a handful of items, ~dozens to hundreds of lines, doable in ~1-2 days
size
small
changes to a single or small number of items, ~a dozen or two lines, doable in ~1-2 hours
size
tiny
a line or two
tests
Timeline
Long
months, years
Timeline
Medium
weeks, up to a month
Timeline
Short
Hours, days, up to a week
ui/ux
zzz-certified-good-issue-or-pr-number
Compat/Breaking
Breaking change that won't be backward compatible
Kind/Bug
Something is not working
Kind/Documentation
Documentation changes
Kind/Enhancement
Improve existing functionality
Kind/Feature
New functionality
Kind/Security
This is security issue
Kind/Tech Debt
Kind/Testing
Issue or pull request related to testing
Priority
Critical
The priority is critical
Priority
High
The priority is high
Priority
Low
The priority is low
Priority
Medium
The priority is medium
Reviewed
Confirmed
Issue has been confirmed
Reviewed
Duplicate
This issue or pull request already exists
Reviewed
Invalid
Invalid issue
Reviewed
Won't Fix
This issue won't be fixed
Status
Abandoned
Somebody has started to work on this but abandoned work
Status
Blocked
Something is blocking this issue or pull request
Status
Need More Info
Feedback is required to reproduce issue or to continue work
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Safeguarding/sciop!456
Reference in a new issue
Safeguarding/sciop
No description provided.
Delete branch "dataset-scopes"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?