示例#1
0
async def request_reset_user_password(
        user_id: str,
        user: UserInfo = Depends(Authentication()),
):
    is_admin = 'admin' in user['roles']
    if not is_admin:
        raise HTTPException(401)
    user_data = DotDict.from_obj(await async_user_collection.find_one(
        {'_id': user_id}))
    if user_data is None:
        raise HTTPException(404, "User not found")
    if user_data.get('registration_token'):
        await update_resend_registration(user_data)
    else:
        token_valid_until = int(time.time() +
                                config.manager.token_valid.password_reset)
        user_data['password_reset_token'] = create_token(
            user_data['_id'], token_valid_until)

        await async_user_collection.update_one({'_id': user_data['_id']}, {
            '$set': {
                'password_reset_token': user_data['password_reset_token']
            },
        })
        await async_send_mail_reset_password(user_data, token_valid_until)
示例#2
0
async def get_user(
    user_id: str, user: UserInfo = Depends(Authentication())) -> UserViewData:
    """Gets user data."""
    is_admin = 'admin' in user['roles']
    is_self = user.sub == user_id
    if not is_self and not is_admin:
        raise HTTPException(401)
    user_data = DotDict.from_obj(await async_user_collection.find_one(
        {'_id': user_id}))
    if user_data is None:
        raise HTTPException(404)
    user_data['sub'] = user_data['_id']
    if user_data.get('picture') is not None:
        user_data[
            'picture'] = f"{config.oauth2.base_url}/picture/{user_data['picture']}"
    if 'password' in user_data:
        del user_data['password']
    return UserViewData(
        user_id=user_data['_id'],
        properties=[
            UserPropertyWithValue(key=prop,
                                  value=_get_user_property_value(
                                      prop, user_data, is_self, is_admin),
                                  **config.oauth2.user.properties[prop].dict())
            for prop in config.manager.view
            if config.oauth2.user.properties[prop].can_read.has_access(
                is_self, is_admin) or config.oauth2.user.properties[prop].
            can_edit.has_access(is_self, is_admin)
        ])
示例#3
0
async def get_register_user(
        token: str = Header(..., alias="x-token"), ) -> UserViewData:
    """Gets user data for registration."""
    check_token(token)
    user_data = DotDict.from_obj(await async_user_collection.find_one({
        'registration_token':
        token,
    }))
    if user_data is None:
        raise HTTPException(401, "Invalid token")
    user_data['sub'] = user_data['_id']
    if user_data.get('picture') is not None:
        user_data[
            'picture'] = f"{config.oauth2.base_url}/picture/{user_data['picture']}"
    if 'password' in user_data:
        del user_data['password']
    # User will be active afterwards, send active
    user_data['active'] = True
    return UserViewData(
        user_id=user_data['_id'],
        properties=[
            UserPropertyWithValue(key=prop,
                                  value=_get_user_property_value(
                                      prop,
                                      user_data,
                                      is_self=True,
                                      is_registering=True),
                                  **config.oauth2.user.properties[prop].dict())
            for prop in config.manager.registration
            if config.oauth2.user.properties[prop].can_read.has_access(
                is_self=True) or config.oauth2.user.properties[prop].can_edit.
            has_access(is_self=True)
        ])
async def async_send_mail_reset_password(user_data: DotDict,
                                         token_valid_until: int,
                                         tz: datetime.tzinfo = None):
    if tz is None:
        tz = _get_tz(user_data.get('zoneinfo'))
    await mailer.async_send_mail(
        user_data.get('locale', 'en_us'),
        'password_reset',
        user_data['email'],
        {
            'password_reset_link':
            f"reset-password/{user_data['password_reset_token']}",
            'valid_until': datetime.fromtimestamp(token_valid_until, tz),
            'user': user_data,
        },
    )
async def async_send_mail_register(user_data: DotDict,
                                   token_valid_until: int,
                                   locale: str = None,
                                   tz: datetime.tzinfo = None):
    if tz is None:
        tz = _get_tz(user_data.get('zoneinfo'))
    if locale is None:
        locale = user_data.get('locale', 'en_us'),
    await mailer.async_send_mail(
        locale,
        'register',
        user_data['email'],
        {
            'registration_link': f"register/{user_data['registration_token']}",
            'valid_until': datetime.fromtimestamp(token_valid_until, tz),
            'user': user_data,
        },
    )
示例#6
0
async def resend_registration(
        user_id: str,
        user: UserInfo = Depends(Authentication()),
):
    """Sends the registration token email again."""
    is_admin = 'admin' in user['roles']
    if not is_admin:
        raise HTTPException(401)
    user_data = DotDict.from_obj(await async_user_collection.find_one(
        {'_id': user_id}))
    if user_data is None:
        raise HTTPException(404)
    await update_resend_registration(user_data)
示例#7
0
async def create_user(no_registration: bool = Query(False),
                      create_data: Dict[str, Any] = Body(...),
                      user: UserInfo = Depends(Authentication())):
    """Updates user data."""
    is_admin = 'admin' in user['roles']
    if not is_admin:
        raise HTTPException(401)

    user_data = DotDict()
    await _update_user(user_data,
                       create_data,
                       is_new=True,
                       is_admin=True,
                       no_registration=no_registration)
async def async_send_mail_verify(locale: Optional[str], mail: str,
                                 user_data: DotDict, token_valid_until: int,
                                 tz: datetime.tzinfo):
    if locale is None:
        locale = user_data.get('locale', 'en_us'),
    await mailer.async_send_mail(
        locale,
        'verify_mail',
        mail,
        {
            'verify_link':
            f"verify-email/{user_data['email_verification_token']}",
            'valid_until': datetime.fromtimestamp(token_valid_until, tz),
            'user': user_data,
        },
    )
示例#9
0
async def reverify_email(
        user_id: str,
        user: UserInfo = Depends(Authentication()),
):
    """Updates user data."""
    is_admin = 'admin' in user['roles']
    is_self = user.sub == user_id
    if not is_self and not is_admin:
        raise HTTPException(401)
    user_data = DotDict.from_obj(await async_user_collection.find_one(
        {'_id': user_id}))
    if user_data is None:
        raise HTTPException(404)
    user_data['email_verified'] = False
    await _update_user(user_data, {'email': user_data['email']},
                       is_admin=is_admin,
                       is_self=is_self)
示例#10
0
async def save_register_user(
        token: str = Header(..., alias="x-token"),
        update_data: Dict[str, Any] = Body(...),
):
    """Gets user data for registration."""
    check_token(token)
    user_data = DotDict.from_obj(await async_user_collection.find_one({
        'registration_token':
        token,
    }))
    if user_data is None:
        raise HTTPException(401, "Invalid token")
    del user_data['registration_token']
    await _update_user(user_data,
                       update_data,
                       is_registering=True,
                       is_self=True)
示例#11
0
async def update_user(user_id: str,
                      update_data: Dict[str, Any] = Body(...),
                      user: UserInfo = Depends(Authentication())):
    """Updates user data."""
    is_admin = 'admin' in user['roles']
    is_self = user.sub == user_id
    if not is_self and not is_admin:
        raise HTTPException(401)
    user_data = DotDict.from_obj(await async_user_collection.find_one(
        {'_id': user_id}))
    if user_data is None:
        raise HTTPException(404)
    if not update_data:
        return
    await _update_user(user_data,
                       update_data,
                       is_admin=is_admin,
                       is_self=is_self)
示例#12
0
async def request_reset_password(password_reset: PasswordReset = Body(...), ):
    user_data = DotDict.from_obj(await async_user_collection.find_one(
        {'email': password_reset.email}))
    if user_data is None:
        return
    if user_data.get('registration_token'):
        await update_resend_registration(user_data)
    else:
        token_valid_until = int(time.time() +
                                config.manager.token_valid.password_reset)
        user_data['password_reset_token'] = create_token(
            user_data['_id'], token_valid_until)

        await async_user_collection.update_one({'_id': user_data['_id']}, {
            '$set': {
                'password_reset_token': user_data['password_reset_token']
            },
        })
        await async_send_mail_reset_password(user_data, token_valid_until)
async def update_user(
    user_data: DotDict,
    update_data: Dict[str, Any],
    is_new: bool = False,
    is_registering: bool = False,
    is_admin: bool = False,
    is_self: bool = False,
    no_registration: bool = False,
):
    if 'sub' in update_data or '_id' in update_data or 'picture' in update_data:
        raise HTTPException(400, f"Cannot modify 'sub', '_id' or 'picture'")
    was_active = user_data.get('active', False)
    reset_user_cache = False

    if is_new:
        assert '_id' not in user_data
        user_data['_id'] = generate_token(48)

    if 'password' in update_data:
        if not isinstance(update_data['password'], str):
            raise HTTPException(400, "'password' must be a string")
        _validate_property_write('password', is_self, is_admin)
        if is_self and not is_registering and user_data.get(
                'password') is not None:
            if 'old_password' not in update_data:
                raise HTTPException(
                    400, f"Need {repr('old_password')} for setting password")
            if not isinstance(update_data['old_password'], str):
                raise HTTPException(400,
                                    f"{repr('old_password')} is not a string")
            is_valid, _ = verify_and_update(update_data['old_password'],
                                            user_data['password'])
            if not is_valid:
                raise HTTPException(401, "Old password does not match")
        try:
            user_data['password'] = create_password(update_data['password'])
            del update_data['password']
        except PasswordLeakedException:
            raise HTTPException(
                400,
                "Password is leaked and cannot be used. See https://haveibeenpwned.com/"
            )

    async def send_mail():
        pass

    if is_registering and update_data.get(
            'email', user_data['email']) == user_data['email']:
        user_data['email_verified'] = True
    elif 'email' in update_data:
        if not isinstance(update_data['email'], str):
            raise HTTPException(400, "'email' must be a string")
        _validate_property_write('email', is_self, is_admin)
        if not is_email(update_data['email'], check_dns=True):
            raise HTTPException(400, "E-Mail address not accepted")
        if await async_user_collection.count_documents(
            {'email': update_data['email']}, limit=1) != 0:
            raise HTTPException(
                400,
                "E-Mail address already in use, please use existing account")
        new_mail = update_data['email']
        locale = update_data.get(
            'locale',
            user_data.get('locale',
                          config.oauth2.user.properties['locale'].default))
        if locale is None:
            locale = 'en_us'
        tz = _get_tz(update_data.get('zoneinfo', user_data.get('zoneinfo')))
        del update_data['email']
        if is_new and not no_registration:
            user_data['email'] = new_mail
            user_data['email_verified'] = False
            token_valid_until = int(time.time() +
                                    config.manager.token_valid.registration)
            user_data['registration_token'] = create_token(
                user_data['_id'], token_valid_until)

            async def send_mail():
                await async_send_mail_register(user_data, token_valid_until,
                                               locale, tz)
        elif not is_admin:
            token_valid_until = int(time.time() +
                                    config.manager.token_valid.email_set)
            user_data['email_verification_token'] = create_token(
                new_mail, token_valid_until)
            if is_registering:
                user_data['email'] = new_mail
                user_data['email_verified'] = False

            async def send_mail():
                await async_send_mail_verify(locale, new_mail, user_data,
                                             token_valid_until, tz)
        else:
            user_data['email'] = new_mail
            user_data['email_verified'] = False

    if 'access_tokens' in update_data:
        if not isinstance(update_data['access_tokens'], list):
            raise HTTPException(400, "'access_tokens' must be a list")
        try:
            access_tokens = [
                ValidateAccessToken.validate(val)
                for val in update_data['access_tokens']
            ]
        except ValueError as err:
            raise HTTPException(400, str(err))
        _validate_property_write('access_tokens', is_self, is_admin)
        existing_access_tokens = [
            UserPasswordAccessToken.validate(access_token)
            for access_token in user_data.get('access_tokens', [])
        ]
        existing_access_tokens_by_id = {
            existing_access_token.id: existing_access_token
            for existing_access_token in existing_access_tokens
        }
        new_access_tokens = []
        for access_token in access_tokens:
            if access_token.id is not None:
                store_token = existing_access_tokens_by_id.get(access_token.id)
                if store_token is None:
                    raise HTTPException(400,
                                        f"Invalid token ID {access_token.id}")
                store_token.description = access_token.description
                if access_token.token is not None:
                    store_token.token = access_token.token
            else:
                store_token = UserPasswordAccessToken(
                    id=generate_token(24),
                    description=access_token.description,
                    token=access_token.token,
                )
            new_access_tokens.append(store_token)
        del update_data['access_tokens']
        user_data['access_tokens'] = [
            access_token.dict() for access_token in new_access_tokens
        ]

    if 'groups' in update_data:
        if await _update_groups(
                user_data,
                update_data,
                property='groups',
                is_self=is_self,
                is_admin=is_admin,
                existence_check_property=None,
                groups_add_property='members',
                groups_pull_properties=(
                    'members',
                    'email_allowed_forward_members',
                    'email_forward_members',
                    'email_postbox_access_members',
                ),
                members_pull_properties=(
                    'email_allowed_forward_members',
                    'email_forward_members',
                    'email_postbox_access_members',
                ),
        ):
            reset_user_cache = True

    if 'email_allowed_forward_groups' in update_data:
        await _update_groups(
            user_data,
            update_data,
            property='email_allowed_forward_groups',
            is_self=is_self,
            is_admin=is_admin,
            existence_check_property='groups',
            groups_add_property='email_allowed_forward_members',
            groups_pull_properties=('email_allowed_forward_members',
                                    'email_forward_members'),
            members_pull_properties=('email_forward_members', ))

    if 'email_forward_groups' in update_data:
        await _update_groups(
            user_data,
            update_data,
            property='email_forward_groups',
            is_self=is_self,
            is_admin=is_admin,
            existence_check_property='email_allowed_forward_groups',
            groups_add_property='email_forward_members',
            groups_pull_properties=('email_forward_members', ),
        )

    if 'email_postbox_access_groups' in update_data:
        await _update_groups(
            user_data,
            update_data,
            property='email_postbox_access_groups',
            is_self=is_self,
            is_admin=is_admin,
            existence_check_property='groups',
            groups_add_property='email_postbox_access_members',
            groups_pull_properties=('email_postbox_access_members', ),
        )

    for key, value in update_data.items():
        _validate_property_write(key, is_self, is_admin)
        prop = config.oauth2.user.properties[key]
        if prop.write_once and user_data.get(key) is not None:
            raise HTTPException(400, f"{repr(key)} can only be set once")
        if not value and not prop.required and key in user_data:
            del user_data[key]
        if prop.type in (UserPropertyType.str, UserPropertyType.multistr,
                         UserPropertyType.token):
            if not isinstance(value, str):
                raise HTTPException(
                    400, f"{repr(key)}={repr(value)} is not a string")
            if prop.template is not None:
                raise HTTPException(400,
                                    f"{repr(key)}={repr(value)} is generated")
            if prop.format is not None:
                regex = get_regex(prop.format)
                if not regex.fullmatch(value):
                    raise HTTPException(
                        400,
                        f"{repr(key)}={repr(value)} does not match pattern {repr(regex.pattern)}"
                    )
            user_data[key] = value
        elif prop.type == UserPropertyType.int:
            if isinstance(value, float):
                if not value.is_integer():
                    raise HTTPException(
                        400, f"{repr(key)}={repr(value)} is not an integer")
                value = int(value)
            if not isinstance(value, int):
                raise HTTPException(
                    400, f"{repr(key)}={repr(value)} is not an integer")
            user_data[key] = value
        elif prop.type == UserPropertyType.bool:
            if not isinstance(value, bool):
                raise HTTPException(
                    400, f"{repr(key)}={repr(value)} is not a boolean")
            user_data[key] = value
        elif prop.type == UserPropertyType.datetime:
            if not isinstance(value, str):
                raise HTTPException(
                    400, f"{repr(key)}={repr(value)} is not a datetime string")
            try:
                parse_datetime(value)
                user_data[key] = value
            except ValueError:
                raise HTTPException(
                    400, f"{repr(key)}={repr(value)} is not a datetime string")
        elif prop.type == UserPropertyType.date:
            if not isinstance(value, str):
                raise HTTPException(
                    400, f"{repr(key)}={repr(value)} is not a date string")
            try:
                parse_date(value)
                user_data[key] = value
            except ValueError:
                raise HTTPException(
                    400, f"{repr(key)}={repr(value)} is not a datetime string")
        elif prop.type == UserPropertyType.enum:
            if not isinstance(value, str):
                raise HTTPException(
                    400, f"{repr(key)}={repr(value)} is not a string")
            assert prop.values is not None
            values = [enum_value.value for enum_value in prop.values]
            if value not in values:
                raise HTTPException(
                    400,
                    f"{repr(key)}={repr(value)} is not a valid enum value")
            user_data[key] = value
        else:
            raise NotImplementedError(f"{repr(prop.type)}")

    # Set others to default
    if is_new or is_registering:
        for key, value in config.oauth2.user.properties.items():
            if value.default is not None and key not in user_data:
                user_data[key] = value.default

    # Activate the user after registration
    if is_registering:
        user_data['active'] = True

    # Apply all templates and validate required, when not active
    if user_data.get('active', False):
        # Validate that all required variables are present
        for key, value in config.oauth2.user.properties.items():
            if value.required and user_data.get(key) is None:
                raise HTTPException(400, f"Missing {repr(key)}")
        # Apply templates (they should not be required)
        for key, value in config.oauth2.user.properties.items():
            if (value.type == UserPropertyType.str
                    and value.template is not None
                    and (not value.write_once or not user_data.get(key))):
                assert "'''" not in value.template, f"Invalid ''' in template: {value.template}"
                user_data[key] = eval(
                    f"f'''{value.template}'''",
                    {
                        'make_username': make_username,
                        'config': config
                    },
                    user_data,
                )

    user_data['updated_at'] = int(time.time())

    User.validate(user_data)

    if is_new:
        await async_user_collection.insert_one(user_data)
    else:
        await async_user_collection.replace_one({'_id': user_data['_id']},
                                                user_data)
    if user_data.get('active', False):
        if reset_user_cache:
            await async_client_user_cache_collection.delete_many(
                {'user_id': user_data['_id']})
        else:
            await async_client_user_cache_collection.update_many(
                {'user_id': user_data['_id']},
                {'$set': {
                    'last_modified': user_data['updated_at']
                }},
            )
    elif was_active:
        await async_client_user_cache_collection.delete_many(
            {'user_id': user_data['_id']})
        await async_token_collection.delete_many({'user_id': user_data['_id']})
        await async_session_collection.delete_many(
            {'user_id': user_data['_id']})
        await async_authorization_code_collection.delete_many(
            {'user_id': user_data['_id']})
    elif reset_user_cache:
        await async_client_user_cache_collection.delete_many(
            {'user_id': user_data['_id']})
    # Last: Send the email if there is one
    await send_mail()
async def _update_groups(
        user_data: DotDict,
        update_data: Dict[str, Any],
        property: str,
        is_self: bool,
        is_admin: bool,
        existence_check_property: Optional[str],
        groups_add_property: str,
        groups_pull_properties: Sequence[str],
        members_pull_properties: Sequence[str] = (),
) -> bool:
    if not isinstance(update_data[property], list) or \
            not all(isinstance(group, str) for group in update_data[property]):
        raise HTTPException(400, f"{repr(property)} must be a string")
    _validate_property_write(property, is_self, is_admin)

    reset_user_cache = False

    new_groups = update_data[property]
    new_groups_set = set(new_groups)
    if existence_check_property is None:
        if await async_user_group_collection.count_documents(
            {'_id': {
                '$in': new_groups
            }}) != len(new_groups):
            raise HTTPException(400, "At least one group does not exist")
    else:
        if not new_groups_set.issubset(user_data[existence_check_property]):
            raise HTTPException(400, f"{property} contains invalid group")
    added_groups = list(new_groups_set.difference(user_data[property]))
    removed_groups = list(set(user_data[property]).difference(new_groups))
    user_data[property] = new_groups
    if added_groups:
        await async_user_group_collection.update_many(
            {'_id': {
                '$in': added_groups
            }},
            {'$addToSet': {
                groups_add_property: user_data['_id']
            }},
        )
        reset_user_cache = True
    if removed_groups:
        await async_user_group_collection.update_many(
            {'_id': {
                '$in': removed_groups
            }},
            {
                '$pull':
                {prop: user_data['_id']
                 for prop in groups_pull_properties}
            },
        )
        for member_property_attr in members_pull_properties:
            member_property: List[str] = user_data.get(member_property_attr,
                                                       [])
            for group in removed_groups:
                try:
                    member_property.remove(group)
                except ValueError:
                    pass
        reset_user_cache = True
    del update_data[property]
    return reset_user_cache