users: Introduce User guard for routes Rockets guards are very powerful to disallow users for certain routes. This far this wasn't implemented, and allowed no-one other than the first user to sign up. This change introduces the User guard and employs it for a few routes. The guard works by checking the encrypted cookie for the user_id, and perform a database lookup on it.

zj
Sep 13, 2021, 7:12 PM
DSWQKJRHGLKXUDXKMRXCIQZKEGXYV3H5LPUMGSV4HQ4HYPTI47GQC

Dependencies

  • [2] KULVODXD routes: User route mod exposes routes() now Other routes already did this to keep the routes clean when mounting. Now the user routes do too.
  • [3] FS2NWBVN pijul: Start of push/pull work This change includes one API endpoint, .pijul. It allows for getting a channels remote ID. A lot of plumbing around repositories is added too, from init to opening pristine and actions like it.
  • [4] T7TT5B4G models: Put User model in their own file Before adding a next model, let's start organizing the code a little, to make extending easier.
  • [5] 5UNA2DEA routes: Register and authenticate users Allow users to sign up, and sign in/sign out. The routes are added, though the design of the pages is very bare bones still, it's hard to go through the full flow to demo. On the server side: Passwords are stored encrypted in the database with salts. This uses the PG encrypt tooling to prevent against bugs and maintainance costs on this project. When a user is signed in, the user ID is set in a private cookie. Rocket has Guards for routes, which has not been implemented yet for this project.
  • [6] TWIZ7QV4 db: Add interface to add a project Right now a project has a name, and an owner which is hardcoded to 1. This is because basically I'm speedrunning to implement push/pull of Pijul and then revisit to add depth to features and tests. Model code is now split into files properly too.

Change contents

  • replacement in src/users.rs at line 3
    [3.2378][3.2378:2409]()
    http::{Cookie, CookieJar},
    [3.2378]
    [3.2409]
    http::{Cookie, CookieJar, Status},
    request,
    request::{FromRequest, Outcome},
  • replacement in src/users.rs at line 7
    [3.2442][2.0:18]()
    Route, State,
    [3.2442]
    [3.2453]
    Request, Route, State,
  • edit in src/users.rs at line 13
    [3.32]
    [3.2543]
    use rocket::outcome::try_outcome;
  • replacement in src/users.rs at line 16
    [3.2544][3.2544:2559]()
    #[get("/new")]
    [3.2544]
    [2.19]
    // TODO decide if this routing file is the place for this guard to be at?
    #[rocket::async_trait]
    impl<'r> FromRequest<'r> for User {
    type Error = ();
    async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
    let db = try_outcome!(request.guard::<&State<Database>>().await);
    // &State<Database>>().await);
    let cookies = request.cookies();
    let some_id = match get_user_id_from_cookie(cookies) {
    Some(id) => id,
    None => return Outcome::Failure((Status::Unauthorized, ())),
    };
    match User::find(db, some_id).await {
    Ok(u) => Outcome::Success(u),
    Err(_e) => Outcome::Failure((Status::Unauthorized, ())),
    }
    }
    }
    #[get("/new", rank = 2)]
  • edit in src/users.rs at line 43
    [3.2648]
    [3.2648]
    #[get("/new", rank = 1)]
    fn new_already_user(_user: User) -> Redirect {
    Redirect::to("/")
    }
  • edit in src/users.rs at line 134
    [3.5288]
    [3.5288]
    }
    fn get_user_id_from_cookie(jar: &CookieJar) -> Option<i32> {
    match jar.get_private(COOKIE_USER_ID_KEY) {
    Some(id) => Some(id.value().parse::<i32>().ok()?),
    None => None,
    }
  • replacement in src/users.rs at line 144
    [2.241][2.241:298]()
    routes![new, create, get_sign_in, sign_in, sign_out]
    [2.241]
    [2.298]
    routes![
    new,
    new_already_user,
    create,
    get_sign_in,
    sign_in,
    sign_out
    ]
  • edit in src/projects.rs at line 2
    [3.629]
    [3.667]
    use crate::models::users::User;
  • replacement in src/projects.rs at line 13
    [3.875][3.875:898]()
    fn new() -> Template {
    [3.875]
    [3.898]
    fn new(_user: User) -> Template {
  • edit in src/projects.rs at line 43
    [3.1517]
    [3.1517]
    user: User,
  • replacement in src/projects.rs at line 46
    [3.302][3.302:366]()
    match projects::create(db, 1, project.name.clone()).await {
    [3.302]
    [3.1736]
    match projects::create(db, user.id, project.name.clone()).await {
  • replacement in src/pijul.rs at line 6
    [3.519][3.519:539](),[3.539][3.539:562]()
    #[derive(FromForm)]
    struct ChangelistReq {
    [3.519]
    [3.562]
    /// Because of the overlap with other routes in parameters, this route has
    /// rank 2. This way the changelist matches first, and if it doesn't, this route
    /// will respond. Note that in the push flow this order is reversed.
    #[get("/<org_path>/<proj_path>/.pijul?<channel>&<id>", rank = 2)]
    async fn remote_id(
    db: &State<Database>,
    repoman: &State<RepoMan>,
    org_path: String,
    proj_path: String,
  • edit in src/pijul.rs at line 17
    [3.655]
    [3.655]
    ) -> Option<String> {
    let p = match projects::find(db, org_path, proj_path).await {
    Some(p) => p,
    None => return None,
    };
    match p.repository(&repoman.storage_root) {
    Ok(r) => r.channel_remote_id(channel, id),
    Err(e) => {
    println!("{}", e);
    None
    }
    }
  • replacement in src/pijul.rs at line 32
    [3.658][3.658:698](),[3.698][3.698:750]()
    // TODO is this for push and/or pull???
    #[get("/<org_path>/<proj_path>/.pijul?<cl_req..>")]
    [3.658]
    [3.750]
    #[get("/<org_path>/<proj_path>/.pijul?<changelist>&<channel>")]
  • replacement in src/pijul.rs at line 38
    [3.872][3.872:899]()
    cl_req: ChangelistReq,
    [3.872]
    [3.899]
    channel: String, // TODO check if this breaks non-UTF-8 channels
    changelist: u64,
  • replacement in src/pijul.rs at line 47
    [3.1094][3.1094:1148]()
    Ok(r) => r.channel_remote_id(cl_req.channel),
    [3.1094]
    [3.1148]
    Ok(r) => r.changelist(channel, changelist).ok(),
  • replacement in src/pijul.rs at line 49
    [3.1168][3.1168:1199]()
    println!("{}", e);
    [3.1168]
    [3.1199]
    println!("error getting changelist: {}", e);
  • edit in src/pijul.rs at line 53
    [3.1232][3.1232:1275]()
    // - Reverse engineer the rest of Push
  • replacement in src/pijul.rs at line 56
    [3.1310][3.1310:1334]()
    routes![changelist]
    [3.1310]
    [3.1334]
    routes![remote_id, changelist]
  • edit in src/models/users.rs at line 18
    [3.2407]
    [3.2407]
    pub async fn find(db: &State<Database>, id: i32) -> Result<Self, sqlx::Error> {
    query_as!(User, "SELECT * FROM users WHERE id = $1", id)
    .fetch_one(&**db)
    .await
    }
  • edit in src/models/pijul/repositories.rs at line 4
    [3.2642]
    [3.2642]
    use libpijul::Base32;
  • edit in src/models/pijul/repositories.rs at line 7
    [3.2685]
    [3.2685]
    use libpijul::TxnTExt;
  • replacement in src/models/pijul/repositories.rs at line 72
    [3.4541][3.4541:4614]()
    pub fn channel_remote_id(&self, channel: String) -> Option<String> {
    [3.4541]
    [3.4614]
    pub fn channel_remote_id(&self, channel: String, _id: Option<String>) -> Option<String> {
  • edit in src/models/pijul/repositories.rs at line 80
    [3.4857][3.4857:4868]()
    //
  • edit in src/models/pijul/repositories.rs at line 93
    [3.5177]
    [3.5177]
    // TODO figure out how to stream the response, instead of allocating all now
    pub fn changelist(&self, channel: String, from: u64) -> Result<String, anyhow::Error> {
    let txn = self.pristine()?.mut_txn_begin()?;
    // TODO validate the txn needs closing?
    // TODO validate the txn needs closing?
    //
    // Or does the drop function do that?
    let chan = match txn.load_channel(&channel)? {
    Some(c) => c,
    None => bail!("failed to read channel transaction"),
    };
    let mut out: String = "".to_string();
    for change in txn.log(&chan.read(), from)? {
    let (offset, (hash, merkle)) = change?;
    let h: libpijul::Hash = hash.into();
    let m: libpijul::Merkle = merkle.into();
    out.push_str(format!("{}.{}.{}", offset, h.to_base32(), m.to_base32()).as_str());
    }
    Ok(out)
    }