Repository: illinois/queue
Pull request #230 (2019-03-11 00:24:01)

Confidential queues!
This PR contains the work from #199 and #224 that implements the frontend and backend for confidential queues. #224 tested the combined work pretty extensively; this PR is just a final chance to review everything in one place before we ship it.

now[bot]: 
This pull request is [automatically deployed](https://zeit.co/docs/features/now-for-github?utm_source=automated&utm_medium=github&utm_campaign=now_bot) with [Now](https://zeit.co/now?utm_source=automated&utm_medium=github&utm_campaign=now_bot).
To access deployments, click Details below or on the icon next to each push.


	


Patch:
diff --git a/doc/Auth.md b/doc/Auth.md
index 92724b2..8e8fa96 100644
--- a/doc/Auth.md
+++ b/doc/Auth.md
@@ -3,9 +3,9 @@
 The Queue uses Shibboleth to authenticate users. However, we don't do Shib
 authentication on every request, for a couple of reasons:
 
-* Websockets don't end up with Shibboleth authentication/identity headers,
+- Websockets don't end up with Shibboleth authentication/identity headers,
   which we'll need for private queues
-* Shibboleth is configured with policies that enforce reauthentication
+- Shibboleth is configured with policies that enforce reauthentication
   whenever the user changes networks, and also every 24 hours. With 2FA
   on the way, this will become increasingly annoying
 
@@ -14,3 +14,13 @@ identity. We then establish our own "session" for them using a JSON Web
 Token. When that token expires, we re-authenticate using Shibboleth. This
 also allows us to use other forms of authentication, such as OAuth. In
 all cases, we simply issue a JWT that identifies the user to us.
+
+# Authorization
+
+Every API request goes through the authorization middleware defined in
+`src/middleware/authz.js`. That middleware attaches authorization information
+to `res.locals.userAuthz`:
+
+- `res.locals.userAuthz.isAdmin`: If the current user is a global admin
+- `res.locals.userAuthz.staffedCourseIds`: A list of all course IDs that the current
+  user is on staff for
diff --git a/src/api/questions.js b/src/api/questions.js
index b0c6af7..8230a5e 100644
--- a/src/api/questions.js
+++ b/src/api/questions.js
@@ -12,6 +12,8 @@ const {
   requireQueueForQuestion,
   requireQuestion,
   failIfErrors,
+  canUserSeeQuestionDetailsForConfidentialQueue,
+  filterConfidentialQueueQuestionsForUser,
 } = require('./util')
 const requireCourseStaffForQueue = require('../middleware/requireCourseStaffForQueue')
 const requireCourseStaffForQueueForQuestion = require('../middleware/requireCourseStaffForQueueForQuestion')
@@ -117,22 +119,46 @@ router.get(
   '/',
   [requireQueue, failIfErrors],
   safeAsync(async (req, res, _next) => {
-    const { id: queueId } = res.locals.queue
-    const questions = await Question.findAll({
+    const { id: queueId, courseId, isConfidential } = res.locals.queue
+    const questionsResult = await Question.findAll({
       where: {
         queueId,
         dequeueTime: null,
       },
       order: [['id', 'ASC']],
     })
+
+    // Convert questions to array of plain objects that we can filter and manipulate
+    let questions = questionsResult.map(question =>
+      question.get({ plain: true })
+    )
+    if (isConfidential) {
+      const { userAuthz } = res.locals
+      if (!canUserSeeQuestionDetailsForConfidentialQueue(userAuthz, courseId)) {
+        const { id: userId } = res.locals.userAuthn
+        questions = filterConfidentialQueueQuestionsForUser(userId, questions)
+      }
+    }
+
     res.send(questions)
   })
 )
 
 router.get(
   '/:questionId',
-  [requireQuestion, failIfErrors],
+  [requireQuestion, requireQueueForQuestion, failIfErrors],
   (req, res, _next) => {
+    const { courseId, isConfidential } = res.locals.queue
+    if (isConfidential) {
+      const { id: userId } = res.locals.userAuthn
+      const { userAuthz } = res.locals
+      if (!canUserSeeQuestionDetailsForConfidentialQueue(userAuthz, courseId)) {
+        if (res.locals.question.askedById !== userId) {
+          res.status(403).send('You are not authorized to access that question')
+          return
+        }
+      }
+    }
     res.send(res.locals.question)
   }
 )
diff --git a/src/api/queues.js b/src/api/queues.js
index 60bcc09..06b49d9 100644
--- a/src/api/queues.js
+++ b/src/api/queues.js
@@ -13,6 +13,8 @@ const {
   requireQueue,
   requireUser,
   failIfErrors,
+  canUserSeeQuestionDetailsForConfidentialQueue,
+  filterConfidentialQueueQuestionsForUser,
 } = require('./util')
 
 const requireCourseStaffForQueue = require('../middleware/requireCourseStaffForQueue')
@@ -63,6 +65,7 @@ router.post(
       name: data.name,
       location: data.location,
       fixedLocation: data.fixedLocation === true,
+      isConfidential: req.body.isConfidential === true,
       courseId,
       createdByUserId: res.locals.userAuthn.id,
     })
@@ -80,7 +83,7 @@ router.get(
   [requireQueue, failIfErrors],
   safeAsync(async (req, res, _next) => {
     const { id: queueId } = res.locals.queue
-    const queue = await Queue.findOne({
+    const queueResults = await Queue.findOne({
       where: {
         id: queueId,
       },
@@ -105,6 +108,29 @@ router.get(
       order: [[Question, 'id', 'ASC']],
     })
 
+    // Convert to plain object that we can manipulate/filter/etc. before
+    // sending back to the client
+    const queue = queueResults.get({ plain: true })
+
+    // If this is a confidential queue, don't send any actual question data
+    // back to the client, besides IDs
+    if (queue.isConfidential) {
+      const { userAuthz } = res.locals
+      if (
+        !canUserSeeQuestionDetailsForConfidentialQueue(
+          userAuthz,
+          queue.courseId
+        )
+      ) {
+        const { id: userId } = res.locals.userAuthn
+        const filtered = filterConfidentialQueueQuestionsForUser(
+          userId,
+          queue.questions
+        )
+        queue.questions = filtered
+      }
+    }
+
     res.json(queue)
   })
 )
diff --git a/src/api/util.js b/src/api/util.js
index e35eaec..60d0ef9 100644
--- a/src/api/util.js
+++ b/src/api/util.js
@@ -58,6 +58,28 @@ const requireModelForModel = (
   }
 }
 
+const canUserSeeQuestionDetailsForConfidentialQueue = (userAuthz, courseId) => {
+  const { isAdmin, staffedCourseIds } = userAuthz
+  const staffsQueue = staffedCourseIds.findIndex(id => id === courseId) !== -1
+  return isAdmin || staffsQueue
+}
+
+/**
+ * Removes any sensitive user information from all questions not asked by the
+ * user making this request.
+ *
+ * @param {Response} res The response containing user info on res.locals
+ * @param {Questions[]} questions The list of questions to remove sensitive info from
+ */
+const filterConfidentialQueueQuestionsForUser = (userId, questions) => {
+  return questions.map(question => {
+    if (question.askedById === userId) {
+      return question
+    }
+    return { id: question.id }
+  })
+}
+
 module.exports = {
   failIfErrors(req, res, next) {
     const errors = validationResult(req)
@@ -83,4 +105,8 @@ module.exports = {
   // These have to be exported for testing
   findPropertyInRequest,
   requireModel,
+
+  // Stuff for confidential queues
+  canUserSeeQuestionDetailsForConfidentialQueue,
+  filterConfidentialQueueQuestionsForUser,
 }
diff --git a/src/auth/util.js b/src/auth/util.js
index 70c52a6..8cf4dc8 100644
--- a/src/auth/util.js
+++ b/src/auth/util.js
@@ -1,6 +1,7 @@
 const jwt = require('jsonwebtoken')
 const url = require('url')
 
+const { Course } = require('../models')
 const { User } = require('../models')
 const { isDev } = require('../util')
 
@@ -62,3 +63,26 @@ module.exports.isSafeUrl = (req, redirect) => {
   const redirectUrl = new url.URL(redirect, originUrl)
   return redirectUrl.host === originUrl.host
 }
+
+module.exports.getAuthzForUser = async user => {
+  const staffedCourses = await Course.findAll({
+    where: {
+      '$staff.id$': user.id,
+    },
+    attributes: ['id'],
+    include: [
+      {
+        model: User,
+        as: 'staff',
+        attributes: [],
+      },
+    ],
+    raw: true,
+  })
+  const staffedCourseIds = staffedCourses.map(row => row.id)
+
+  return {
+    isAdmin: user.isAdmin,
+    staffedCourseIds,
+  }
+}
diff --git a/src/components/ConfidentialQueuePanel.js b/src/components/ConfidentialQueuePanel.js
new file mode 100644
index 0000000..94e94ac
--- /dev/null
+++ b/src/components/ConfidentialQueuePanel.js
@@ -0,0 +1,70 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { ListGroupItem } from 'reactstrap'
+
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faSpinner } from '@fortawesome/free-solid-svg-icons'
+
+const ConfidentialQueuePanel = props => {
+  const questionId = props.getUserActiveQuestionIdForQueue
+  let position = -1
+  if (props.queue && props.queue.questions) {
+    props.queue.questions.some(qId => {
+      position += 1
+      return qId === questionId
+    })
+
+    const numQuestions = props.queue.questions.length
+    if (numQuestions === 0) {
+      return null
+    }
+    const cardTotalQuestionsMessage =
+      numQuestions === 1
+        ? 'There is 1 person on the queue'
+        : `There are ${numQuestions} people on the queue`
+    const singularOrPluralPerson = position === 1 ? 'person' : 'people'
+    const cardMessage =
+      questionId === -1
+        ? `${cardTotalQuestionsMessage}.`
+        : `${cardTotalQuestionsMessage} with ${position} ${singularOrPluralPerson} in front of you.`
+
+    return (
+      <div>
+        <ListGroupItem className="text-center text-muted pt-4 pb-4 mb-3">
+          {cardMessage}
+        </ListGroupItem>
+      </div>
+    )
+  }
+  return (
+    <div>
+      <ListGroupItem className="text-center pt-4 pb-4">
+        <FontAwesomeIcon icon={faSpinner} pulse />
+      </ListGroupItem>
+    </div>
+  )
+}
+
+ConfidentialQueuePanel.propTypes = {
+  queue: PropTypes.shape({
+    questions: PropTypes.arrayOf(PropTypes.number),
+    fixedLocation: PropTypes.bool,
+    isConfidential: PropTypes.bool,
+  }),
+  questions: PropTypes.objectOf(
+    PropTypes.shape({
+      name: PropTypes.string,
+      location: PropTypes.string,
+      topic: PropTypes.string,
+    })
+  ),
+  getUserActiveQuestionIdForQueue: PropTypes.number,
+}
+
+ConfidentialQueuePanel.defaultProps = {
+  queue: null,
+  questions: null,
+  getUserActiveQuestionIdForQueue: -1,
+}
+
+export default ConfidentialQueuePanel
diff --git a/src/components/NewQueue.js b/src/components/NewQueue.js
index eb96d36..2603f12 100644
--- a/src/components/NewQueue.js
+++ b/src/components/NewQueue.js
@@ -26,6 +26,7 @@ class NewQueue extends React.Component {
       name: '',
       location: '',
       fixedLocation: false,
+      isConfidential: false,
       fieldErrors: {},
     }
 
@@ -82,6 +83,7 @@ class NewQueue extends React.Component {
       name: this.state.name,
       location: this.state.location,
       fixedLocation: this.state.fixedLocation,
+      isConfidential: this.state.isConfidential,
     }
 
     let courseId
@@ -169,6 +171,24 @@ class NewQueue extends React.Component {
             </FormText>
           </Col>
         </FormGroup>
+        <FormGroup row>
+          <Label for="isConfidential" sm={3}>
+            Confidential Queue
+          </Label>
+          <Col sm={9}>
+            <CustomInput
+              id="isConfidential"
+              type="switch"
+              name="isConfidential"
+              defaultChecked={false}
+              onChange={this.handleInputChange}
+            />
+            <FormText color="muted">
+              If a queue is marked as confidential, students won&apos;t be able
+              to see other student names and topics.
+            </FormText>
+          </Col>
+        </FormGroup>
         <FormGroup row>
           <Label for="location" sm={3}>
             Location
diff --git a/src/components/QuestionList.js b/src/components/QuestionList.js
index ac0e551..de1808c 100644
--- a/src/components/QuestionList.js
+++ b/src/components/QuestionList.js
@@ -153,31 +153,41 @@ class QuestionList extends React.Component {
   }
 
   render() {
+    const { queue, userId } = this.props
     let questions
-    if (this.props.queue && this.props.queue.questions) {
-      if (this.props.queue.questions.length > 0) {
-        questions = this.props.queue.questions.map(questionId => {
+    if (queue && queue.questions) {
+      if (queue.questions.length > 0) {
+        questions = queue.questions.map(questionId => {
           const question = this.props.questions[questionId]
-          return (
-            <Question
-              key={questionId}
-              isUserCourseStaff={this.props.isUserCourseStaff}
-              isUserActiveStaffForQueue={this.props.isUserActiveStaffForQueue}
-              isUserAnsweringQuestion={
-                this.props.userId === question.answeredById
-              }
-              isUserAnsweringOtherQuestion={
-                this.props.isUserAnsweringQuestionForQueue
-              }
-              didUserAskQuestion={this.props.userId === question.askedById}
-              deleteQuestion={() => this.deleteQuestion(questionId)}
-              cancelQuestion={() => this.cancelQuestion(questionId)}
-              startQuestion={() => this.startQuestion(questionId)}
-              finishedAnswering={() => this.handleFinishedAnswering(questionId)}
-              editQuestion={() => this.handleEditQuestion(questionId)}
-              {...question}
-            />
-          )
+          if (
+            !queue.isConfidential ||
+            (userId === question.askedById ||
+              this.props.isUserCourseStaff ||
+              this.props.isUserAdmin)
+          ) {
+            return (
+              <Question
+                key={questionId}
+                isConfidential={queue.isConfidential}
+                isUserCourseStaff={this.props.isUserCourseStaff}
+                isUserActiveStaffForQueue={this.props.isUserActiveStaffForQueue}
+                isUserAnsweringQuestion={userId === question.answeredById}
+                isUserAnsweringOtherQuestion={
+                  this.props.isUserAnsweringQuestionForQueue
+                }
+                didUserAskQuestion={userId === question.askedById}
+                deleteQuestion={() => this.deleteQuestion(questionId)}
+                cancelQuestion={() => this.cancelQuestion(questionId)}
+                startQuestion={() => this.startQuestion(questionId)}
+                finishedAnswering={() =>
+                  this.handleFinishedAnswering(questionId)
+                }
+                editQuestion={() => this.handleEditQuestion(questionId)}
+                {...question}
+              />
+            )
+          }
+          return null
         })
       } else {
         questions = (
@@ -237,7 +247,7 @@ class QuestionList extends React.Component {
         />
         <QuestionEdit
           question={this.props.questions[this.state.attributeId]}
-          queue={this.props.queue}
+          queue={queue}
           isOpen={this.state.showQuestionEditModal}
           onSubmitQuestionEdit={attributes =>
             this.handleSubmitQuestionEdit(attributes)
@@ -253,6 +263,7 @@ QuestionList.propTypes = {
   queue: PropTypes.shape({
     questions: PropTypes.arrayOf(PropTypes.number),
     fixedLocation: PropTypes.bool,
+    isConfidential: PropTypes.bool,
   }),
   questions: PropTypes.objectOf(
     PropTypes.shape({
@@ -265,6 +276,7 @@ QuestionList.propTypes = {
   isUserCourseStaff: PropTypes.bool.isRequired,
   isUserActiveStaffForQueue: PropTypes.bool.isRequired,
   isUserAnsweringQuestionForQueue: PropTypes.bool.isRequired,
+  isUserAdmin: PropTypes.bool.isRequired,
   deleteQuestion: PropTypes.func.isRequired,
   updateQuestionBeingAnswered: PropTypes.func.isRequired,
   finishAnsweringQuestion: PropTypes.func.isRequired,
diff --git a/src/components/QueueCard.js b/src/components/QueueCard.js
index e75cb2d..6fce955 100644
--- a/src/components/QueueCard.js
+++ b/src/components/QueueCard.js
@@ -5,6 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
 import {
   faMapMarker,
   faQuestionCircle,
+  faEyeSlash,
 } from '@fortawesome/free-solid-svg-icons'
 
 import ShowForCourseStaff from './ShowForCourseStaff'
@@ -17,7 +18,7 @@ const QueueCard = ({
   onUpdate,
   ...rest
 }) => {
-  const { name: queueName, location, questionCount } = queue
+  const { name: queueName, location, questionCount, isConfidential } = queue
 
   const questionCountText = `${questionCount} Question${
     questionCount !== 1 ? 's' : ''
@@ -50,7 +51,12 @@ const QueueCard = ({
     >
       <CardBody>
         <CardTitle className="d-flex flex-wrap align-items-center">
-          <span className="h5 mb-2 mr-auto pr-3">{title}</span>
+          <span className="h5 mb-2 mr-auto pr-3">
+            {isConfidential && !showQueueNameInBody && (
+              <FontAwesomeIcon icon={faEyeSlash} fixedWidth className="mr-2" />
+            )}
+            {title}
+          </span>
           <div>
             <ShowForCourseStaff courseId={queue.courseId}>
               <Button
@@ -78,7 +84,12 @@ const QueueCard = ({
           </div>
         </CardTitle>
         {showQueueNameInBody && (
-          <CardSubtitle className="mb-2">{queueName}</CardSubtitle>
+          <CardSubtitle className="mb-2">
+            {isConfidential && (
+              <FontAwesomeIcon icon={faEyeSlash} fixedWidth className="mr-2" />
+            )}
+            {queueName}
+          </CardSubtitle>
         )}
         <div className="text-muted">
           <FontAwesomeIcon icon={faMapMarker} fixedWidth className="mr-2" />
diff --git a/src/containers/ConfidentialQueuePanelContainer.js b/src/containers/ConfidentialQueuePanelContainer.js
new file mode 100644
index 0000000..d545a6a
--- /dev/null
+++ b/src/containers/ConfidentialQueuePanelContainer.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux'
+
+import ConfidentialQueuePanel from '../components/ConfidentialQueuePanel'
+import { getUserActiveQuestionIdForQueue } from '../selectors'
+
+const mapStateToProps = (state, props) => ({
+  queue: state.queues.queues[props.queueId],
+  questions: state.questions.questions,
+  getUserActiveQuestionIdForQueue: getUserActiveQuestionIdForQueue(
+    state,
+    props
+  ),
+})
+
+export default connect(mapStateToProps)(ConfidentialQueuePanel)
diff --git a/src/containers/QuestionListContainer.js b/src/containers/QuestionListContainer.js
index 81e214c..b607386 100644
--- a/src/containers/QuestionListContainer.js
+++ b/src/containers/QuestionListContainer.js
@@ -10,6 +10,7 @@ import {
   isUserCourseStaffForQueue,
   isUserActiveStaffForQueue,
   isUserAnsweringQuestionForQueue,
+  isUserAdmin,
 } from '../selectors'
 
 import QuestionList from '../components/QuestionList'
@@ -23,6 +24,7 @@ const mapStateToProps = (state, props) => ({
     state,
     props
   ),
+  isUserAdmin: isUserAdmin(state, props),
   userId: state.user.user.id,
 })
 
diff --git a/src/middleware/authz.js b/src/middleware/authz.js
index 1ff8a5b..fed9112 100644
--- a/src/middleware/authz.js
+++ b/src/middleware/authz.js
@@ -1,29 +1,10 @@
-const { User, Course } = require('../models')
+const { getAuthzForUser } = require('../auth/util')
 
 module.exports = async (req, res, next) => {
-  // Grab the user from the authn stage
+  // Grab the user from the authentication stage
   const { userAuthn } = res.locals
 
-  const staffedCourses = await Course.findAll({
-    where: {
-      '$staff.id$': userAuthn.id,
-    },
-    attributes: ['id'],
-    include: [
-      {
-        model: User,
-        as: 'staff',
-        attributes: [],
-      },
-    ],
-    raw: true,
-  })
-  const staffedCourseIds = staffedCourses.map(row => row.id)
-
-  res.locals.userAuthz = {
-    isAdmin: userAuthn.isAdmin,
-    staffedCourseIds,
-  }
+  res.locals.userAuthz = await getAuthzForUser(userAuthn)
 
   next()
 }
diff --git a/src/migrations/20181029192933-is-confidential-queue.js b/src/migrations/20181029192933-is-confidential-queue.js
new file mode 100644
index 0000000..561d4f9
--- /dev/null
+++ b/src/migrations/20181029192933-is-confidential-queue.js
@@ -0,0 +1,13 @@
+module.exports = {
+  up: (queryInterface, Sequelize) => {
+    return queryInterface.addColumn('queues', 'isConfidential', {
+      type: Sequelize.BOOLEAN,
+      defaultValue: false,
+      after: 'messageEnabled',
+    })
+  },
+
+  down: (queryInterface, _Sequelize) => {
+    return queryInterface.removeColumn('queues', 'isConfidential')
+  },
+}
diff --git a/src/models/Queue.js b/src/models/Queue.js
index c57de83..c9baec4 100644
--- a/src/models/Queue.js
+++ b/src/models/Queue.js
@@ -17,6 +17,10 @@ module.exports = (sequelize, DataTypes) => {
         type: DataTypes.BOOLEAN,
         defaultValue: false,
       },
+      isConfidential: {
+        type: DataTypes.BOOLEAN,
+        defaultValue: false,
+      },
       startTime: DataTypes.DATE,
       endTime: DataTypes.DATE,
     },
diff --git a/src/pages/queue.js b/src/pages/queue.js
index c97e130..779e0f3 100644
--- a/src/pages/queue.js
+++ b/src/pages/queue.js
@@ -1,9 +1,17 @@
 import React from 'react'
 import PropTypes from 'prop-types'
 import { connect } from 'react-redux'
-import { Container, Row, Col, Card, CardBody, Collapse } from 'reactstrap'
+import {
+  Container,
+  Row,
+  Col,
+  Card,
+  CardBody,
+  Collapse,
+  UncontrolledTooltip,
+} from 'reactstrap'
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faMapMarker } from '@fortawesome/free-solid-svg-icons'
+import { faMapMarker, faEyeSlash } from '@fortawesome/free-solid-svg-icons'
 
 import { fetchQueue, fetchQueueRequest } from '../actions/queue'
 import { connectToQueue, disconnectFromQueue } from '../socket/client'
@@ -19,6 +27,8 @@ import QuestionNotificationsToggle from '../components/QuestionNotificationsTogg
 import QueueStatusToggleContainer from '../containers/QueueStatusToggleContainer'
 import DeleteAllQuestionsButtonContainer from '../containers/DeleteAllQuestionsButtonContainer'
 import QueueMessageEnabledToggleContainer from '../containers/QueueMessageEnabledToggleContainer'
+import { isUserCourseStaff, isUserAdmin } from '../selectors'
+import ConfidentialQueuePanelContainer from '../containers/ConfidentialQueuePanelContainer'
 
 class Queue extends React.Component {
   static getInitialProps({ isServer, store, query }) {
@@ -64,9 +74,29 @@ class Queue extends React.Component {
       return <Error statusCode={404} />
     }
     const locationText = this.props.queue.location || 'No location specified'
+    const confidentialMessage =
+      this.props.isUserCourseStaff || this.props.isUserAdmin
+        ? 'Students'
+        : 'You'
     return (
       <Container fluid>
-        <h3>{this.props.queue.name}</h3>
+        <h3>
+          {this.props.queue.isConfidential && (
+            <span>
+              <FontAwesomeIcon
+                icon={faEyeSlash}
+                fixedWidth
+                className="mr-2"
+                id="confidentialIcon"
+              />
+              <UncontrolledTooltip placement="bottom" target="confidentialIcon">
+                This is a confidential queue! {confidentialMessage} won&apos;t
+                be able to see questions from other students.
+              </UncontrolledTooltip>
+            </span>
+          )}
+          {this.props.queue.name}
+        </h3>
         <h5 className="mb-3 text-muted">
           <FontAwesomeIcon icon={faMapMarker} fixedWidth className="mr-2" />
           {locationText}
@@ -102,6 +132,11 @@ class Queue extends React.Component {
                 </CardBody>
               </Card>
             )}
+            {this.props.queue.isConfidential &&
+              !this.props.isUserCourseStaff &&
+              !this.props.isUserAdmin && (
+                <ConfidentialQueuePanelContainer queueId={this.props.queueId} />
+              )}
             <QuestionListContainer queueId={this.props.queueId} />
           </Col>
         </Row>
@@ -116,10 +151,13 @@ Queue.propTypes = {
   fetchQueue: PropTypes.func.isRequired,
   queueId: PropTypes.number.isRequired,
   dispatch: PropTypes.func.isRequired,
+  isUserCourseStaff: PropTypes.bool.isRequired,
+  isUserAdmin: PropTypes.bool.isRequired,
   queue: PropTypes.shape({
     id: PropTypes.number,
     name: PropTypes.string,
     location: PropTypes.string,
+    isConfidential: PropTypes.bool,
     courseId: PropTypes.number,
     open: PropTypes.bool,
     message: PropTypes.string,
@@ -137,6 +175,8 @@ const mapStateToProps = (state, ownProps) => ({
   isFetching: state.queues.isFetching,
   hasQueue: !!state.queues.queues[ownProps.queueId],
   queue: state.queues.queues[ownProps.queueId],
+  isUserCourseStaff: isUserCourseStaff(state, ownProps),
+  isUserAdmin: isUserAdmin(state, ownProps),
 })
 
 const mapDispatchToProps = dispatch => ({
diff --git a/src/socket/server.js b/src/socket/server.js
index a6df17d..65dc0cf 100644
--- a/src/socket/server.js
+++ b/src/socket/server.js
@@ -3,12 +3,21 @@ const cookieParser = require('cookie-parser')
 
 const logger = require('../util/logger')
 const { sequelize, Question, User, ActiveStaff, Queue } = require('../models')
-const { getUserFromJwt } = require('../auth/util')
+const { getUserFromJwt, getAuthzForUser } = require('../auth/util')
+const {
+  canUserSeeQuestionDetailsForConfidentialQueue,
+  filterConfidentialQueueQuestionsForUser,
+} = require('../api/util')
 
 let io = null
 let queueNamespace = null
 
-const sendInitialState = (queueId, callback) => {
+const sendInitialState = (
+  queueId,
+  userId,
+  sendCompleteQuestionData,
+  callback
+) => {
   const questionPromise = Question.findAll({
     where: {
       queueId,
@@ -26,29 +35,40 @@ const sendInitialState = (queueId, callback) => {
   })
 
   Promise.all([questionPromise, activeStaffPromise]).then(results => {
-    const [questions, activeStaff] = results
+    const [questionsResult, activeStaff] = results
+    let questions = questionsResult.map(q => q.get({ plain: true }))
+    if (!sendCompleteQuestionData) {
+      questions = filterConfidentialQueueQuestionsForUser(userId, questions)
+    }
+
     callback({ questions, activeStaff })
   })
 }
 
-const handleQuestionCreate = (id, queueId) => {
-  Question.findOne({ where: { id } }).then(question => {
-    queueNamespace
-      .to(`queue-${queueId}`)
-      .emit('question:create', { id, question })
-  })
+const handleQuestionCreate = async (id, queueId) => {
+  const question = await Question.findOne({ where: { id } })
+  queueNamespace.to(`queue-${queueId}`).emit('question:create', { question })
+  // Public confidential queues only need to learn that a question was added
+  queueNamespace
+    .to(`queue-${queueId}-public`)
+    .emit('question:create', { question: { id } })
 }
 
-const handleQuestionUpdate = (id, queueId) => {
-  Question.findOne({ where: { id } }).then(question => {
-    queueNamespace
-      .to(`queue-${queueId}`)
-      .emit('question:update', { id, question })
-  })
+const handleQuestionUpdate = async (id, queueId) => {
+  const question = await Question.findOne({ where: { id } })
+  // Public confidential queues don't need to know about question updates
+  // However, the user that *asked* the question should in fact get this update
+  queueNamespace
+    .to(`queue-${queueId}`)
+    .to(`queue-${queueId}-user-${question.askedById}`)
+    .emit('question:update', { question })
 }
 
 const handleQuestionDelete = (id, queueId) => {
-  queueNamespace.to(`queue-${queueId}`).emit('question:delete', { id })
+  queueNamespace
+    .to(`queue-${queueId}`)
+    .to(`queue-${queueId}-public`)
+    .emit('question:delete', { id })
 }
 
 const handleQuestionEvent = (event, instance) => {
@@ -84,13 +104,17 @@ const handleActiveStaffCreate = instance => {
     } else {
       queueNamespace
         .to(`queue-${activeStaff.queueId}`)
+        .to(`queue-${activeStaff.queueId}-public`)
         .emit('activeStaff:create', { id, activeStaff })
     }
   })
 }
 
 const handleActiveStaffDelete = (id, queueId) => {
-  queueNamespace.to(`queue-${queueId}`).emit('activeStaff:delete', { id })
+  queueNamespace
+    .to(`queue-${queueId}`)
+    .to(`queue-${queueId}-public`)
+    .emit('activeStaff:delete', { id })
 }
 
 const handleActiveStaffEvent = (event, instance) => {
@@ -111,7 +135,10 @@ const handleActiveStaffEvent = (event, instance) => {
 
 const handleQueueUpdate = id => {
   Queue.findOne({ where: { id } }).then(queue => {
-    queueNamespace.to(`queue-${id}`).emit('queue:update', { id, queue })
+    queueNamespace
+      .to(`queue-${id}`)
+      .to(`queue-${id}-public`)
+      .emit('queue:update', { id, queue })
   })
 }
 
@@ -135,6 +162,7 @@ const parseSocketCookies = () => {
 const stream = sequelizeStream(sequelize)
 stream.on('data', data => {
   const { event, instance } = data
+  // Need to have isConfidential in  question?
   if (instance instanceof Question) {
     handleQuestionEvent(event, instance)
   } else if (instance instanceof ActiveStaff) {
@@ -159,11 +187,42 @@ module.exports = newIo => {
 
   queueNamespace = io.of('/queue')
   queueNamespace.on('connection', socket => {
-    socket.on('join', (msg, callback) => {
+    socket.on('join', async (msg, callback) => {
       if ('queueId' in msg) {
         const { queueId } = msg
-        socket.join(`queue-${queueId}`)
-        sendInitialState(queueId, callback)
+        const queuePromise = Queue.findOne({
+          where: {
+            id: queueId,
+          },
+        })
+        const userAuthzPromise = getAuthzForUser(socket.request.user)
+        const [queue, userAuthz] = await Promise.all([
+          queuePromise,
+          userAuthzPromise,
+        ])
+        const { courseId, isConfidential } = queue
+        const canSeeQuestionDetails = canUserSeeQuestionDetailsForConfidentialQueue(
+          userAuthz,
+          courseId
+        )
+        let sendCompleteQuestionData = true
+        if (isConfidential && !canSeeQuestionDetails) {
+          // All users that shouldn't see confidential information are added
+          // to a "public" version of the room that receives the minimum
+          // possible set of information
+          socket.join(`queue-${queueId}-public`)
+          // Users will also join a specific room for themselves so that they
+          // receive updates about questions being answered, etc.
+          socket.join(`queue-${queueId}-user-${socket.request.user.id}`)
+          sendCompleteQuestionData = false
+        } else {
+          // For non-confidential queues, this room will consider receiving all
+          // updates for all users. For confidential queues, only admins and
+          // course staff will be subscribed to this room
+          socket.join(`queue-${queueId}`)
+        }
+        const { id: userId } = socket.request.user
+        sendInitialState(queueId, userId, sendCompleteQuestionData, callback)
       }
     })
   })


Test patch:
diff --git a/src/api/questions.test.js b/src/api/questions.test.js
index 20e0acf..95f1f2a 100644
--- a/src/api/questions.test.js
+++ b/src/api/questions.test.js
@@ -166,8 +166,8 @@ describe('Questions API', () => {
   })
 
   describe('GET /api/queues/:queueId/questions', () => {
-    test('succeeds with valid response', async () => {
-      const request = await requestAsUser(app, 'admin')
+    const checkValidResponseForUser = async user => {
+      const request = await requestAsUser(app, user)
       const res = await request.get('/api/queues/1/questions')
       expect(res.statusCode).toBe(200)
       expect(res.body).toHaveLength(2)
@@ -178,6 +178,35 @@ describe('Questions API', () => {
       expect(res.body[1]).toHaveProperty('askedBy')
       expect(res.body[1].id).toBe(2)
       expect(res.body[1].askedBy.netid).toBe('student')
+    }
+
+    test('succeeds with valid response for admin', async () => {
+      await checkValidResponseForUser('admin')
+    })
+
+    test('succeeds with valid response for student', async () => {
+      await checkValidResponseForUser('student')
+    })
+
+    const checkValidResponseForUserOnConfidentialQueue = async user => {
+      const request = await requestAsUser(app, user)
+      const res = await request.get('/api/queues/5/questions')
+      expect(res.statusCode).toBe(200)
+      expect(Array.isArray(res.body)).toBeTruthy()
+      expect(res.body).toHaveLength(2)
+      const [question1, question2] = res.body
+      expect(question1).toHaveProperty('askedById', 5)
+      expect(question1).toHaveProperty('askedBy.netid', 'student')
+      expect(question2).toHaveProperty('askedById', 6)
+      expect(question2).toHaveProperty('askedBy.netid', 'otherstudent')
+    }
+
+    test('includes data for admin on confidential queue', async () => {
+      await checkValidResponseForUserOnConfidentialQueue('admin')
+    })
+
+    test('includes data for course staff on confidential queue', async () => {
+      await checkValidResponseForUserOnConfidentialQueue('225staff')
     })
 
     test('fails if queue does not exist', async () => {
@@ -186,18 +215,29 @@ describe('Questions API', () => {
       expect(res.statusCode).toBe(404)
     })
 
-    test('succeeds with valid response for non admin', async () => {
-      const request = await requestAsUser(app, 'student')
-      const res = await request.get('/api/queues/1/questions')
+    const excludesDataForUser = async user => {
+      const request = await requestAsUser(app, user)
+      const res = await request.get('/api/queues/5/questions')
       expect(res.statusCode).toBe(200)
+      expect(Array.isArray(res.body)).toBeTruthy()
       expect(res.body).toHaveLength(2)
-      // Ensure the questions are ordered correctly
-      expect(res.body[0].id).toBe(1)
-      expect(res.body[0]).toHaveProperty('askedBy')
-      expect(res.body[0].askedBy.netid).toBe('admin')
-      expect(res.body[1].id).toBe(2)
-      expect(res.body[1]).toHaveProperty('askedBy')
-      expect(res.body[1].askedBy.netid).toBe('student')
+      // We expect all questions not asked by 'student' (user 5) to have no
+      // information besides question ID
+      res.body.forEach(question => {
+        if (Object.keys(question).length > 1) {
+          expect(question.askedById).toEqual(5)
+        } else {
+          expect(Object.keys(question)).toEqual(['id'])
+        }
+      })
+    }
+
+    test('excludes question information for other staff on a confidential queue', async () => {
+      await excludesDataForUser('241staff')
+    })
+
+    test('excludes question information for students on a confidential queue', async () => {
+      await excludesDataForUser('student')
     })
   })
 
@@ -225,6 +265,38 @@ describe('Questions API', () => {
       expect(res.body.topic).toBe('Queue')
       expect(res.body.id).toBe(1)
     })
+
+    test('succeeds for course staff of confidential queue', async () => {
+      const request = await requestAsUser(app, '225staff')
+      const res = await request.get('/api/queues/5/questions/4')
+      expect(res.statusCode).toBe(200)
+      expect(res.body.id).toBe(4)
+      expect(res.body.name).toBe('Student')
+      expect(res.body).toHaveProperty('askedBy.netid', 'student')
+    })
+
+    test('succeeds for student who asked question on confidential queue', async () => {
+      const request = await requestAsUser(app, 'student')
+      const res = await request.get('/api/queues/5/questions/4')
+      expect(res.statusCode).toBe(200)
+      expect(res.body.id).toBe(4)
+      expect(res.body.name).toBe('Student')
+      expect(res.body).toHaveProperty('askedBy.netid', 'student')
+    })
+
+    test('fails for other course staff on confidential queue', async () => {
+      const request = await requestAsUser(app, '241staff')
+      const res = await request.get('/api/queues/5/questions/4')
+      expect(res.statusCode).toBe(403)
+      expect(res.body).toEqual({})
+    })
+
+    test('fails for student on confidential queue', async () => {
+      const request = await requestAsUser(app, 'otherstudent')
+      const res = await request.get('/api/queues/5/questions/4')
+      expect(res.statusCode).toBe(403)
+      expect(res.body).toEqual({})
+    })
   })
 
   describe('PATCH /api/queues/:queueId/questions/:questionId', () => {
diff --git a/src/api/queues.test.js b/src/api/queues.test.js
index 3c3e9f0..ba48153 100644
--- a/src/api/queues.test.js
+++ b/src/api/queues.test.js
@@ -17,7 +17,7 @@ describe('Queues API', () => {
       const request = await requestAsUser(app, user)
       const res = await request.get(`/api/queues`)
       expect(res.statusCode).toBe(200)
-      expect(res.body.length).toEqual(4)
+      expect(res.body.length).toEqual(5)
       expect(res.body[0].name).toBe('CS225 Queue')
       expect(res.body[1].name).toBe('CS241 Queue')
       expect(res.body[0].location).toBe('Here')
@@ -86,6 +86,54 @@ describe('Queues API', () => {
     })
   })
 
+  describe('GET /api/queues/5 (confidential queue)', () => {
+    const includesDataForUser = async user => {
+      const request = await requestAsUser(app, user)
+      const res = await request.get('/api/queues/5')
+
+      expect(Array.isArray(res.body.questions)).toBeTruthy()
+      expect(res.body.questions).toHaveLength(2)
+      const [question1, question2] = res.body.questions
+      expect(question1).toHaveProperty('askedById', 5)
+      expect(question1).toHaveProperty('askedBy.netid', 'student')
+      expect(question2).toHaveProperty('askedById', 6)
+      expect(question2).toHaveProperty('askedBy.netid', 'otherstudent')
+    }
+
+    test('includes all question data for admin', async () => {
+      await includesDataForUser('admin')
+    })
+
+    test('includes all question data for course staff', async () => {
+      await includesDataForUser('225staff')
+    })
+
+    const excludesDataForUser = async user => {
+      const request = await requestAsUser(app, user)
+      const res = await request.get('/api/queues/5')
+
+      // We expect all questions not asked by 'student' (user 5) to have no
+      // information besides question ID
+      expect(Array.isArray(res.body.questions)).toBeTruthy()
+      expect(res.body.questions).toHaveLength(2)
+      res.body.questions.forEach(question => {
+        if (Object.keys(question).length > 1) {
+          expect(question.askedById).toEqual(5)
+        } else {
+          expect(Object.keys(question)).toEqual(['id'])
+        }
+      })
+    }
+
+    test('excludes question data for other course staff on confidential queue', async () => {
+      await excludesDataForUser('241staff')
+    })
+
+    test('excludes question data for students on confidential queue', async () => {
+      await excludesDataForUser('student')
+    })
+  })
+
   describe('GET /api/queues/2/staff', () => {
     test('succeeds for admin', async () => {
       const request = await requestAsUser(app, 'admin')
@@ -97,41 +145,74 @@ describe('Queues API', () => {
 
   describe('POST /api/queues', () => {
     test('succeeds for admin', async () => {
-      const queue = { name: 'CS225 Queue 2', location: 'Where' }
+      const queue = {
+        name: 'CS225 Queue 2',
+        location: 'Where',
+        isConfidential: false,
+      }
       const request = await requestAsUser(app, 'admin')
       const res = await request.post('/api/courses/1/queues').send(queue)
       expect(res.statusCode).toBe(201)
       expect(res.body.name).toBe('CS225 Queue 2')
       expect(res.body.location).toBe('Where')
       expect(res.body.questionCount).toBe(0)
+      expect(res.body.isConfidential).toBe(false)
     })
 
     test('succeeds for course staff', async () => {
-      const queue = { name: 'CS225 Queue 2', location: 'Where' }
+      const queue = {
+        name: 'CS225 Queue 2',
+        location: 'Where',
+        isConfidential: false,
+      }
       const request = await requestAsUser(app, '225staff')
       const res = await request.post('/api/courses/1/queues').send(queue)
       expect(res.statusCode).toBe(201)
       expect(res.body.name).toBe('CS225 Queue 2')
       expect(res.body.location).toBe('Where')
       expect(res.body.questionCount).toBe(0)
+      expect(res.body.isConfidential).toBe(false)
+    })
+
+    test('succeeds for confidential queue', async () => {
+      const queue = {
+        name: 'CS225 Confidential Queue',
+        location: 'Where',
+        isConfidential: true,
+      }
+      const request = await requestAsUser(app, '225staff')
+      const res = await request.post('/api/courses/1/queues').send(queue)
+      expect(res.statusCode).toBe(201)
+      expect(res.body.name).toBe('CS225 Confidential Queue')
+      expect(res.body.location).toBe('Where')
+      expect(res.body.questionCount).toBe(0)
+      expect(res.body.isConfidential).toBe(true)
     })
 
     test('fails if name is missing', async () => {
-      const queue = { location: 'Where' }
+      const queue = { location: 'Where', isConfidential: false }
       const request = await requestAsUser(app, '225staff')
       const res = await request.post('/api/courses/1/queues').send(queue)
       expect(res.statusCode).toBe(422)
     })
 
     test('fails if location is missing and queue is fixed-location', async () => {
-      const queue = { name: 'CS225 Queue 2', fixedLocation: true }
+      const queue = {
+        name: 'CS225 Queue 2',
+        fixedLocation: true,
+        isConfidential: false,
+      }
       const request = await requestAsUser(app, '225staff')
       const res = await request.post('/api/courses/1/queues').send(queue)
       expect(res.statusCode).toBe(422)
     })
 
     test('fails for student', async () => {
-      const queue = { name: 'CS225 Queue 2', location: 'Where' }
+      const queue = {
+        name: 'CS225 Queue 2',
+        location: 'Where',
+        isConfidential: false,
+      }
       const request = await requestAsUser(app, 'student')
       const res = await request.post('/api/courses/1/queues').send(queue)
       expect(res.statusCode).toBe(403)
@@ -139,7 +220,11 @@ describe('Queues API', () => {
     })
 
     test('fails for course staff of different course', async () => {
-      const queue = { name: 'CS225 Queue 2', location: 'Where' }
+      const queue = {
+        name: 'CS225 Queue 2',
+        location: 'Where',
+        isConfidential: false,
+      }
       const request = await requestAsUser(app, '241staff')
       const res = await request.post('/api/courses/1/queues').send(queue)
       expect(res.statusCode).toBe(403)
@@ -388,6 +473,22 @@ describe('Queues API', () => {
       expect(res2.body.open).toBe(true)
     })
 
+    test('succeeds for confidential queue not changing when closing queue', async () => {
+      const attributes = { messageEnabled: false }
+      const request = await requestAsUser(app, 'admin')
+      const res = await request.patch('/api/queues/5').send(attributes)
+      expect(res.statusCode).toBe(201)
+      expect(res.body.isConfidential).toBe(true)
+    })
+
+    test('succeeds for confidential queue not changing when closing message', async () => {
+      const attributes = { open: false }
+      const request = await requestAsUser(app, 'admin')
+      const res = await request.patch('/api/queues/5').send(attributes)
+      expect(res.statusCode).toBe(201)
+      expect(res.body.isConfidential).toBe(true)
+    })
+
     test('wont allow properties to be set to null', async () => {
       const attributes = {
         open: false,
@@ -425,7 +526,7 @@ describe('Queues API', () => {
       const request2 = await requestAsUser(app, 'admin')
       const res2 = await request2.get('/api/queues')
       expect(res2.statusCode).toBe(200)
-      expect(res2.body).toHaveLength(3)
+      expect(res2.body).toHaveLength(4)
       expect(res2.body[0].id).toBe(2)
     })
 
@@ -436,7 +537,7 @@ describe('Queues API', () => {
       const request2 = await requestAsUser(app, 'admin')
       const res2 = await request2.get('/api/queues')
       expect(res2.statusCode).toBe(200)
-      expect(res2.body).toHaveLength(3)
+      expect(res2.body).toHaveLength(4)
       expect(res2.body[0].id).toBe(2)
     })
 
@@ -448,7 +549,7 @@ describe('Queues API', () => {
       const request2 = await requestAsUser(app, 'admin')
       const res2 = await request2.get('/api/queues')
       expect(res2.statusCode).toBe(200)
-      expect(res2.body).toHaveLength(4)
+      expect(res2.body).toHaveLength(5)
       expect(res2.body[0].id).toBe(1)
       expect(res2.body[1].id).toBe(2)
     })
@@ -461,7 +562,7 @@ describe('Queues API', () => {
       const request2 = await requestAsUser(app, 'admin')
       const res2 = await request2.get('/api/queues')
       expect(res2.statusCode).toBe(200)
-      expect(res2.body).toHaveLength(4)
+      expect(res2.body).toHaveLength(5)
       expect(res2.body[0].id).toBe(1)
       expect(res2.body[1].id).toBe(2)
     })
@@ -531,7 +632,7 @@ describe('Queues API', () => {
       const request2 = await requestAsUser(app, 'admin')
       const res2 = await request2.get('/api/queues')
       expect(res2.statusCode).toBe(200)
-      expect(res2.body).toHaveLength(3)
+      expect(res2.body).toHaveLength(4)
     })
 
     test('succeeds for admin', async () => {
@@ -540,7 +641,7 @@ describe('Queues API', () => {
       expect(res.statusCode).toBe(202)
       const res2 = await request.get('/api/queues')
       expect(res2.statusCode).toBe(200)
-      expect(res2.body).toHaveLength(3)
+      expect(res2.body).toHaveLength(4)
     })
 
     test('fails for course staff of different course', async () => {
@@ -551,7 +652,7 @@ describe('Queues API', () => {
       const request2 = await requestAsUser(app, 'admin')
       const res2 = await request2.get('/api/queues')
       expect(res2.statusCode).toBe(200)
-      expect(res2.body).toHaveLength(4)
+      expect(res2.body).toHaveLength(5)
     })
 
     test('fails for student', async () => {
@@ -562,7 +663,7 @@ describe('Queues API', () => {
       const request2 = await requestAsUser(app, 'admin')
       const res2 = await request2.get('/api/queues')
       expect(res2.statusCode).toBe(200)
-      expect(res2.body).toHaveLength(4)
+      expect(res2.body).toHaveLength(5)
     })
   })
 })
diff --git a/src/api/util.test.js b/src/api/util.test.js
index fe79b19..b51e92a 100644
--- a/src/api/util.test.js
+++ b/src/api/util.test.js
@@ -125,4 +125,72 @@ describe('Testing Utils', () => {
       expect(send).toBeCalled()
     })
   })
+
+  describe('canUserSeeQuestionDetailsForConfidentialQueue', () => {
+    test('returns true for an admin', () => {
+      const userAuthz = {
+        isAdmin: true,
+        staffedCourseIds: [],
+      }
+      const value = util.canUserSeeQuestionDetailsForConfidentialQueue(
+        userAuthz,
+        123
+      )
+      expect(value).toBeTruthy()
+    })
+
+    test('returns true for course staff', () => {
+      const userAuthz = {
+        isAdmin: false,
+        staffedCourseIds: [123],
+      }
+      const value = util.canUserSeeQuestionDetailsForConfidentialQueue(
+        userAuthz,
+        123
+      )
+      expect(value).toBeTruthy()
+    })
+
+    test('returns false for non-admin, non-course staff', () => {
+      const userAuthz = {
+        isAdmin: false,
+        staffedCourseIds: [],
+      }
+      const value = util.canUserSeeQuestionDetailsForConfidentialQueue(
+        userAuthz,
+        123
+      )
+      expect(value).toBeFalsy()
+    })
+  })
+
+  describe('filterConfidentialQueueQuestionsForUser', () => {
+    test('removes question info from other users', () => {
+      const questions = [
+        {
+          id: 10,
+          askedById: 321,
+          name: 'Secret',
+        },
+        {
+          id: 11,
+          askedById: 123,
+          name: 'Not a Secret',
+        },
+        {
+          id: 12,
+          askedById: 456,
+          name: 'Also a Secret',
+        },
+      ]
+      const result = util.filterConfidentialQueueQuestionsForUser(
+        123,
+        questions
+      )
+      expect(result).toHaveLength(3)
+      expect(result[0]).toEqual({ id: 10 })
+      expect(result[1]).toEqual(questions[1])
+      expect(result[2]).toEqual({ id: 12 })
+    })
+  })
 })
diff --git a/src/middleware/redirectNoQueue.test.js b/src/middleware/redirectNoQueue.test.js
index a87cd1f..ef6247c 100644
--- a/src/middleware/redirectNoQueue.test.js
+++ b/src/middleware/redirectNoQueue.test.js
@@ -52,7 +52,7 @@ describe('redirectNoQueue middleware', () => {
     await Queue.destroy({ where: { id: 2 } })
     const { req, res, next } = makeArgs(2)
     await redirectNoQueue(req, res, next)
-    expect(res.redirect).toBeCalledWith('/queue/5')
+    expect(res.redirect).toBeCalledWith('/queue/6')
     expect(next).not.toBeCalled()
   })
 
diff --git a/test/util.js b/test/util.js
index 7aa39d9..0abf926 100644
--- a/test/util.js
+++ b/test/util.js
@@ -40,6 +40,14 @@ module.exports.createTestQueues = async () => {
       location: 'Everywhere',
       courseId: 1,
     },
+    {
+      name: 'CS225 Confidential Queue',
+      fixedLocation: false,
+      location: 'Everywhere',
+      isConfidential: true,
+      messageEnabled: true,
+      courseId: 1,
+    },
   ])
 }
 
@@ -66,6 +74,20 @@ module.exports.createTestQuestions = async () => {
       topic: 'Sauce',
       askedById: 2,
     },
+    {
+      queueId: 5,
+      name: 'Student',
+      location: '',
+      topic: 'Secret',
+      askedById: 5,
+    },
+    {
+      queueId: 5,
+      name: 'Other Student',
+      location: '',
+      topic: 'Secret',
+      askedById: 6,
+    },
   ])
 }