async def authorize_card(request: Request, card: CardModel = Body(...), api_key_auth: str = Security(api_key)): """Authorize by card ID, requires configured Api Token.""" if not config.oauth2.card_authentication_api_key: raise HTTPException(500, "Not configured") if api_key_auth != config.oauth2.card_authentication_api_key: raise HTTPException(403) # Find user by card id and authorize by that. user_data = await async_user_collection.find_one({'card_id': card.card_id}) if user_data is None: raise HTTPException(404, "Unknown card") user = User.validate(user_data) if not user.active: raise HTTPException(400, "User not active") user_with_groups = await UserWithRoles.async_load_groups( user, config.oauth2.card_authentication_client_id) request = await oauth2_request(request) request.data['client_id'] = config.oauth2.card_authentication_client_id resp = await run_in_threadpool( authorization.create_authorization_response, request=request, grant_user=user_with_groups, ) if isinstance(resp, ErrorJSONResponse): return resp if isinstance(resp, RedirectResponse): return resp.to_json_response() assert not isinstance(resp, JSONResponse)
async def async_load_all(client_id: str) -> AsyncIterator[User]: client = await async_client_collection.find_one({'_id': client_id}, { '_id': 0, 'access_groups': 1 }) if client is None: raise HTTPException(400, "Invalid client") client_groups = [ access_group for access_group in client['access_groups'] ] client_user_groups = [ access_group['group'] for access_group in client_groups ] all_client_group_maps = await _async_resolve_groups(client_user_groups) all_client_groups = set(group for groups in all_client_group_maps.values() for group in groups) async for user_data in async_user_collection.find( {'groups': { '$in': all_client_groups }}): yield User.validate(user_data)
def load(user_id: str, client_id: str) -> Optional['UserWithRoles']: user_data = user_collection.find_one({'_id': user_id}) if user_data is None: return None user = User.validate(user_data) if not user.active: raise HTTPException(401, "User inactive") return UserWithRoles.load_groups(user, client_id)
async def upload_picture( user_id: str, file: UploadFile = File(..., media_type='application/octet-stream'), user: UserInfo = Depends(Authentication(auto_error=False)), registration_token: Optional[str] = Header(None, alias="x-token"), ): """Uploads a new picture for the passed user.""" if user is not None: is_admin = 'admin' in user['roles'] is_self = user.sub == user_id if not is_self or not is_admin: raise HTTPException(401) user_data = await async_user_collection.find_one({'_id': user_id}) elif registration_token is not None: check_token(registration_token) user_data = await async_user_collection.find_one( {'registration_token': registration_token}) else: raise HTTPException(401) if user_data is None: raise HTTPException(404) user = User.validate(user_data) if user.picture is None: user.picture = generate_token(48) updated_at = int(time.time()) await async_user_collection.update_one( {'_id': user_id}, {'$set': { 'picture': user.picture, 'updated_at': updated_at }}) await async_client_user_cache_collection.update_many( {'user_id': user_data['_id']}, {'$set': { 'last_modified': updated_at }}, ) else: try: await async_user_picture_bucket.delete(user.picture) except gridfs.errors.NoFile: pass hash_ = hashlib.sha512() while True: chunk = await file.read(4 * 1024) if not chunk: break hash_.update(chunk) await file.seek(0) file.file.seek(0) await async_user_picture_bucket.upload_from_stream_with_id( user.picture, user.id, file.file, metadata={ 'content_type': file.content_type, 'hash': hash_.digest() })
def authenticate_user(self, username: str, password: str): user_data = user_collection.find_one({ 'email': username, 'access_tokens.token': password, 'active': True }) if user_data is None: return None return UserWithRoles.load_groups(User.validate(user_data), self.client.id)
], ).dict(exclude_none=True, by_alias=True)) mongo.user_group_collection.insert_one( UserGroup( id='users', visible=False, group_name="All Users", members=[admin_id], ).dict(exclude_none=True, by_alias=True)) mongo.user_group_collection.insert_one( UserGroup( id='admin', visible=False, group_name="Admins", members=[admin_id], ).dict(exclude_none=True, by_alias=True)) registration_token = create_token( admin_id, now + config.manager.token_valid.registration) mongo.user_collection.insert_one( User( id=admin_id, email="admin@localhost", active=False, email_verified=True, groups=['users', 'admin'], updated_at=now, registration_token=registration_token, ).dict(exclude_none=True, by_alias=True)) print("Use the following link to register the administrator:") print(f"{config.manager.frontend_base_url}/register/{registration_token}")
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()
new_id = user.uid else: new_id = generate_token(48) user_mapping[user.uid] = new_id users.append( User( id=new_id, email=user.email, active=True, email_verified=user.email_verified, notes=f"Imported '{user.uid}' from OC", groups=user.groups + ['users'], locale=user.language, zoneinfo=user.timezone, password=user.password, phone_number=user.phone, phone_number_verified=False, updated_at=int(time.time()), preferred_username=preferred_username, given_name=given_name, family_name=family_name, # This token is not usable, but it enforces that the user reviews the user credentials before logging in registration_token='imported', ).dict(exclude_none=True, by_alias=True)) print("Create", users[-1]) groups = [] for group in reader.read_groups(): members = [user_mapping[member] for member in group.members] gid = group.gid.lower()
async def authorize( request: Request, _query_params: OAuthAuthorizeRequestQueryParams = Depends( OAuthAuthorizeRequestQueryParams), auth_credentials: AuthCredentials = Body(...), ): """Perform authorization from given credentials and returns the result.""" retry_delay, retry_after = await async_throttle(request) if retry_delay is not None and retry_after is not None: raise HTTPException(403, "Wait", headers={ 'X-Retry-After': retry_after, 'X-Retry-Wait': retry_delay }) potential_users = async_user_collection.find( {'email': auth_credentials.email}) async for potential_user in potential_users: if potential_user.get('password') is not None: password_valid, new_hash = verify_and_update( auth_credentials.password, potential_user['password']) if password_valid: user_data = potential_user break else: retry_after, retry_delay = await async_throttle_failure_request(request ) raise HTTPException(403, "Invalid E-Mail or Password", headers={ 'X-Retry-After': retry_after, 'X-Retry-Wait': retry_delay }) user = User.validate(user_data) if new_hash is not None: await async_user_collection.update_one( {'_id': user.id}, {'$set': { 'password': new_hash }}) user.password = new_hash if user.registration_token: # If the user password is correct, but a registration token is pending, redirect to registration page from ...manager.api.user_helpers import check_token, create_token import urllib.parse registration_token = user.registration_token try: check_token(user.registration_token) except HTTPException: token_valid_until = int(time.time() + config.manager.token_valid.registration) registration_token = create_token(user_data['_id'], token_valid_until) await async_user_collection.update_one( {'_id': user.id}, {'$set': { 'registration_token': registration_token }}, ) args = dict(url_decode(request.url.query)) args.update((await request.form()) or {}) #: dict of query and body params return_url = urllib.parse.quote_plus(config.oauth2.base_url + '/authorize?' + url_encode(args.items())) return JSONResponse( content={ 'redirect_uri': f"{config.manager.frontend_base_url}/register/{registration_token}" f"?return_url={return_url}" }, status_code=200, headers=dict(default_json_headers), ) user_group_data = await UserWithRoles.async_load_groups( user, _query_params.client_id) oauth_request = await oauth2_request(request) resp = await run_in_threadpool( authorization.create_authorization_response, request=oauth_request, grant_user=user_group_data, ) if isinstance(resp, ErrorJSONResponse): return resp elif isinstance(resp, ErrorRedirectResponse): return resp.to_json_response() elif isinstance(resp, RedirectResponse): expires_in = config.oauth2.token_expiration.session add_session_state(resp, oauth_request.user) resp = resp.to_json_response() if auth_credentials.remember: now = int(time.time()) sess = Session( id=generate_token(config.oauth2.token_length), user_id=user.id, issued_at=now, expires_in=expires_in, expiration_time=datetime.utcnow() + timedelta(seconds=expires_in), ) await async_session_collection.insert_one( sess.dict(exclude_none=True, by_alias=True)) update_session_sid(resp, sess.id) update_session_state(resp, oauth_request.user) return resp assert False