Skip to content

Commit ef883f2

Browse files
authored
feat: add personal access tokens (#434)
This commit adds API endpoints for managing personal access tokens. Users can create and revoke tokens, and tokens can be used to authenticate API requests. Tokens are created with a description, and an optional expiry date. Tokens can be revoked at any time. There is a UI for managing tokens in the account settings - users can create and revoke tokens from there. Tokens are displayed with their description, expiry date, the granted permissions, and the date they were created. An email is sent to users when a new token is created to notify them of the token's creation. The token creation UI contains a questionnaire flow to help users understand that what they are doing is potentially dangerous, and to guide users to use alternative authentication mechanisms such as OIDC or interactive flow where possible. Fixes #393
1 parent 22a5d95 commit ef883f2

31 files changed

+1677
-63
lines changed

api/.sqlx/query-766d32b1437d5ae0a40ce065d1306ed46b739bb972f1b83cda07841bb0f07453.json

Lines changed: 81 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/.sqlx/query-9e0369495156d984196939d4cb2838ed135acca6f927abd071760f0924e7492d.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/src/api.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1945,6 +1945,31 @@ components:
19451945

19461946
Permission:
19471947
oneOf:
1948+
- type: object
1949+
properties:
1950+
permission:
1951+
type: string
1952+
description: The permission name.
1953+
enum: ["package/publish"]
1954+
scope:
1955+
$ref: "#/components/schemas/ScopeName"
1956+
required:
1957+
- permission
1958+
- scope
1959+
- type: object
1960+
properties:
1961+
permission:
1962+
type: string
1963+
description: The permission name.
1964+
enum: ["package/publish"]
1965+
scope:
1966+
$ref: "#/components/schemas/ScopeName"
1967+
package:
1968+
$ref: "#/components/schemas/PackageName"
1969+
required:
1970+
- permission
1971+
- scope
1972+
- package
19481973
- type: object
19491974
properties:
19501975
permission:

api/src/api/authorization.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ mod tests {
255255
use crate::api::ApiAuthorizationExchangeResponse;
256256
use crate::api::ApiCreateAuthorizationResponse;
257257
use crate::api::ApiFullUser;
258+
use crate::db::PackagePublishPermission;
258259
use crate::db::Permission;
259260
use crate::db::Permissions;
260261
use crate::util::test::ApiResultExt;
@@ -367,12 +368,14 @@ mod tests {
367368

368369
let (verifier, challenge) = new_verifier_and_challenge();
369370

370-
let permissions = Permissions(vec![Permission::VersionPublish {
371-
scope: t.scope.scope.clone(),
372-
package: "test".try_into().unwrap(),
373-
version: "1.0.0".try_into().unwrap(),
374-
tarball_hash: "sha256-1234567890".into(),
375-
}]);
371+
let permissions = Permissions(vec![Permission::PackagePublish(
372+
PackagePublishPermission::Version {
373+
scope: t.scope.scope.clone(),
374+
package: "test".try_into().unwrap(),
375+
version: "1.0.0".try_into().unwrap(),
376+
tarball_hash: "sha256-1234567890".into(),
377+
},
378+
)]);
376379

377380
let mut resp =
378381
create_authorization(&mut t, &challenge, Some(permissions)).await;

api/src/api/errors.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ errors!(
5252
status: NOT_FOUND,
5353
"The requested path was not found.",
5454
},
55+
TokenNotFound {
56+
status: NOT_FOUND,
57+
"The requested token was not found.",
58+
},
5559
InternalServerError {
5660
status: INTERNAL_SERVER_ERROR,
5761
"Internal Server Error",

api/src/api/package.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,14 +1408,19 @@ mod test {
14081408
use crate::db::NewPackageVersion;
14091409
use crate::db::NewPublishingTask;
14101410
use crate::db::NewScopeInvite;
1411+
use crate::db::PackagePublishPermission;
1412+
use crate::db::Permission;
1413+
use crate::db::Permissions;
14111414
use crate::db::PublishingTaskStatus;
1415+
use crate::db::TokenType;
14121416
use crate::ids::PackageName;
14131417
use crate::ids::PackagePath;
14141418
use crate::ids::ScopeName;
14151419
use crate::ids::Version;
14161420
use crate::publish::tests::create_mock_tarball;
14171421
use crate::publish::tests::process_tarball_setup;
14181422
use crate::publish::tests::process_tarball_setup2;
1423+
use crate::token::create_token;
14191424
use crate::util::test::ApiResultExt;
14201425
use crate::util::test::TestSetup;
14211426

@@ -2379,6 +2384,51 @@ ggHohNAjhbzDaY2iBW/m3NC5dehGUP4T2GBo/cwGhg==
23792384
.await;
23802385
}
23812386

2387+
#[tokio::test]
2388+
async fn test_publishing_with_missing_auth() {
2389+
let mut t = TestSetup::new().await;
2390+
2391+
let permission =
2392+
Permission::PackagePublish(PackagePublishPermission::Scope {
2393+
scope: ScopeName::new("otherscope".to_owned()).unwrap(),
2394+
});
2395+
2396+
let token = create_token(
2397+
&t.db(),
2398+
t.user1.user.id,
2399+
TokenType::Web,
2400+
None,
2401+
None,
2402+
Some(Permissions(vec![permission])),
2403+
)
2404+
.await
2405+
.unwrap();
2406+
2407+
let scope = t.scope.scope.clone();
2408+
2409+
let name = PackageName::new("foo".to_owned()).unwrap();
2410+
2411+
let CreatePackageResult::Ok(_) =
2412+
t.db().create_package(&scope, &name).await.unwrap()
2413+
else {
2414+
unreachable!();
2415+
};
2416+
2417+
let data = create_mock_tarball("ok");
2418+
let mut resp = t
2419+
.http()
2420+
.post("/api/scopes/scope/packages/foo/versions/1.2.3?config=/jsr.json")
2421+
.gzip()
2422+
.token(Some(&token))
2423+
.body(Body::from(data))
2424+
.call()
2425+
.await
2426+
.unwrap();
2427+
resp
2428+
.expect_err_code(StatusCode::FORBIDDEN, "missingPermission")
2429+
.await;
2430+
}
2431+
23822432
#[tokio::test]
23832433
async fn test_package_docs() {
23842434
let mut t = TestSetup::new().await;

0 commit comments

Comments
 (0)