Skip to content
Snippets Groups Projects
account.rs 20.4 KiB
Newer Older
  • Learn to ignore specific revisions
  • Timo Kösters's avatar
    Timo Kösters committed
    use std::{collections::BTreeMap, convert::TryInto};
    
    
    use super::{State, DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
    use crate::{pdu::PduBuilder, utils, ConduitResult, Database, Error, Ruma};
    use ruma::{
        api::client::{
            error::ErrorKind,
            r0::{
                account::{
                    change_password, deactivate, get_username_availability, register, whoami,
                    ThirdPartyIdRemovalStatus,
                },
                uiaa::{AuthFlow, UiaaInfo},
            },
        },
    
    Timo Kösters's avatar
    Timo Kösters committed
        events::{
            room::canonical_alias, room::guest_access, room::history_visibility, room::join_rules,
            room::member, room::name, room::topic, EventType,
        },
        RoomAliasId, RoomId, RoomVersionId, UserId,
    
    use register::RegistrationKind;
    
    #[cfg(feature = "conduit_bin")]
    use rocket::{get, post};
    
    const GUEST_NAME_LENGTH: usize = 10;
    
    
    /// # `GET /_matrix/client/r0/register/available`
    ///
    /// Checks if a username is valid and available on this server.
    ///
    /// - Returns true if no user or appservice on this server claimed this username
    /// - This will not reserve the username, so the username might become invalid when trying to register
    
    #[cfg_attr(
        feature = "conduit_bin",
        get("/_matrix/client/r0/register/available", data = "<body>")
    )]
    pub fn get_register_available_route(
        db: State<'_, Database>,
    
    Timo Kösters's avatar
    Timo Kösters committed
        body: Ruma<get_username_availability::Request<'_>>,
    
    ) -> ConduitResult<get_username_availability::Response> {
        // Validate user id
        let user_id = UserId::parse_with_server_name(body.username.clone(), db.globals.server_name())
            .ok()
            .filter(|user_id| {
                !user_id.is_historical() && user_id.server_name() == db.globals.server_name()
            })
            .ok_or(Error::BadRequest(
                ErrorKind::InvalidUsername,
                "Username is invalid.",
            ))?;
    
        // Check if username is creative enough
        if db.users.exists(&user_id)? {
            return Err(Error::BadRequest(
                ErrorKind::UserInUse,
                "Desired user ID is already taken.",
            ));
        }
    
        // TODO add check for appservice namespaces
    
        // If no if check is true we have an username that's available to be used.
        Ok(get_username_availability::Response { available: true }.into())
    }
    
    
    Timo's avatar
    Timo committed
    /// # `POST /_matrix/client/r0/register`
    
    ///
    /// Register an account on this homeserver.
    ///
    /// - Returns the device id and access_token unless `inhibit_login` is true
    /// - When registering a guest account, all parameters except initial_device_display_name will be
    /// ignored
    /// - Creates a new account and a device for it
    /// - The account will be populated with default account data
    
    #[cfg_attr(
        feature = "conduit_bin",
        post("/_matrix/client/r0/register", data = "<body>")
    )]
    
    Timo Kösters's avatar
    Timo Kösters committed
    pub async fn register_route(
    
        db: State<'_, Database>,
    
    Timo Kösters's avatar
    Timo Kösters committed
        body: Ruma<register::Request<'_>>,
    
    ) -> ConduitResult<register::Response> {
        if db.globals.registration_disabled() {
            return Err(Error::BadRequest(
                ErrorKind::Forbidden,
                "Registration has been disabled.",
            ));
        }
    
    
    Timo Kösters's avatar
    Timo Kösters committed
        let is_guest = body.kind == RegistrationKind::Guest;
    
    Timo's avatar
    Timo committed
        let mut missing_username = false;
    
    
        // Validate user id
        let user_id = UserId::parse_with_server_name(
    
            if is_guest {
                utils::random_string(GUEST_NAME_LENGTH)
            } else {
    
    Timo's avatar
    Timo committed
                body.username.clone().unwrap_or_else(|| {
                    // If the user didn't send a username field, that means the client is just trying
                    // the get an UIAA error to see available flows
                    missing_username = true;
                    // Just give the user a random name. He won't be able to register with it anyway.
                    utils::random_string(GUEST_NAME_LENGTH)
                })
    
            }
            .to_lowercase(),
    
            db.globals.server_name(),
        )
        .ok()
        .filter(|user_id| !user_id.is_historical() && user_id.server_name() == db.globals.server_name())
        .ok_or(Error::BadRequest(
            ErrorKind::InvalidUsername,
            "Username is invalid.",
        ))?;
    
        // Check if username is creative enough
    
    Timo's avatar
    Timo committed
        if !missing_username && db.users.exists(&user_id)? {
    
            return Err(Error::BadRequest(
                ErrorKind::UserInUse,
                "Desired user ID is already taken.",
            ));
        }
    
        // UIAA
        let mut uiaainfo = UiaaInfo {
            flows: vec![AuthFlow {
                stages: vec!["m.login.dummy".to_owned()],
            }],
            completed: Vec::new(),
            params: Default::default(),
            session: None,
            auth_error: None,
        };
    
        if let Some(auth) = &body.auth {
            let (worked, uiaainfo) =
                db.uiaa
                    .try_auth(&user_id, "".into(), auth, &uiaainfo, &db.users, &db.globals)?;
            if !worked {
                return Err(Error::Uiaa(uiaainfo));
            }
        // Success!
        } else {
            uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
            db.uiaa.create(&user_id, "".into(), &uiaainfo)?;
            return Err(Error::Uiaa(uiaainfo));
        }
    
    
    Timo's avatar
    Timo committed
        if missing_username {
    
            return Err(Error::BadRequest(
                ErrorKind::MissingParam,
                "Missing username field.",
            ));
    
        let password = if is_guest {
            None
        } else {
            body.password.clone()
        }
        .unwrap_or_default();
    
    
        // Create user
        db.users.create(&user_id, &password)?;
    
    
        // Initial data
        db.account_data.update(
            None,
            &user_id,
            EventType::PushRules,
            &ruma::events::push_rules::PushRulesEvent {
                content: ruma::events::push_rules::PushRulesEventContent {
                    global: crate::push_rules::default_pushrules(&user_id),
                },
            },
            &db.globals,
        )?;
    
    
        if !is_guest && body.inhibit_login {
    
            return Ok(register::Response {
                access_token: None,
                user_id,
                device_id: None,
            }
            .into());
        }
    
    
        // Generate new device id if the user didn't specify one
    
        let device_id = if is_guest {
            None
        } else {
            body.device_id.clone()
        }
        .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
    
    
        // Generate new token for the device
        let token = utils::random_string(TOKEN_LENGTH);
    
        // Add device
        db.users.create_device(
            &user_id,
            &device_id,
            &token,
            body.initial_device_display_name.clone(),
        )?;
    
    
    Timo Kösters's avatar
    Timo Kösters committed
        // If this is the first user on this server, create the admins room
        if db.users.count() == 1 {
            // Create a user for the server
            let conduit_user = UserId::parse_with_server_name("conduit", db.globals.server_name())
                .expect("@conduit:server_name is valid");
    
            db.users.create(&conduit_user, "")?;
    
            let room_id = RoomId::new(db.globals.server_name());
    
            let mut content = ruma::events::room::create::CreateEventContent::new(conduit_user.clone());
            content.federate = true;
            content.predecessor = None;
            content.room_version = RoomVersionId::Version6;
    
            // 1. The room create event
            db.rooms.build_and_append_pdu(
                PduBuilder {
                    event_type: EventType::RoomCreate,
                    content: serde_json::to_value(content).expect("event is valid, we just created it"),
                    unsigned: None,
                    state_key: Some("".to_owned()),
                    redacts: None,
                },
                &conduit_user,
                &room_id,
                &db.globals,
                &db.sending,
                &db.account_data,
            )?;
    
            // 2. Make conduit bot join
            db.rooms.build_and_append_pdu(
                PduBuilder {
                    event_type: EventType::RoomMember,
                    content: serde_json::to_value(member::MemberEventContent {
                        membership: member::MembershipState::Join,
                        displayname: None,
                        avatar_url: None,
                        is_direct: None,
                        third_party_invite: None,
                    })
                    .expect("event is valid, we just created it"),
                    unsigned: None,
                    state_key: Some(conduit_user.to_string()),
                    redacts: None,
                },
                &conduit_user,
                &room_id,
                &db.globals,
                &db.sending,
                &db.account_data,
            )?;
    
            // 3. Power levels
            let mut users = BTreeMap::new();
            users.insert(conduit_user.clone(), 100.into());
            users.insert(user_id.clone(), 100.into());
    
            db.rooms.build_and_append_pdu(
                PduBuilder {
                    event_type: EventType::RoomPowerLevels,
                    content: serde_json::to_value(
                        ruma::events::room::power_levels::PowerLevelsEventContent {
                            ban: 50.into(),
                            events: BTreeMap::new(),
                            events_default: 0.into(),
                            invite: 50.into(),
                            kick: 50.into(),
                            redact: 50.into(),
                            state_default: 50.into(),
                            users,
                            users_default: 0.into(),
                            notifications: ruma::events::room::power_levels::NotificationPowerLevels {
                                room: 50.into(),
                            },
                        },
                    )
                    .expect("event is valid, we just created it"),
                    unsigned: None,
                    state_key: Some("".to_owned()),
                    redacts: None,
                },
                &conduit_user,
                &room_id,
                &db.globals,
                &db.sending,
                &db.account_data,
            )?;
    
            // 4.1 Join Rules
            db.rooms.build_and_append_pdu(
                PduBuilder {
                    event_type: EventType::RoomJoinRules,
                    content: serde_json::to_value(join_rules::JoinRulesEventContent::new(
                        join_rules::JoinRule::Invite,
                    ))
                    .expect("event is valid, we just created it"),
                    unsigned: None,
                    state_key: Some("".to_owned()),
                    redacts: None,
                },
                &conduit_user,
                &room_id,
                &db.globals,
                &db.sending,
                &db.account_data,
            )?;
    
            // 4.2 History Visibility
            db.rooms.build_and_append_pdu(
                PduBuilder {
                    event_type: EventType::RoomHistoryVisibility,
                    content: serde_json::to_value(
                        history_visibility::HistoryVisibilityEventContent::new(
                            history_visibility::HistoryVisibility::Shared,
                        ),
                    )
                    .expect("event is valid, we just created it"),
                    unsigned: None,
                    state_key: Some("".to_owned()),
                    redacts: None,
                },
                &conduit_user,
                &room_id,
                &db.globals,
                &db.sending,
                &db.account_data,
            )?;
    
            // 4.3 Guest Access
            db.rooms.build_and_append_pdu(
                PduBuilder {
                    event_type: EventType::RoomGuestAccess,
                    content: serde_json::to_value(guest_access::GuestAccessEventContent::new(
                        guest_access::GuestAccess::Forbidden,
                    ))
                    .expect("event is valid, we just created it"),
                    unsigned: None,
                    state_key: Some("".to_owned()),
                    redacts: None,
                },
                &conduit_user,
                &room_id,
                &db.globals,
                &db.sending,
                &db.account_data,
            )?;
    
            // 6. Events implied by name and topic
            db.rooms.build_and_append_pdu(
                PduBuilder {
                    event_type: EventType::RoomName,
                    content: serde_json::to_value(
                        name::NameEventContent::new("Admin Room".to_owned()).map_err(|_| {
                            Error::BadRequest(ErrorKind::InvalidParam, "Name is invalid.")
                        })?,
                    )
                    .expect("event is valid, we just created it"),
                    unsigned: None,
                    state_key: Some("".to_owned()),
                    redacts: None,
                },
                &conduit_user,
                &room_id,
                &db.globals,
                &db.sending,
                &db.account_data,
            )?;
    
            db.rooms.build_and_append_pdu(
                PduBuilder {
                    event_type: EventType::RoomTopic,
                    content: serde_json::to_value(topic::TopicEventContent {
                        topic: format!("Manage {}", db.globals.server_name()),
                    })
                    .expect("event is valid, we just created it"),
                    unsigned: None,
                    state_key: Some("".to_owned()),
                    redacts: None,
                },
                &conduit_user,
                &room_id,
                &db.globals,
                &db.sending,
                &db.account_data,
            )?;
    
            // Room alias
            let alias: RoomAliasId = format!("#admins:{}", db.globals.server_name())
                .try_into()
                .expect("#admins:server_name is a valid alias name");
    
            db.rooms.build_and_append_pdu(
                PduBuilder {
                    event_type: EventType::RoomCanonicalAlias,
                    content: serde_json::to_value(canonical_alias::CanonicalAliasEventContent {
                        alias: Some(alias.clone()),
                        alt_aliases: Vec::new(),
                    })
                    .expect("event is valid, we just created it"),
                    unsigned: None,
                    state_key: Some("".to_owned()),
                    redacts: None,
                },
                &conduit_user,
                &room_id,
                &db.globals,
                &db.sending,
                &db.account_data,
            )?;
    
            db.rooms.set_alias(&alias, Some(&room_id), &db.globals)?;
    
            // Invite and join the real user
            db.rooms.build_and_append_pdu(
                PduBuilder {
                    event_type: EventType::RoomMember,
                    content: serde_json::to_value(member::MemberEventContent {
                        membership: member::MembershipState::Invite,
                        displayname: None,
                        avatar_url: None,
                        is_direct: None,
                        third_party_invite: None,
                    })
                    .expect("event is valid, we just created it"),
                    unsigned: None,
                    state_key: Some(user_id.to_string()),
                    redacts: None,
                },
                &conduit_user,
                &room_id,
                &db.globals,
                &db.sending,
                &db.account_data,
            )?;
            db.rooms.build_and_append_pdu(
                PduBuilder {
                    event_type: EventType::RoomMember,
                    content: serde_json::to_value(member::MemberEventContent {
                        membership: member::MembershipState::Join,
                        displayname: None,
                        avatar_url: None,
                        is_direct: None,
                        third_party_invite: None,
                    })
                    .expect("event is valid, we just created it"),
                    unsigned: None,
                    state_key: Some(user_id.to_string()),
                    redacts: None,
                },
                &user_id,
                &room_id,
                &db.globals,
                &db.sending,
                &db.account_data,
            )?;
        }
    
    
        Ok(register::Response {
            access_token: Some(token),
            user_id,
            device_id: Some(device_id),
        }
        .into())
    }
    
    
    /// # `POST /_matrix/client/r0/account/password`
    ///
    /// Changes the password of this account.
    ///
    /// - Invalidates all other access tokens if logout_devices is true
    /// - Deletes all other devices and most of their data (to-device events, last seen, etc.) if
    /// logout_devices is true
    
    #[cfg_attr(
        feature = "conduit_bin",
        post("/_matrix/client/r0/account/password", data = "<body>")
    )]
    pub fn change_password_route(
        db: State<'_, Database>,
    
    Timo Kösters's avatar
    Timo Kösters committed
        body: Ruma<change_password::Request<'_>>,
    
    ) -> ConduitResult<change_password::Response> {
        let sender_id = body.sender_id.as_ref().expect("user is authenticated");
        let device_id = body.device_id.as_ref().expect("user is authenticated");
    
        let mut uiaainfo = UiaaInfo {
            flows: vec![AuthFlow {
                stages: vec!["m.login.password".to_owned()],
            }],
            completed: Vec::new(),
            params: Default::default(),
            session: None,
            auth_error: None,
        };
    
        if let Some(auth) = &body.auth {
            let (worked, uiaainfo) = db.uiaa.try_auth(
                &sender_id,
                device_id,
                auth,
                &uiaainfo,
                &db.users,
                &db.globals,
            )?;
            if !worked {
                return Err(Error::Uiaa(uiaainfo));
            }
        // Success!
        } else {
            uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
            db.uiaa.create(&sender_id, &device_id, &uiaainfo)?;
            return Err(Error::Uiaa(uiaainfo));
        }
    
        db.users.set_password(&sender_id, &body.new_password)?;
    
        // TODO: Read logout_devices field when it's available and respect that, currently not supported in Ruma
        // See: https://github.com/ruma/ruma/issues/107
        // Logout all devices except the current one
        for id in db
            .users
            .all_device_ids(&sender_id)
            .filter_map(|id| id.ok())
            .filter(|id| id != device_id)
        {
            db.users.remove_device(&sender_id, &id)?;
        }
    
        Ok(change_password::Response.into())
    }
    
    
    /// # `GET _matrix/client/r0/account/whoami`
    ///
    /// Get user_id of this account.
    ///
    /// - Also works for Application Services
    
    #[cfg_attr(
        feature = "conduit_bin",
        get("/_matrix/client/r0/account/whoami", data = "<body>")
    )]
    pub fn whoami_route(body: Ruma<whoami::Request>) -> ConduitResult<whoami::Response> {
        let sender_id = body.sender_id.as_ref().expect("user is authenticated");
        Ok(whoami::Response {
            user_id: sender_id.clone(),
        }
        .into())
    }
    
    
    /// # `POST /_matrix/client/r0/account/deactivate`
    ///
    /// Deactivate this user's account
    ///
    /// - Leaves all rooms and rejects all invitations
    /// - Invalidates all access tokens
    /// - Deletes all devices
    /// - Removes ability to log in again
    
    #[cfg_attr(
        feature = "conduit_bin",
        post("/_matrix/client/r0/account/deactivate", data = "<body>")
    )]
    
    pub async fn deactivate_route(
    
        db: State<'_, Database>,
    
    Timo Kösters's avatar
    Timo Kösters committed
        body: Ruma<deactivate::Request<'_>>,
    
    ) -> ConduitResult<deactivate::Response> {
        let sender_id = body.sender_id.as_ref().expect("user is authenticated");
        let device_id = body.device_id.as_ref().expect("user is authenticated");
    
        let mut uiaainfo = UiaaInfo {
            flows: vec![AuthFlow {
                stages: vec!["m.login.password".to_owned()],
            }],
            completed: Vec::new(),
            params: Default::default(),
            session: None,
            auth_error: None,
        };
    
        if let Some(auth) = &body.auth {
            let (worked, uiaainfo) = db.uiaa.try_auth(
                &sender_id,
                &device_id,
                auth,
                &uiaainfo,
                &db.users,
                &db.globals,
            )?;
            if !worked {
                return Err(Error::Uiaa(uiaainfo));
            }
        // Success!
        } else {
            uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
            db.uiaa.create(&sender_id, &device_id, &uiaainfo)?;
            return Err(Error::Uiaa(uiaainfo));
        }
    
        // Leave all joined rooms and reject all invitations
        for room_id in db
            .rooms
            .rooms_joined(&sender_id)
            .chain(db.rooms.rooms_invited(&sender_id))
        {
            let room_id = room_id?;
            let event = member::MemberEventContent {
                membership: member::MembershipState::Leave,
                displayname: None,
                avatar_url: None,
                is_direct: None,
                third_party_invite: None,
            };
    
    
    Timo Kösters's avatar
    Timo Kösters committed
            db.rooms.build_and_append_pdu(
                PduBuilder {
                    event_type: EventType::RoomMember,
                    content: serde_json::to_value(event).expect("event is valid, we just created it"),
                    unsigned: None,
                    state_key: Some(sender_id.to_string()),
                    redacts: None,
                },
                &sender_id,
                &room_id,
                &db.globals,
                &db.sending,
                &db.account_data,
            )?;
    
        }
    
        // Remove devices and mark account as deactivated
        db.users.deactivate_account(&sender_id)?;
    
        Ok(deactivate::Response {
            id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
        }
        .into())
    }