From 2a58f0b5abf1c5a56bd549c295754a31d0aec0e8 Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Fri, 23 Aug 2019 22:14:12 +0200
Subject: [PATCH] Secure graphql endpoint

add option to scan individual users photos
---
 README.md                               |  2 --
 api/src/index.js                        | 26 ++++++++++++++++++++
 api/src/resolvers/scanner.js            | 32 +++++++++++++++++++++++++
 api/src/scanner/Scanner.js              |  2 +-
 api/src/schema.graphql                  |  2 ++
 ui/src/Pages/SettingsPage/UserRow.js    | 19 +++++++++++++++
 ui/src/Pages/SettingsPage/UsersTable.js |  6 ++---
 7 files changed, 83 insertions(+), 6 deletions(-)

diff --git a/README.md b/README.md
index 3df02c2..951b47a 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,5 @@
 # PhotoView
 
-> NOTE: This software should not be used in production yet, since it is in early development, and still contains security holes.
-
 [Follow the development progress here](https://www.notion.so/e8a95059eaa74fd7b8b69f8c949c9d08?v=c999e954941b43f4bf5fcec8451a789a)
 
 ![screenshot](/screenshot.png)
diff --git a/api/src/index.js b/api/src/index.js
index e262765..49340ff 100644
--- a/api/src/index.js
+++ b/api/src/index.js
@@ -7,6 +7,7 @@ import http from 'http'
 import PhotoScanner from './scanner/Scanner'
 import _ from 'lodash'
 import config from './config'
+import gql from 'graphql-tag'
 
 import { getUserFromToken, getTokenFromBearer } from './token'
 
@@ -32,6 +33,7 @@ const scanner = new PhotoScanner(driver)
 app.use((req, res, next) => {
   req.driver = driver
   req.scanner = scanner
+
   next()
 })
 
@@ -41,6 +43,30 @@ setInterval(scanner.scanAll, 1000 * 60 * 60 * 4)
 // Specify port and path for GraphQL endpoint
 const graphPath = '/graphql'
 
+app.use(graphPath, (req, res, next) => {
+  if (req.body.query) {
+    const query = gql(req.body.query)
+    const defs = query.definitions.filter(x => x.kind == 'OperationDefinition')
+
+    const selections = defs.reduce((prev, curr) => {
+      return prev.concat(curr.selectionSet.selections)
+    }, [])
+
+    const names = selections.map(x => x.name.value)
+    const illegalNames = names.filter(
+      name => name.substr(0, 1) == name.substr(0, 1).match(/[A-Z]/)
+    )
+
+    if (illegalNames.length > 0) {
+      return res
+        .status(403)
+        .send({ error: `Illegal query, types not allowed: ${illegalNames}` })
+    }
+  }
+
+  next()
+})
+
 const endpointUrl = new URL(config.host)
 // endpointUrl.port = process.env.GRAPHQL_LISTEN_PORT || 4001
 
diff --git a/api/src/resolvers/scanner.js b/api/src/resolvers/scanner.js
index 9b19d64..f831feb 100644
--- a/api/src/resolvers/scanner.js
+++ b/api/src/resolvers/scanner.js
@@ -4,6 +4,38 @@ const Mutation = {
   async scanAll(root, args, ctx, info) {
     ctx.scanner.scanAll()
 
+    return {
+      finished: false,
+      success: true,
+      progress: 0,
+      message: 'Starting scanner',
+    }
+  },
+  async scanUser(root, args, ctx, info) {
+    const session = ctx.driver.session()
+
+    const userResult = await session.run(
+      `MATCH (u:User { id: {userId} }) RETURN u`,
+      {
+        userId: args.userId,
+      }
+    )
+
+    session.close()
+
+    if (userResult.records.length == 0) {
+      return {
+        finished: false,
+        success: false,
+        progress: 0,
+        message: 'Could not scan user: User not found',
+      }
+    }
+
+    const user = userResult.records[0].get('u').properties
+
+    ctx.scanner.scanUser(user)
+
     return {
       finished: false,
       success: true,
diff --git a/api/src/scanner/Scanner.js b/api/src/scanner/Scanner.js
index 0913336..7ffa5e2 100644
--- a/api/src/scanner/Scanner.js
+++ b/api/src/scanner/Scanner.js
@@ -120,7 +120,7 @@ class PhotoScanner {
 
   async scanUser(user) {
     await _execScan(this, async () => {
-      await _scanUser({ driver: this.driver, scanAlbum: this.scanAlbum }, user)
+      await _scanUser(this, user)
     })
   }
 
diff --git a/api/src/schema.graphql b/api/src/schema.graphql
index aec2783..14a6612 100644
--- a/api/src/schema.graphql
+++ b/api/src/schema.graphql
@@ -130,6 +130,7 @@ type Mutation {
     @neo4j_ignore
 
   scanAll: ScannerResult! @isAuthenticated @neo4j_ignore
+  scanUser(userId: ID!): ScannerResult! @isAuthenticated @neo4j_ignore
 
   initialSetupWizard(
     username: String!
@@ -151,6 +152,7 @@ type Query {
   siteInfo: SiteInfo
 
   myUser: User @isAuthenticated
+  user: [User] @hasRole(roles: [admin])
 
   myAlbums: [Album] @isAuthenticated
   album(id: ID): Album @isAuthenticated
diff --git a/ui/src/Pages/SettingsPage/UserRow.js b/ui/src/Pages/SettingsPage/UserRow.js
index 3261117..9e6a0b9 100644
--- a/ui/src/Pages/SettingsPage/UserRow.js
+++ b/ui/src/Pages/SettingsPage/UserRow.js
@@ -51,6 +51,14 @@ const changeUserPasswordMutation = gql`
   }
 `
 
+const scanUserMutation = gql`
+  mutation scanUser($userId: ID!) {
+    scanUser(userId: $userId) {
+      success
+    }
+  }
+`
+
 const ChangePasswordModal = ({ onClose, user, ...props }) => {
   const [passwordInput, setPasswordInput] = useState('')
 
@@ -223,6 +231,17 @@ const UserRow = ({ user, refetchUsers }) => {
                 <Icon name="edit" />
                 Edit
               </Button>
+              <Mutation mutation={scanUserMutation}>
+                {(scanUser, { data, called }) => (
+                  <Button
+                    disabled={called}
+                    onClick={() => scanUser({ variables: { userId: user.id } })}
+                  >
+                    <Icon name="sync" />
+                    Scan
+                  </Button>
+                )}
+              </Mutation>
               <Button onClick={() => setChangePassword(true)}>
                 <Icon name="key" />
                 Change password
diff --git a/ui/src/Pages/SettingsPage/UsersTable.js b/ui/src/Pages/SettingsPage/UsersTable.js
index d313d12..f509f66 100644
--- a/ui/src/Pages/SettingsPage/UsersTable.js
+++ b/ui/src/Pages/SettingsPage/UsersTable.js
@@ -8,7 +8,7 @@ import AddUserRow from './AddUserRow'
 
 const usersQuery = gql`
   query settingsUsersQuery {
-    User {
+    user {
       id
       username
       rootPath
@@ -24,8 +24,8 @@ const UsersTable = () => {
     <Query query={usersQuery}>
       {({ loading, error, data, refetch }) => {
         let userRows = []
-        if (data && data.User) {
-          userRows = data.User.map(user => (
+        if (data && data.user) {
+          userRows = data.user.map(user => (
             <UserRow user={user} refetchUsers={refetch} key={user.id} />
           ))
         }
