From: Adam Dullage Date: Sat, 29 Jul 2023 07:30:19 +0000 (+0100) Subject: Implement read-only mode X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=e6a6e2e1e5c64e916101bd6f1f4ee6f2b51e862e;p=flatnotes.git Implement read-only mode --- diff --git a/README.md b/README.md index 4054b7e..e74f7d0 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Equally, the only thing flatnotes caches is the search index and that's incremen * Advanced search functionality. * Note "tagging" functionality. * Light/dark themes. -* Multiple authentication options (none, username/password, 2FA). +* Multiple authentication options (none, read only, username/password, 2FA). * Restful API. See [the wiki](https://github.com/dullage/flatnotes/wiki) for more details. diff --git a/flatnotes/auth.py b/flatnotes/auth.py index 7ea6871..e3d7b05 100644 --- a/flatnotes/auth.py +++ b/flatnotes/auth.py @@ -44,16 +44,3 @@ def validate_token(token: str = Depends(oauth2_scheme)): def no_auth(): return - - -def get_auth(for_edit: bool = True): - if config.auth_type == AuthType.NONE: - return no_auth - elif ( - config.auth_type - in [AuthType.PASSWORD_EDIT_ONLY, AuthType.TOTP_EDIT_ONLY] - and for_edit is False - ): - return no_auth - else: - return validate_token diff --git a/flatnotes/config.py b/flatnotes/config.py index 482fb40..80b03fb 100644 --- a/flatnotes/config.py +++ b/flatnotes/config.py @@ -8,10 +8,9 @@ from logger import logger class AuthType(str, Enum): NONE = "none" + READ_ONLY = "read_only" PASSWORD = "password" - PASSWORD_EDIT_ONLY = "password_edit_only" TOTP = "totp" - TOTP_EDIT_ONLY = "totp_edit_only" class Config: diff --git a/flatnotes/main.py b/flatnotes/main.py index 79e10f2..f186f60 100644 --- a/flatnotes/main.py +++ b/flatnotes/main.py @@ -7,7 +7,7 @@ from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from qrcode import QRCode -from auth import create_access_token, get_auth, validate_token +from auth import create_access_token, no_auth, validate_token from config import AuthType, config from error_responses import ( invalid_title_response, @@ -31,6 +31,11 @@ totp = ( ) last_used_totp = None +if config.auth_type in [AuthType.NONE, AuthType.READ_ONLY]: + authenticate = no_auth +else: + authenticate = validate_token + # Display TOTP QR code if config.auth_type == AuthType.TOTP: uri = totp.provisioning_uri(issuer_name="flatnotes", name=config.username) @@ -43,37 +48,41 @@ if config.auth_type == AuthType.TOTP: qr.print_ascii() print(f"Or manually enter this key: {totp.secret.decode('utf-8')}\n") +if config.auth_type not in [AuthType.NONE, AuthType.READ_ONLY]: -@app.post("/api/token") -def token(data: LoginModel): - global last_used_totp - - username_correct = secrets.compare_digest( - config.username.lower(), data.username.lower() - ) + @app.post("/api/token") + def token(data: LoginModel): + global last_used_totp - expected_password = config.password - if config.auth_type == AuthType.TOTP: - current_totp = totp.now() - expected_password += current_totp - password_correct = secrets.compare_digest(expected_password, data.password) - - if not ( - username_correct - and password_correct - # Prevent TOTP from being reused - and ( - config.auth_type != AuthType.TOTP or current_totp != last_used_totp + username_correct = secrets.compare_digest( + config.username.lower(), data.username.lower() ) - ): - raise HTTPException( - status_code=400, detail="Incorrect login credentials." + + expected_password = config.password + if config.auth_type == AuthType.TOTP: + current_totp = totp.now() + expected_password += current_totp + password_correct = secrets.compare_digest( + expected_password, data.password ) - access_token = create_access_token(data={"sub": config.username}) - if config.auth_type == AuthType.TOTP: - last_used_totp = current_totp - return {"access_token": access_token, "token_type": "bearer"} + if not ( + username_correct + and password_correct + # Prevent TOTP from being reused + and ( + config.auth_type != AuthType.TOTP + or current_totp != last_used_totp + ) + ): + raise HTTPException( + status_code=400, detail="Incorrect login credentials." + ) + + access_token = create_access_token(data={"sub": config.username}) + if config.auth_type == AuthType.TOTP: + last_used_totp = current_totp + return {"access_token": access_token, "token_type": "bearer"} @app.get("/") @@ -87,26 +96,28 @@ def root(title: str = ""): return HTMLResponse(content=html) -@app.post( - "/api/notes", - dependencies=[Depends(get_auth(for_edit=True))], - response_model=NoteModel, -) -def post_note(data: NoteModel): - """Create a new note.""" - try: - note = Note(flatnotes, data.title, new=True) - note.content = data.content - return NoteModel.dump(note, include_content=True) - except InvalidTitleError: - return invalid_title_response - except FileExistsError: - return title_exists_response +if config.auth_type != AuthType.READ_ONLY: + + @app.post( + "/api/notes", + dependencies=[Depends(authenticate)], + response_model=NoteModel, + ) + def post_note(data: NoteModel): + """Create a new note.""" + try: + note = Note(flatnotes, data.title, new=True) + note.content = data.content + return NoteModel.dump(note, include_content=True) + except InvalidTitleError: + return invalid_title_response + except FileExistsError: + return title_exists_response @app.get( "/api/notes/{title}", - dependencies=[Depends(get_auth(for_edit=False))], + dependencies=[Depends(authenticate)], response_model=NoteModel, ) def get_note( @@ -123,43 +134,45 @@ def get_note( return note_not_found_response -@app.patch( - "/api/notes/{title}", - dependencies=[Depends(get_auth(for_edit=True))], - response_model=NoteModel, -) -def patch_note(title: str, new_data: NotePatchModel): - try: - note = Note(flatnotes, title) - if new_data.new_title is not None: - note.title = new_data.new_title - if new_data.new_content is not None: - note.content = new_data.new_content - return NoteModel.dump(note, include_content=True) - except InvalidTitleError: - return invalid_title_response - except FileExistsError: - return title_exists_response - except FileNotFoundError: - return note_not_found_response - +if config.auth_type != AuthType.READ_ONLY: -@app.delete( - "/api/notes/{title}", dependencies=[Depends(get_auth(for_edit=True))] -) -def delete_note(title: str): - try: - note = Note(flatnotes, title) - note.delete() - except InvalidTitleError: - return invalid_title_response - except FileNotFoundError: - return note_not_found_response + @app.patch( + "/api/notes/{title}", + dependencies=[Depends(authenticate)], + response_model=NoteModel, + ) + def patch_note(title: str, new_data: NotePatchModel): + try: + note = Note(flatnotes, title) + if new_data.new_title is not None: + note.title = new_data.new_title + if new_data.new_content is not None: + note.content = new_data.new_content + return NoteModel.dump(note, include_content=True) + except InvalidTitleError: + return invalid_title_response + except FileExistsError: + return title_exists_response + except FileNotFoundError: + return note_not_found_response + + +if config.auth_type != AuthType.READ_ONLY: + + @app.delete("/api/notes/{title}", dependencies=[Depends(authenticate)]) + def delete_note(title: str): + try: + note = Note(flatnotes, title) + note.delete() + except InvalidTitleError: + return invalid_title_response + except FileNotFoundError: + return note_not_found_response @app.get( "/api/tags", - dependencies=[Depends(get_auth(for_edit=False))], + dependencies=[Depends(authenticate)], ) def get_tags(): """Get a list of all indexed tags.""" @@ -168,7 +181,7 @@ def get_tags(): @app.get( "/api/search", - dependencies=[Depends(get_auth(for_edit=False))], + dependencies=[Depends(authenticate)], response_model=List[SearchResultModel], ) def search( diff --git a/flatnotes/src/components/App.vue b/flatnotes/src/components/App.vue index 33729e4..9397c8a 100644 --- a/flatnotes/src/components/App.vue +++ b/flatnotes/src/components/App.vue @@ -12,7 +12,7 @@ v-if="currentView != views.login" class="w-100 mb-5" :show-logo="currentView != views.home" - :show-log-out="authType != null && authType != constants.authTypes.none" + :auth-type="authType" :dark-theme="darkTheme" @logout="logout()" @toggleTheme="toggleTheme()" @@ -29,16 +29,7 @@
diff --git a/flatnotes/src/components/NavBar.vue b/flatnotes/src/components/NavBar.vue index 467d973..c0b82ef 100644 --- a/flatnotes/src/components/NavBar.vue +++ b/flatnotes/src/components/NavBar.vue @@ -12,7 +12,7 @@