Skip to content

Commit 2cfcc9a

Browse files
authored
Merge pull request #86 from ScilifelabDataCentre/PATs-for-cli-users
SQ-889: Personal access tokens for programmatic access
2 parents eeb9f33 + b09f9a0 commit 2cfcc9a

53 files changed

Lines changed: 2487 additions & 283 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Personal Access Tokens (PATs)
2+
3+
Personal Access Tokens (PATs) provide an alternative authentication mechanism to password and JSON web tokens (JWTs) for users who need to make use of DivBase programmatically. For example, from HPC job scripts or pipelines.
4+
5+
A user never logs in with a PAT, they instead create a PAT via the frontend and pass the PAT in each API call. PATs can be configured to expire or not expire. On every API call, a db lookup is made in the `personal_access_token` table to verify the token and check its permissions - and get the corresponding user. This means that PATs can be revoked immediately by deleting/soft-deleting them from the db.
6+
7+
## PATs vs JWTs
8+
9+
JWTs: When an interactive user logs in via the CLI or the frontend, the server issues a short-lived **JWT access token** and a longer-lived **JWT refresh token**. The CLI auto-refreshes the access token when needed using the refresh token.
10+
11+
PATs have no refresh cycle and no concept of login. A PAT is much closer to an API key. A user creates a PAT via the frontend, copies the plaintext token to their device (probably making it an environment variable) and each API call to DivBase will use the PAT as a `Bearer` token in the `Authorization` header. The `get_current_user` in `deps.py` is responsible for handling the two diff auth mechanisms in the API (JWTs or PATs). All PATs are prefixed with `divbase_pat_` followed by a long random string. The token is only shown to the user at creation time and a hash of it is stored in the db.
12+
13+
## Permissions model
14+
15+
Permissions are stored in the `permissions` column of the `personal_access_token` table as a **JSONB**, validated against the `PATPermissions` Pydantic model (defined in `divbase-lib`):
16+
17+
```python
18+
class PATPermissions(BaseModel):
19+
all_projects: bool = False
20+
projects: dict[str, str] = Field(default_factory=dict)
21+
task_history: bool = False
22+
```
23+
24+
| Scope | Meaning |
25+
|---|---|
26+
| `all_projects` | Access all projects the user is a member of, at their membership role |
27+
| `projects` | Access specific projects only; each project mapped to a max role (`read`, `edit`, `manage`) |
28+
| `task_history` | Access the task history endpoints which are not project specific |
29+
30+
Note that almost all divbase-api routes called by the CLI tool are project scoped. So they rely on the `get_project_member` dependency. The only exceptions to that are:
31+
32+
1. `divbase-cli auth whoami`
33+
2. `divbase-cli task-history user` and `divbase-cli task-history id [job_id]`
34+
35+
For whoami, decision made that all PATs can call this endpoint so can just rely on `get_current_user` dependency which checks if the token is valid but does not check any scopes/permissions. For the task history endpoints, we have a `require_task_history_scope` dependency that checks if the token has the `task_history` scope is set to True. Note that task history project is dependent on the `projects` scope like all other endpoints.
36+
37+
The effective project role for a scoped PAT is `min(pat_role, user_membership_role)` — the PAT can never escalate beyond the user's actual membership.
Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,52 @@
1-
# Account management
1+
# Account Management
22

3-
Simple guide on how to use the website and how to request a project etc...
3+
## Getting a project
44

5-
TODO
5+
!!! Warning
6+
DivBase is under active development and not in production yet. We are shortly planning to create a number of projects for some pilot users. If you would like to be involved in this pilot testing phase, please let us know!
7+
8+
Projects in DivBase are created by the DivBase team. To request a new project, you need to contact us. Please include a very short description of what the project would be for and who you plan to give access too. Please email us with your university affiliated email address. please make sure you have already registered for a DivBase account and verified your email before contacting us about creating a project.
9+
10+
Once your project is created, you will be added as a member with the **Manage** role, which allows you to add and remove other members.
11+
12+
## Adding members to a project
13+
14+
Project members can be added by anyone with the **Manage** role on that project.
15+
16+
1. Navigate to your project page via **Projects** in the navigation bar.
17+
2. Open the project you want to manage.
18+
3. In the **Members** section, click **Add Member**.
19+
4. Enter the email address of the person you want to add and select their role (`read`, `edit`, or `manage`).
20+
5. Click **Add Member** to confirm.
21+
22+
!!! note "The user must already have a DivBase account"
23+
The person must have registered and verified their email before they can be added. If they haven't signed up yet, ask them to do so first.
24+
25+
You can also change a member's role or remove them from the project from the same Members section.
26+
27+
## Resetting your password
28+
29+
If you've forgotten your password, click **Forgot your password?** on the login page. Enter your email address and you'll receive a reset link. The link expires after a short period — if it has expired you can simply request a new one.
30+
31+
You can also change your password at any time from your **Profile** page once logged in.
32+
33+
## Personal Access Tokens
34+
35+
Personal Access Tokens (PATs) let you authenticate with DivBase programmatically without having to store your password on the device. This could be useful in pipelines, or HPC jobs that need to interact with DivBase. You can manage your personal access tokens from the **Profile****Personal Access Tokens** page.
36+
37+
!!! Info
38+
To learn more about how to use a PAT in scripts/pipelines and/or HPC jobs, see [Using DivBase Programmatically](./using-divbase-programmatically.md#use-personal-access-tokens-to-authenticate-programmatically).
39+
40+
To create a PAT:
41+
42+
1. Log in to DivBase and navigate to your **Profile** page. From there you'll see a section to manage your Personal Access Tokens.
43+
2. When adding a new token, you need to give it a name, an optional description and an expiry date.
44+
3. You can also configure the scope of the PAT (What permissions the token has):
45+
- **All projects** — the token can access all projects you're a member of, with same role as you have.
46+
- **Specific projects** — restrict the token to selected projects, each with a maximum role.
47+
- **Task history** — Allow you to use the token to view your entire task history, not filtered by project.
48+
4. Click **Create**. Copy the token immediately — it is shown **only once** and cannot be recovered afterwards.
49+
50+
You can have up to **5 active tokens** at a time. To revoke a token, click **Revoke** next to it on the Personal Access Tokens page.
51+
52+
For how to use a PAT in scripts and HPC jobs, see [Using DivBase Programmatically](./using-divbase-programmatically.md#use-personal-access-tokens-to-authenticate-programmatically).

docs/user-guides/using-divbase-programmatically.md

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,50 @@ If you have any tips or suggestions to add to this page or any desired features,
66

77
TODO: E.G. how to wait for a query to be complete, and download the query results programmatically.
88

9+
## Use Personal Access Tokens to Authenticate programmatically
10+
11+
For scripts, pipelines, and HPC jobs the recommended approach is to use a **Personal Access Token (PAT)**. A PAT is a static bearer token that you create once via the website and pass to `divbase-cli` via an environment variable. When using a PAT, there is no login step and no password storage required.
12+
13+
See [Account Management — Personal Access Tokens](./account-management.md#personal-access-tokens) for how to create/and remove PATs.
14+
15+
Once you have a token, set the `DIVBASE_API_PAT` environment variable to it. `divbase-cli` will automatically use it in every request.
16+
17+
!!! question "What if I have both an active login session and a Personal Access Token set?"
18+
`divbase-cli` prioritises an active login session over a PAT. If you have both, the CLI will use the active session and ignore the PAT. To use the PAT, you would need to run `divbase-cli auth logout` first.
19+
20+
```bash
21+
export DIVBASE_API_PAT="divbase_pat_your_token_here"
22+
divbase-cli files ls
23+
```
24+
25+
When `DIVBASE_API_PAT` is set, `divbase-cli` does not need you to be logged in.
26+
27+
### Example: Slurm job script
28+
29+
The cleanest way to use a PAT in a SLURM job is to store the token in a restricted file and load it at job start:
30+
31+
```bash
32+
echo "divbase_pat_your_token_here" > ~/.divbase_pat
33+
chmod 600 ~/.divbase_pat # only readable/writeable by the owner
34+
```
35+
36+
Then in your SLURM script:
37+
38+
```bash
39+
#!/bin/bash
40+
#SBATCH --job-name=my_divbase_job
41+
#SBATCH --time=24:00:00
42+
# ....
43+
44+
export DIVBASE_API_PAT=$(cat ~/.divbase_pat)
45+
46+
# Download the files you need
47+
divbase-cli files download my_data.vcf.gz
48+
```
49+
50+
!!! tip "Scope your token to what the job needs and when you need it for"
51+
When creating the PAT, restrict it to the specific project(s) you need it for. Consider also setting an appropriate expiry date for the token. You can always revoke the token immediately if needed from the divbase website.
52+
953
## Parse divbase-cli files ls/info output programmatically
1054

1155
1. You can make the output of the `divbase-cli files info` and `divbase-cli files ls` commands in TSV format for easier parsing. Use the `--tsv` flag:
@@ -34,30 +78,3 @@ TODO: E.G. how to wait for a query to be complete, and download the query result
3478
```bash
3579
divbase-cli files stream my_file.vcf.gz | bcftools view -h -
3680
```
37-
38-
## Login programmatically
39-
40-
If you want to use DivBase in a script/job that may take **longer than 1 week** to complete from the time of submission, you can login to DivBase programmatically in the script itself. For security reasons (not having to store your password) it is preferable to instead log in interactively before submitting the script, which will keep you logged in for 1 week.
41-
42-
To login programmatically, use the `--password-stdin` (or `-p`) flag to provide your password via standard input (STDIN) when logging into DivBase with the `divbase-cli auth login` command. Using STDIN prevents the password from ending up in the shell's history, or log-files.
43-
44-
If you use a password manager that allows you to output your password to standard output, you can pipe the password directly into the `divbase-cli auth login` command. For example, if you use `pass` as your password manager, you could do:
45-
46-
```bash
47-
pass show my_divbase_password | divbase-cli auth login EMAIL_ADDRESS --password-stdin
48-
```
49-
50-
An alternative (but less secure) way to do this is to store your password in a plain text file (make sure to set the appropriate permissions on the file to help make it more secure) and then read the password from the file and pipe it into the `divbase-cli auth login` command. For example:
51-
52-
```bash
53-
chmod 600 ~/my_password.txt # only readable/writeable by the owner
54-
cat ~/my_password.txt | divbase-cli auth login EMAIL_ADDRESS --password-stdin
55-
```
56-
57-
Another less secure alternative is to use environment variables to store your password and then echo the password from the environment variable and pipe it into the `divbase-cli auth login` command. For example:
58-
59-
```bash
60-
echo $DIVBASE_PASSWORD | divbase-cli auth login EMAIL_ADDRESS --password-stdin
61-
```
62-
63-
Where `DIVBASE_PASSWORD` is an environment variable that contains your password.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ nav:
104104
- Deployment: development/deployment.md
105105
- Documentation: development/documentation.md
106106
- Sending Emails: development/sending-emails.md
107+
- Personal Access Tokens: development/personal_access_tokens.md
107108
- S3 Transfers: development/s3_transfers.md
108109
- Testing: development/testing.md
109110
- Monitoring and observability: development/monitoring_and_observability.md

packages/divbase-api/src/divbase_api/admin_panel.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@
3838
from divbase_api.deps import _authenticate_frontend_user_from_tokens
3939
from divbase_api.frontend_routes.auth import get_login, post_logout
4040
from divbase_api.models.announcements import AnnouncementDB, AnnouncementLevel, AnnouncementTarget
41+
from divbase_api.models.personal_access_tokens import PersonalAccessTokenDB
4142
from divbase_api.models.project_versions import ProjectVersionDB
4243
from divbase_api.models.projects import ProjectDB, ProjectMembershipDB, ProjectRoles
43-
from divbase_api.models.queue_status import QueueStatus
44+
from divbase_api.models.queue_status import QueueStatusDB
4445
from divbase_api.models.revoked_tokens import RevokedTokenDB, TokenRevokeReason
4546
from divbase_api.models.task_history import CeleryTaskMeta, TaskHistoryDB, TaskStartedAtDB
4647
from divbase_api.models.users import UserDB
@@ -631,7 +632,7 @@ class AnnouncementView(ModelView):
631632

632633
class QueueStatusView(ModelView):
633634
"""
634-
Custom admin panel View for the QueueStatus model.
635+
Custom admin panel View for the QueueStatusDB model.
635636
636637
This is a singleton table (only 1 row allowed).
637638
Deletion and creation are disabled - only editing the existing row is allowed.
@@ -724,6 +725,76 @@ async def validate(self, request: Request, data: dict[str, Any]) -> None:
724725
# as does not work well when modifying the timestamp in edit view, easier to just display as UTC
725726

726727

728+
class PersonalAccessTokenView(ModelView):
729+
"""
730+
Custom admin panel View for the PersonalAccessTokenDB model.
731+
As with passwords, the hashed_token is never displayed in the admin panel on purpose.
732+
"""
733+
734+
page_size_options = PAGINATION_DEFAULTS
735+
fields = [
736+
IntegerField("id", label="ID", disabled=True),
737+
StringField("name", label="Name", required=True),
738+
TextAreaField("description", required=False, label="Description"),
739+
JSONField(
740+
"permissions",
741+
required=False,
742+
label="Permissions",
743+
help_text="Permissions associated with the PAT.",
744+
disabled=True,
745+
),
746+
DateTimeField(
747+
"expires_at",
748+
help_text="Timestamp when the PAT expires. Value can be left empty for no expiration. Value determined by system, cannot be edited.",
749+
required=False,
750+
disabled=True,
751+
),
752+
DateTimeField(
753+
"last_used_at",
754+
help_text="Timestamp when the PAT was last used. Value empty if not yet used. Value determined by system, cannot be edited.",
755+
required=False,
756+
disabled=True,
757+
),
758+
BooleanField("is_deleted", required=True, label="Is Deleted", help_text="Mark the PAT as deleted or not."),
759+
DateTimeField(
760+
"date_deleted",
761+
help_text="Timestamp when the PAT was soft deleted (else None). Value determined by system, cannot be edited.",
762+
disabled=True,
763+
),
764+
IntegerField("user_id", label="User ID", required=False),
765+
HasOne("user", identity="user", label="User"),
766+
DateTimeField("created_at", label="Created At", disabled=True),
767+
DateTimeField("updated_at", label="Updated At", disabled=True),
768+
]
769+
770+
exclude_fields_from_list = ["hashed_token", "permissions", "user_id"]
771+
exclude_fields_from_edit = ["hashed_token", "id", "created_at", "updated_at", "user_id", "user", "permissions"]
772+
exclude_fields_from_detail = ["hashed_token", "user_id"]
773+
774+
def can_create(self, request: Request) -> bool:
775+
"""Disable manual creation of PATs."""
776+
return False
777+
778+
async def edit(self, request: Request, pk: Any, data: dict) -> Any:
779+
"""
780+
Override the default edit method to ensure that the `date_deleted` field is updated
781+
when/if a users soft deletion status is changed.
782+
"""
783+
if "is_deleted" in data:
784+
if data["is_deleted"]:
785+
data["date_deleted"] = datetime.now(tz=timezone.utc)
786+
else:
787+
data["date_deleted"] = None
788+
789+
return await super().edit(request=request, pk=pk, data=data)
790+
791+
async def serialize_field_value(self, value: Any, field: Any, action: RequestAction, request: Request) -> Any:
792+
formatted = _format_cet_datetime(value, field, ["created_at", "updated_at", "expires_at", "last_used_at"])
793+
if formatted is not None:
794+
return formatted
795+
return await super().serialize_field_value(value, field, action, request)
796+
797+
727798
class DivBaseAuthProvider(AuthProvider):
728799
"""
729800
This class enables starlette-admin to make use of DivBase's pre-existing auth system.
@@ -807,5 +878,12 @@ def register_admin_panel(app: FastAPI, engine: AsyncEngine) -> None:
807878
admin.add_view(
808879
AnnouncementView(AnnouncementDB, icon="fas fa-bullhorn", label="Announcements", identity="announcement")
809880
)
810-
admin.add_view(QueueStatusView(QueueStatus, icon="fas fa-power-off", label="Queue Status", identity="queue-status"))
881+
admin.add_view(
882+
QueueStatusView(QueueStatusDB, icon="fas fa-power-off", label="Queue Status", identity="queue-status")
883+
)
884+
admin.add_view(
885+
PersonalAccessTokenView(
886+
PersonalAccessTokenDB, icon="fas fa-key", label="Personal Access Tokens", identity="personal-access-token"
887+
)
888+
)
811889
admin.mount_to(app)

0 commit comments

Comments
 (0)