6
0
Эх сурвалжийг харах

Added authentication / login

Users can login and the provided credentials are used for the backend API.
Missing: Custom image for logout button in sidebar, more pleasant login form. Also, a flask secret should be specified.
blunk 3 жил өмнө
parent
commit
5ee79d8ef5

+ 2 - 1
app.py

@@ -1,9 +1,10 @@
 #!/usr/bin/env python
 
 from pycs import app
+from pycs import htpasswd
 from pycs import settings
 from pycs.frontend.WebServer import WebServer
 
 if __name__ == '__main__':
-    server = WebServer(app, settings)
+    server = WebServer(app, htpasswd, settings)
     server.run()

+ 9 - 0
pycs/__init__.py

@@ -15,6 +15,8 @@ from sqlalchemy import event
 from sqlalchemy import pool
 from sqlalchemy.engine import Engine
 
+from flask_htpasswd import HtPasswdAuth
+
 print('=== Loading settings ===')
 with open('settings.json') as file:
     settings = munchify(json.load(file))
@@ -29,6 +31,13 @@ app = Flask(__name__)
 app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{DB_FILE}"
 app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
 
+# Protect via http basic authentication
+app.config['FLASK_HTPASSWD_PATH'] = '.htpasswd'
+if not os.path.isfile(app.config['FLASK_HTPASSWD_PATH']):
+    raise FileNotFoundError("You need to specify a .htpasswd-file. The following file could not be located: " + app.config['FLASK_HTPASSWD_PATH'] + "!")
+app.config['FLASK_SECRET'] = 'Hey Hey Kids, secure me!'
+htpasswd = HtPasswdAuth(app)
+
 # pylint: disable=unused-argument
 @event.listens_for(Engine, "connect")
 def set_sqlite_pragma(dbapi_connection, connection_record):

+ 52 - 48
pycs/frontend/WebServer.py

@@ -64,10 +64,11 @@ class WebServer:
 
     index: Path = Path.cwd() / 'webui' / 'index.html'
 
-    def __init__(self, app, settings: munch.Munch, discovery: bool = True):
+    def __init__(self, app, htpasswd, settings: munch.Munch, discovery: bool = True):
 
         logging.config.dictConfig(settings.logging)
         self.app = app
+        self.htpasswd = htpasswd
         # set json encoder so database objects are serialized correctly
         self.app.json_encoder = JSONEncoder
 
@@ -88,7 +89,10 @@ class WebServer:
             @self.app.after_request
             def after_request(response):
                 # pylint: disable=unused-variable
-                response.headers['Access-Control-Allow-Origin'] = '*'
+                response.headers['Access-Control-Allow-Origin'] = 'http://localhost:8080'
+                response.headers['Access-Control-Allow-Credentials'] = 'true'
+                response.headers['Access-Control-Allow-Methods'] = 'POST, GET'
+                response.headers['Access-Control-Allow-Headers'] = 'Authorization, Content-Type'
                 return response
 
         # create service objects
@@ -161,189 +165,189 @@ class WebServer:
         # additional
         self.app.add_url_rule(
             '/folder',
-            view_func=FolderInformation.as_view('folder_information')
+            view_func=self.htpasswd.required( FolderInformation.as_view('folder_information') )
         )
 
         # jobs
         self.app.add_url_rule(
             '/jobs',
-            view_func=ListJobs.as_view('list_jobs', self.jobs)
+            view_func=self.htpasswd.required( ListJobs.as_view('list_jobs', self.jobs) )
         )
         self.app.add_url_rule(
             '/jobs/<job_id>/remove',
-            view_func=RemoveJob.as_view('remove_job', self.jobs)
+            view_func=self.htpasswd.required( RemoveJob.as_view('remove_job', self.jobs) )
         )
 
         # models
         self.app.add_url_rule(
             '/models',
-            view_func=ListModels.as_view('list_models')
+            view_func=self.htpasswd.required( ListModels.as_view('list_models') )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/model',
-            view_func=GetProjectModel.as_view('get_project_model')
+            view_func=self.htpasswd.required( GetProjectModel.as_view('get_project_model') )
         )
 
         # labels
         self.app.add_url_rule(
             '/label_providers',
-            view_func=ListLabelProviders.as_view('label_providers')
+            view_func=self.htpasswd.required( ListLabelProviders.as_view('label_providers') )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/labels',
-            view_func=ListLabels.as_view('list_labels')
+            view_func=self.htpasswd.required( ListLabels.as_view('list_labels') )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/labels/tree',
-            view_func=ListLabelTree.as_view('list_label_tree')
+            view_func=self.htpasswd.required( ListLabelTree.as_view('list_label_tree') )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/labels',
-            view_func=CreateLabel.as_view('create_label', self.notifications)
+            view_func=self.htpasswd.required( CreateLabel.as_view('create_label', self.notifications) )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/labels/<int:label_id>/remove',
-            view_func=RemoveLabel.as_view('remove_label', self.notifications)
+            view_func=self.htpasswd.required( RemoveLabel.as_view('remove_label', self.notifications) )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/labels/<int:label_id>/name',
-            view_func=EditLabelName.as_view('edit_label_name', self.notifications)
+            view_func=self.htpasswd.required( EditLabelName.as_view('edit_label_name', self.notifications) )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/labels/<int:label_id>/parent',
-            view_func=EditLabelParent.as_view('edit_label_parent', self.notifications)
+            view_func=self.htpasswd.required( EditLabelParent.as_view('edit_label_parent', self.notifications) )
         )
 
         # collections
         self.app.add_url_rule(
             '/projects/<int:project_id>/collections',
-            view_func=ListProjectCollections.as_view('list_collections')
+            view_func=self.htpasswd.required( ListProjectCollections.as_view('list_collections') )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/data/<int:collection_id>/<int:start>/<int:length>',
-            view_func=ListProjectFiles.as_view('list_collection_files')
+            view_func=self.htpasswd.required( ListProjectFiles.as_view('list_collection_files') )
         )
 
         # data
         self.app.add_url_rule(
             '/projects/<int:project_id>/data',
-            view_func=UploadFile.as_view('upload_file', self.notifications)
+            view_func=self.htpasswd.required( UploadFile.as_view('upload_file', self.notifications) )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/data',
-            view_func=ListProjectFiles.as_view('list_all_files')
+            view_func=self.htpasswd.required( ListProjectFiles.as_view('list_all_files') )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/data/<int:start>/<int:length>',
-            view_func=ListProjectFiles.as_view('list_files')
+            view_func=self.htpasswd.required( ListProjectFiles.as_view('list_files') )
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/remove',
-            view_func=RemoveFile.as_view('remove_file', self.notifications)
+            view_func=self.htpasswd.required( RemoveFile.as_view('remove_file', self.notifications) )
         )
         self.app.add_url_rule(
             '/data/<int:file_id>',
-            view_func=GetFile.as_view('get_file')
+            view_func=self.htpasswd.required( GetFile.as_view('get_file') )
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/<resolution>',
-            view_func=GetResizedFile.as_view('get_resized_file')
+            view_func=self.htpasswd.required( GetResizedFile.as_view('get_resized_file') )
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/<resolution>/<crop_box>',
-            view_func=GetCroppedFile.as_view('get_cropped_file')
+            view_func=self.htpasswd.required( GetCroppedFile.as_view('get_cropped_file') )
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/previous_next',
-            view_func=GetPreviousAndNextFile.as_view('get_previous_and_next_file')
+            view_func=self.htpasswd.required( GetPreviousAndNextFile.as_view('get_previous_and_next_file') )
         )
 
         # results
         self.app.add_url_rule(
             '/projects/<int:project_id>/results',
-            view_func=GetProjectResults.as_view('get_project_results')
+            view_func=self.htpasswd.required( GetProjectResults.as_view('get_project_results') )
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/results',
-            view_func=GetResults.as_view('get_results')
+            view_func=self.htpasswd.required( GetResults.as_view('get_results') )
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/results',
-            view_func=CreateResult.as_view('create_result', self.notifications)
+            view_func=self.htpasswd.required( CreateResult.as_view('create_result', self.notifications) )
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/reset',
-            view_func=ResetResults.as_view('reset_results', self.notifications)
+            view_func=self.htpasswd.required( ResetResults.as_view('reset_results', self.notifications) )
         )
         self.app.add_url_rule(
             '/results/<int:result_id>/remove',
-            view_func=RemoveResult.as_view('remove_result', self.notifications)
+            view_func=self.htpasswd.required( RemoveResult.as_view('remove_result', self.notifications) )
         )
         self.app.add_url_rule(
             '/results/<int:result_id>/confirm',
-            view_func=ConfirmResult.as_view('confirm_result', self.notifications)
+            view_func=self.htpasswd.required( ConfirmResult.as_view('confirm_result', self.notifications) )
         )
 
         self.app.add_url_rule(
             '/results/<int:result_id>/label',
-            view_func=EditResultLabel.as_view('edit_result_label', self.notifications)
+            view_func=self.htpasswd.required( EditResultLabel.as_view('edit_result_label', self.notifications) )
         )
         self.app.add_url_rule(
             '/results/<int:result_id>/data',
-            view_func=EditResultData.as_view('edit_result_data', self.notifications)
+            view_func=self.htpasswd.required( EditResultData.as_view('edit_result_data', self.notifications) )
         )
 
         # projects
         self.app.add_url_rule(
             '/projects',
-            view_func=ListProjects.as_view('list_projects')
+            view_func=self.htpasswd.required( ListProjects.as_view('list_projects') )
         )
         self.app.add_url_rule(
             '/projects',
-            view_func=CreateProject.as_view('create_project', self.notifications, self.jobs)
+            view_func=self.htpasswd.required( CreateProject.as_view('create_project', self.notifications, self.jobs) )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/label_provider',
-            view_func=ExecuteLabelProvider.as_view('execute_label_provider',
-                                                   self.notifications, self.jobs)
+            view_func=self.htpasswd.required( ExecuteLabelProvider.as_view('execute_label_provider',
+                                                   self.notifications, self.jobs) )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/external_storage',
-            view_func=ExecuteExternalStorage.as_view('execute_external_storage',
-                                                     self.notifications, self.jobs)
+            view_func=self.htpasswd.required( ExecuteExternalStorage.as_view('execute_external_storage',
+                                                     self.notifications, self.jobs) )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/remove',
-            view_func=RemoveProject.as_view('remove_project', self.notifications)
+            view_func=self.htpasswd.required( RemoveProject.as_view('remove_project', self.notifications) )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/name',
-            view_func=EditProjectName.as_view('edit_project_name', self.notifications)
+            view_func=self.htpasswd.required( EditProjectName.as_view('edit_project_name', self.notifications) )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/description',
-            view_func=EditProjectDescription.as_view('edit_project_description', self.notifications)
+            view_func=self.htpasswd.required( EditProjectDescription.as_view('edit_project_description', self.notifications) )
         )
 
         # pipelines
         self.app.add_url_rule(
             '/projects/<int:project_id>/pipelines/fit',
-            view_func=FitModel.as_view('fit_model', self.jobs, self.pipelines)
+            view_func=self.htpasswd.required( FitModel.as_view('fit_model', self.jobs, self.pipelines) )
         )
         self.app.add_url_rule(
             '/projects/<int:project_id>/pipelines/predict',
-            view_func=PredictModel.as_view('predict_model', self.notifications, self.jobs,
-                                           self.pipelines)
+            view_func=self.htpasswd.required( PredictModel.as_view('predict_model', self.notifications, self.jobs,
+                                           self.pipelines) )
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/predict',
-            view_func=PredictFile.as_view('predict_file', self.notifications,
-                                          self.jobs, self.pipelines)
+            view_func=self.htpasswd.required( PredictFile.as_view('predict_file', self.notifications,
+                                          self.jobs, self.pipelines) )
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/<int:bbox_id>/predict_bounding_box',
-            view_func=PredictBoundingBox.as_view('predict_bounding_box', self.notifications,
-                                          self.jobs, self.pipelines)
+            view_func=self.htpasswd.required( PredictBoundingBox.as_view('predict_bounding_box', self.notifications,
+                                          self.jobs, self.pipelines) )
         )
 
     def run(self):

+ 48 - 2
webui/src/App.vue

@@ -8,8 +8,19 @@
                         v-on:side="window.menu = !window.menu"
                         @close="closeProject"></top-navigation-bar>
 
+    <!-- login -->
+    <div class="login" v-if="!$root.socket.authenticated">
+      <div class="login_form">
+        <h1>Login</h1>
+        <p><input v-model="userName" type="username" placeholder="Username" required></p>
+        <p><input v-model="passwordLogin" type="password" placeholder="Password" required></p>
+        <p><button type="button" v-on:click="login()" :disabled="loginButtonDisabled">Login</button></p>
+        <p><span style="color:red">{{loginErrorText}}</span></p>
+      </div>
+    </div>
+
     <!-- bottom content -->
-    <div class="bottom">
+    <div class="bottom" v-else>
       <!-- side navigation bar -->
       <side-navigation-bar :window="window"
                            v-on:close="window.menu = false"/>
@@ -64,7 +75,11 @@ export default {
         wide: true,
         menu: false,
         content: 'settings'
-      }
+      },
+      loginErrorText: "",
+      loginButtonDisabled: false,
+      userName: "",
+      passwordLogin: "",
     }
   },
   created: function () {
@@ -86,6 +101,21 @@ export default {
     }
   },
   methods: {
+    login() {
+      this.loginButtonDisabled = true;
+      if (this.userName != "" && this.passwordLogin != "") {
+        this.$root.socket.username = this.userName;
+        this.$root.socket.password = this.passwordLogin;
+        if (this.$root.socket.authenticate()) {
+          this.loginErrorText = "";
+        } else {
+          this.loginErrorText = "Invalid username or password!";
+        }
+      } else {
+        this.loginErrorText = "Please enter a valid username and password!";
+      }
+      this.loginButtonDisabled = false;
+    },
     resize: function () {
       this.window.wide = (document.body.offsetWidth > 1024);
     },
@@ -127,6 +157,22 @@ export default {
   position: relative;
 }
 
+.login {
+  align-items: center;
+  justify-content: center;
+  display: flex;
+  flex-wrap: wrap;
+  flex-grow: 1;
+  flex-direction: row;
+  width: 100%;
+  overflow: hidden;
+  position: relative;
+}
+
+.login_form{
+  text-align: center;;
+}
+
 .content {
   flex-grow: 1;
 }

+ 6 - 0
webui/src/components/window/side-navigation-bar.vue

@@ -35,6 +35,12 @@
         <span>Session</span>
       </div>
 
+      <div class="item"
+           @click="$root.socket.logout()">
+        <img alt="Logout" src="@/assets/icons/file-media.svg">
+        <span>Logout</span>
+      </div>
+
       <div class="item"
            :class="{active: window.content === 'about'}"
            @click="show('about')">