Browse Source

Introduce Reservation

binwiederhier 3 years ago
parent
commit
1733323132

+ 6 - 2
cmd/access.go

@@ -183,11 +183,15 @@ func showUserAccess(c *cli.Context, manager *user.Manager, username string) erro
 
 func showUsers(c *cli.Context, manager *user.Manager, users []*user.User) error {
 	for _, u := range users {
+		grants, err := manager.Grants(u.Name)
+		if err != nil {
+			return err
+		}
 		fmt.Fprintf(c.App.ErrWriter, "user %s (%s)\n", u.Name, u.Role)
 		if u.Role == user.RoleAdmin {
 			fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n")
-		} else if len(u.Grants) > 0 {
-			for _, grant := range u.Grants {
+		} else if len(grants) > 0 {
+			for _, grant := range grants {
 				if grant.AllowRead && grant.AllowWrite {
 					fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s\n", grant.TopicPattern)
 				} else if grant.AllowRead {

+ 2 - 2
server/server.go

@@ -1308,7 +1308,7 @@ func (s *Server) runFirebaseKeepaliver() {
 	if s.firebaseClient == nil {
 		return
 	}
-	v := newVisitor(s.config, s.messageCache, netip.IPv4Unspecified(), nil) // Background process, not a real visitor, uses IP 0.0.0.0
+	v := newVisitor(s.config, s.messageCache, s.userManager, netip.IPv4Unspecified(), nil) // Background process, not a real visitor, uses IP 0.0.0.0
 	for {
 		select {
 		case <-time.After(s.config.FirebaseKeepaliveInterval):
@@ -1579,7 +1579,7 @@ func (s *Server) visitorFromID(visitorID string, ip netip.Addr, user *user.User)
 	defer s.mu.Unlock()
 	v, exists := s.visitors[visitorID]
 	if !exists {
-		s.visitors[visitorID] = newVisitor(s.config, s.messageCache, ip, user)
+		s.visitors[visitorID] = newVisitor(s.config, s.messageCache, s.userManager, ip, user)
 		return s.visitors[visitorID]
 	}
 	v.Keepalive()

+ 26 - 11
server/server_account.go

@@ -94,16 +94,27 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis
 				Upgradable: true,
 			}
 		}
-		if len(v.user.Grants) > 0 {
-			response.Access = make([]*apiAccountGrant, 0)
-			for _, grant := range v.user.Grants {
-				if grant.Owner {
-					response.Access = append(response.Access, &apiAccountGrant{
-						Topic: grant.TopicPattern,
-						Read:  grant.AllowRead,
-						Write: grant.AllowWrite,
-					})
+		reservations, err := s.userManager.Reservations(v.user.Name)
+		if err != nil {
+			return err
+		}
+		if len(reservations) > 0 {
+			response.Reservations = make([]*apiAccountReservation, 0)
+			for _, r := range reservations {
+				var everyone string
+				if r.AllowEveryoneRead && r.AllowEveryoneWrite {
+					everyone = "read-write"
+				} else if r.AllowEveryoneRead && !r.AllowEveryoneWrite {
+					everyone = "read-only"
+				} else if !r.AllowEveryoneRead && r.AllowEveryoneWrite {
+					everyone = "write-only"
+				} else {
+					everyone = "deny-all"
 				}
+				response.Reservations = append(response.Reservations, &apiAccountReservation{
+					Topic:    r.TopicPattern,
+					Everyone: everyone,
+				})
 			}
 		}
 	} else {
@@ -356,9 +367,13 @@ func (s *Server) handleAccountAccessDelete(w http.ResponseWriter, r *http.Reques
 	if !topicRegex.MatchString(topic) {
 		return errHTTPBadRequestTopicInvalid
 	}
+	reservations, err := s.userManager.Reservations(v.user.Name) // FIXME replace with HasReservation
+	if err != nil {
+		return err
+	}
 	authorized := false
-	for _, grant := range v.user.Grants {
-		if grant.TopicPattern == topic && grant.Owner {
+	for _, r := range reservations {
+		if r.TopicPattern == topic {
 			authorized = true
 			break
 		}

+ 1 - 1
server/server_firebase_test.go

@@ -326,7 +326,7 @@ func TestMaybeTruncateFCMMessage_NotTooLong(t *testing.T) {
 func TestToFirebaseSender_Abuse(t *testing.T) {
 	sender := &testFirebaseSender{allowed: 2}
 	client := newFirebaseClient(sender, &testAuther{})
-	visitor := newVisitor(newTestConfig(t), newMemTestCache(t), netip.MustParseAddr("1.2.3.4"), nil)
+	visitor := newVisitor(newTestConfig(t), newMemTestCache(t), nil, netip.MustParseAddr("1.2.3.4"), nil)
 
 	require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"}))
 	require.Equal(t, 1, len(sender.Messages()))

+ 1 - 1
server/server_matrix_test.go

@@ -72,7 +72,7 @@ func TestMatrix_WriteMatrixDiscoveryResponse(t *testing.T) {
 func TestMatrix_WriteMatrixError(t *testing.T) {
 	w := httptest.NewRecorder()
 	r, _ := http.NewRequest("POST", "http://ntfy.example.com/_matrix/push/v1/notify", nil)
-	v := newVisitor(newTestConfig(t), nil, netip.MustParseAddr("1.2.3.4"), nil)
+	v := newVisitor(newTestConfig(t), nil, nil, netip.MustParseAddr("1.2.3.4"), nil)
 	require.Nil(t, writeMatrixError(w, r, v, &errMatrix{"https://ntfy.example.com/upABCDEFGHI?up=1", errHTTPBadRequestMatrixPushkeyBaseURLMismatch}))
 	require.Equal(t, 200, w.Result().StatusCode)
 	require.Equal(t, `{"rejected":["https://ntfy.example.com/upABCDEFGHI?up=1"]}`+"\n", w.Body.String())

+ 12 - 13
server/types.go

@@ -259,22 +259,21 @@ type apiAccountStats struct {
 	AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"`
 }
 
-type apiAccountGrant struct {
-	Topic string `json:"topic"`
-	Read  bool   `json:"read"`
-	Write bool   `json:"write"`
+type apiAccountReservation struct {
+	Topic    string `json:"topic"`
+	Everyone string `json:"everyone"`
 }
 
 type apiAccountResponse struct {
-	Username      string                  `json:"username"`
-	Role          string                  `json:"role,omitempty"`
-	Language      string                  `json:"language,omitempty"`
-	Notification  *user.NotificationPrefs `json:"notification,omitempty"`
-	Subscriptions []*user.Subscription    `json:"subscriptions,omitempty"`
-	Access        []*apiAccountGrant      `json:"access,omitempty"`
-	Plan          *apiAccountPlan         `json:"plan,omitempty"`
-	Limits        *apiAccountLimits       `json:"limits,omitempty"`
-	Stats         *apiAccountStats        `json:"stats,omitempty"`
+	Username      string                   `json:"username"`
+	Role          string                   `json:"role,omitempty"`
+	Language      string                   `json:"language,omitempty"`
+	Notification  *user.NotificationPrefs  `json:"notification,omitempty"`
+	Subscriptions []*user.Subscription     `json:"subscriptions,omitempty"`
+	Reservations  []*apiAccountReservation `json:"reservations,omitempty"`
+	Plan          *apiAccountPlan          `json:"plan,omitempty"`
+	Limits        *apiAccountLimits        `json:"limits,omitempty"`
+	Stats         *apiAccountStats         `json:"stats,omitempty"`
 }
 
 type apiAccountAccessRequest struct {

+ 9 - 7
server/visitor.go

@@ -26,6 +26,7 @@ var (
 type visitor struct {
 	config              *Config
 	messageCache        *messageCache
+	userManager         *user.Manager // May be nil!
 	ip                  netip.Addr
 	user                *user.User
 	messages            int64         // Number of messages sent
@@ -57,7 +58,7 @@ type visitorInfo struct {
 	AttachmentFileSizeLimit      int64
 }
 
-func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *user.User) *visitor {
+func newVisitor(conf *Config, messageCache *messageCache, userManager *user.Manager, ip netip.Addr, user *user.User) *visitor {
 	var requestLimiter, emailsLimiter, accountLimiter *rate.Limiter
 	var messages, emails int64
 	if user != nil {
@@ -76,6 +77,7 @@ func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *u
 	return &visitor{
 		config:              conf,
 		messageCache:        messageCache,
+		userManager:         userManager, // May be nil!
 		ip:                  ip,
 		user:                user,
 		messages:            messages,
@@ -192,7 +194,7 @@ func (v *visitor) Info() (*visitorInfo, error) {
 		info.AttachmentTotalSizeLimit = v.config.VisitorAttachmentTotalSizeLimit
 		info.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit
 	}
-	var attachmentsBytesUsed int64
+	var attachmentsBytesUsed int64 // FIXME Maybe move this to endpoint?
 	var err error
 	if v.user != nil {
 		attachmentsBytesUsed, err = v.messageCache.AttachmentBytesUsedByUser(v.user.Name)
@@ -203,12 +205,12 @@ func (v *visitor) Info() (*visitorInfo, error) {
 		return nil, err
 	}
 	var topics int64
-	if v.user != nil {
-		for _, grant := range v.user.Grants {
-			if grant.Owner {
-				topics++
-			}
+	if v.user != nil && v.userManager != nil {
+		reservations, err := v.userManager.Reservations(v.user.Name) // FIXME dup call, move this to endpoint?
+		if err != nil {
+			return nil, err
 		}
+		topics = int64(len(reservations))
 	}
 	info.Messages = messages
 	info.MessagesRemaining = zeroIfNegative(info.MessagesLimit - info.Messages)

+ 46 - 15
user/manager.go

@@ -92,7 +92,7 @@ const (
 		SELECT read, write
 		FROM user_access a
 		JOIN user u ON u.id = a.user_id
-		WHERE (u.user = '*' OR u.user = ?) AND ? LIKE a.topic
+		WHERE (u.user = ? OR u.user = ?) AND ? LIKE a.topic
 		ORDER BY u.user DESC
 	`
 )
@@ -123,11 +123,19 @@ const (
 		DO UPDATE SET read=excluded.read, write=excluded.write, owner_user_id=excluded.owner_user_id
 	`
 	selectUserAccessQuery = `
-		SELECT topic, read, write, IIF(owner_user_id IS NOT NULL AND user_id = owner_user_id,1,0) AS owner
+		SELECT topic, read, write
 		FROM user_access 
 		WHERE user_id = (SELECT id FROM user WHERE user = ?) 
 		ORDER BY write DESC, read DESC, topic
 	`
+	selectUserReservationsQuery = `
+		SELECT a_user.topic, a_user.read, a_user.write, a_everyone.read AS everyone_read, a_everyone.write AS everyone_write
+		FROM user_access a_user
+		LEFT JOIN  user_access a_everyone ON a_user.topic = a_everyone.topic AND a_everyone.user_id = (SELECT id FROM user WHERE user = ?)
+		WHERE a_user.user_id = a_user.owner_user_id
+		  AND a_user.owner_user_id = (SELECT id FROM user WHERE user = ?)
+		ORDER BY a_user.topic
+	`
 	selectOtherAccessCountQuery = `
 		SELECT count(*)
 		FROM user_access
@@ -354,7 +362,7 @@ func (a *Manager) Authorize(user *User, topic string, perm Permission) error {
 	}
 	// Select the read/write permissions for this user/topic combo. The query may return two
 	// rows (one for everyone, and one for the user), but prioritizes the user.
-	rows, err := a.db.Query(selectTopicPermsQuery, username, topic)
+	rows, err := a.db.Query(selectTopicPermsQuery, Everyone, username, topic)
 	if err != nil {
 		return err
 	}
@@ -479,15 +487,10 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
 	} else if err := rows.Err(); err != nil {
 		return nil, err
 	}
-	grants, err := a.readGrants(username)
-	if err != nil {
-		return nil, err
-	}
 	user := &User{
-		Name:   username,
-		Hash:   hash,
-		Role:   Role(role),
-		Grants: grants,
+		Name: username,
+		Hash: hash,
+		Role: Role(role),
 		Stats: &Stats{
 			Messages: messages,
 			Emails:   emails,
@@ -513,7 +516,8 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
 	return user, nil
 }
 
-func (a *Manager) readGrants(username string) ([]Grant, error) {
+// Grants returns all user-specific access control entries
+func (a *Manager) Grants(username string) ([]Grant, error) {
 	rows, err := a.db.Query(selectUserAccessQuery, username)
 	if err != nil {
 		return nil, err
@@ -522,8 +526,8 @@ func (a *Manager) readGrants(username string) ([]Grant, error) {
 	grants := make([]Grant, 0)
 	for rows.Next() {
 		var topic string
-		var read, write, owner bool
-		if err := rows.Scan(&topic, &read, &write, &owner); err != nil {
+		var read, write bool
+		if err := rows.Scan(&topic, &read, &write); err != nil {
 			return nil, err
 		} else if err := rows.Err(); err != nil {
 			return nil, err
@@ -532,12 +536,39 @@ func (a *Manager) readGrants(username string) ([]Grant, error) {
 			TopicPattern: fromSQLWildcard(topic),
 			AllowRead:    read,
 			AllowWrite:   write,
-			Owner:        owner,
 		})
 	}
 	return grants, nil
 }
 
+// Reservations returns all user-owned topics, and the associated everyone-access
+func (a *Manager) Reservations(username string) ([]Reservation, error) {
+	rows, err := a.db.Query(selectUserReservationsQuery, Everyone, username)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	reservations := make([]Reservation, 0)
+	for rows.Next() {
+		var topic string
+		var read, write bool
+		var everyoneRead, everyoneWrite sql.NullBool
+		if err := rows.Scan(&topic, &read, &write, &everyoneRead, &everyoneWrite); err != nil {
+			return nil, err
+		} else if err := rows.Err(); err != nil {
+			return nil, err
+		}
+		reservations = append(reservations, Reservation{
+			TopicPattern:       topic,
+			AllowRead:          read,
+			AllowWrite:         write,
+			AllowEveryoneRead:  everyoneRead.Bool,  // false if null
+			AllowEveryoneWrite: everyoneWrite.Bool, // false if null
+		})
+	}
+	return reservations, nil
+}
+
 // ChangePassword changes a user's password
 func (a *Manager) ChangePassword(username, password string) error {
 	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)

+ 16 - 9
user/types.go

@@ -9,14 +9,13 @@ import (
 
 // User is a struct that represents a user
 type User struct {
-	Name   string
-	Hash   string // password hash (bcrypt)
-	Token  string // Only set if token was used to log in
-	Role   Role
-	Grants []Grant
-	Prefs  *Prefs
-	Plan   *Plan
-	Stats  *Stats
+	Name  string
+	Hash  string // password hash (bcrypt)
+	Token string // Only set if token was used to log in
+	Role  Role
+	Prefs *Prefs
+	Plan  *Plan
+	Stats *Stats
 }
 
 // Auther is an interface for authentication and authorization
@@ -91,7 +90,15 @@ type Grant struct {
 	TopicPattern string // May include wildcard (*)
 	AllowRead    bool
 	AllowWrite   bool
-	Owner        bool // This user owns this ACL entry
+}
+
+// Reservation is a struct that represents the ownership over a topic by a user
+type Reservation struct {
+	TopicPattern       string
+	AllowRead          bool
+	AllowWrite         bool
+	AllowEveryoneRead  bool
+	AllowEveryoneWrite bool
 }
 
 // Permission represents a read or write permission to a topic

+ 16 - 15
web/public/static/langs/en.json

@@ -239,21 +239,22 @@
   "prefs_users_dialog_button_save": "Save",
   "prefs_appearance_title": "Appearance",
   "prefs_appearance_language_title": "Language",
-  "prefs_access_title": "Reserved topics",
-  "prefs_access_description": "You may reserve topic names for personal use here, and define access to a topic for other users.",
-  "prefs_access_add_button": "Add reserved topic",
-  "prefs_access_edit_button": "Edit topic access",
-  "prefs_access_delete_button": "Reset topic access",
-  "prefs_access_table": "Reserved topics table",
-  "prefs_access_table_topic_header": "Topic",
-  "prefs_access_table_access_header": "Access",
-  "prefs_access_table_perms_private": "Only I can publish and subscribe",
-  "prefs_access_table_perms_public_read": "I can publish, everyone can subscribe",
-  "prefs_access_table_perms_public": "Everyone can publish and subscribe",
-  "prefs_access_dialog_title_add": "Reserve topic",
-  "prefs_access_dialog_title_edit": "Edit reserved topic",
-  "prefs_access_dialog_topic_label": "Topic",
-  "prefs_access_dialog_access_label": "Access",
+  "prefs_reservations_title": "Reserved topics",
+  "prefs_reservations_description": "You may reserve topic names for personal use here, and define access to a topic for other users.",
+  "prefs_reservations_add_button": "Add reserved topic",
+  "prefs_reservations_edit_button": "Edit topic access",
+  "prefs_reservations_delete_button": "Reset topic access",
+  "prefs_reservations_table": "Reserved topics table",
+  "prefs_reservations_table_topic_header": "Topic",
+  "prefs_reservations_table_access_header": "Access",
+  "prefs_reservations_table_everyone_deny_all": "Only I can publish and subscribe",
+  "prefs_reservations_table_everyone_read_only": "I can publish and subscribe, everyone can subscribe",
+  "prefs_reservations_table_everyone_write_only": "I can publish and subscribe, everyone can publish",
+  "prefs_reservations_table_everyone_read_write": "Everyone can publish and subscribe",
+  "prefs_reservations_dialog_title_add": "Reserve topic",
+  "prefs_reservations_dialog_title_edit": "Edit reserved topic",
+  "prefs_reservations_dialog_topic_label": "Topic",
+  "prefs_reservations_dialog_access_label": "Access",
   "priority_min": "min",
   "priority_low": "low",
   "priority_default": "default",

+ 59 - 33
web/src/components/Preferences.js

@@ -50,7 +50,7 @@ const Preferences = () => {
         <Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}>
             <Stack spacing={3}>
                 <Notifications/>
-                <Access/>
+                <Reservations/>
                 <Users/>
                 <Appearance/>
             </Stack>
@@ -476,7 +476,7 @@ const Language = () => {
     )
 };
 
-const Access = () => {
+const Reservations = () => {
     const { t } = useTranslation();
     const { account } = useOutletContext();
     const [dialogKey, setDialogKey] = useState(0);
@@ -506,19 +506,19 @@ const Access = () => {
     }
 
     return (
-        <Card sx={{ padding: 1 }} aria-label={t("prefs_access_title")}>
+        <Card sx={{ padding: 1 }} aria-label={t("prefs_reservations_title")}>
             <CardContent sx={{ paddingBottom: 1 }}>
                 <Typography variant="h5" sx={{marginBottom: 2}}>
-                    {t("prefs_access_title")}
+                    {t("prefs_reservations_title")}
                 </Typography>
                 <Paragraph>
-                    {t("prefs_access_description")}
+                    {t("prefs_reservations_description")}
                 </Paragraph>
-                {account.access.length > 0 && <AccessTable entries={account.access}/>}
+                {account.reservations.length > 0 && <ReservationsTable reservations={account.reservations}/>}
             </CardContent>
             <CardActions>
-                <Button onClick={handleAddClick}>{t("prefs_access_add_button")}</Button>
-                <AccessDialog
+                <Button onClick={handleAddClick}>{t("prefs_reservations_add_button")}</Button>
+                <ReservationsDialog
                     key={`accessAddDialog${dialogKey}`}
                     open={dialogOpen}
                     entry={null}
@@ -531,7 +531,7 @@ const Access = () => {
     );
 };
 
-const AccessTable = (props) => {
+const ReservationsTable = (props) => {
     const { t } = useTranslation();
     const [dialogKey, setDialogKey] = useState(0);
     const [dialogOpen, setDialogOpen] = useState(false);
@@ -557,37 +557,59 @@ const AccessTable = (props) => {
     };
 
     return (
-        <Table size="small" aria-label={t("prefs_access_table")}>
+        <Table size="small" aria-label={t("prefs_reservations_table")}>
             <TableHead>
                 <TableRow>
-                    <TableCell sx={{paddingLeft: 0}}>{t("prefs_access_table_topic_header")}</TableCell>
-                    <TableCell>{t("prefs_access_table_access_header")}</TableCell>
+                    <TableCell sx={{paddingLeft: 0}}>{t("prefs_reservations_table_topic_header")}</TableCell>
+                    <TableCell>{t("prefs_reservations_table_access_header")}</TableCell>
                     <TableCell/>
                 </TableRow>
             </TableHead>
             <TableBody>
-                {props.entries.map(entry => (
+                {props.reservations.map(reservation => (
                     <TableRow
-                        key={entry.topic}
+                        key={reservation.topic}
                         sx={{'&:last-child td, &:last-child th': {border: 0}}}
                     >
-                        <TableCell component="th" scope="row" sx={{paddingLeft: 0}} aria-label={t("prefs_access_table_topic_header")}>{entry.topic}</TableCell>
-                        <TableCell aria-label={t("prefs_access_table_access_header")}>
-                            <LockIcon fontSize="small" sx={{verticalAlign: "bottom", mr: 0.5}}/>
-                            {t("prefs_access_table_perms_private")}
+                        <TableCell component="th" scope="row" sx={{paddingLeft: 0}} aria-label={t("prefs_reservations_table_topic_header")}>{reservation.topic}</TableCell>
+                        <TableCell aria-label={t("prefs_reservations_table_access_header")}>
+                            {reservation.everyone === "read-write" &&
+                                <>
+                                    <Public fontSize="small" sx={{verticalAlign: "bottom", mr: 0.5}}/>
+                                    {t("prefs_reservations_table_everyone_read_write")}
+                                </>
+                            }
+                            {reservation.everyone === "read-only" &&
+                                <>
+                                    <PublicOff fontSize="small" sx={{verticalAlign: "bottom", mr: 0.5}}/>
+                                    {t("prefs_reservations_table_everyone_read_only")}
+                                </>
+                            }
+                            {reservation.everyone === "write-only" &&
+                                <>
+                                    <PublicOff fontSize="small" sx={{verticalAlign: "bottom", mr: 0.5}}/>
+                                    {t("prefs_reservations_table_everyone_write_only")}
+                                </>
+                            }
+                            {reservation.everyone === "deny-all" &&
+                                <>
+                                    <LockIcon fontSize="small" sx={{verticalAlign: "bottom", mr: 0.5}}/>
+                                    {t("prefs_reservations_table_everyone_deny_all")}
+                                </>
+                            }
                         </TableCell>
                         <TableCell align="right">
-                            <IconButton onClick={() => handleEditClick(entry)} aria-label={t("prefs_access_edit_button")}>
+                            <IconButton onClick={() => handleEditClick(reservation)} aria-label={t("prefs_reservations_edit_button")}>
                                 <EditIcon/>
                             </IconButton>
-                            <IconButton onClick={() => handleDeleteClick(entry)} aria-label={t("prefs_access_delete_button")}>
+                            <IconButton onClick={() => handleDeleteClick(reservation)} aria-label={t("prefs_reservations_delete_button")}>
                                 <CloseIcon/>
                             </IconButton>
                         </TableCell>
                     </TableRow>
                 ))}
             </TableBody>
-            <AccessDialog
+            <ReservationsDialog
                 key={`accessEditDialog${dialogKey}`}
                 open={dialogOpen}
                 entry={dialogEntry}
@@ -599,7 +621,7 @@ const AccessTable = (props) => {
     );
 };
 
-const AccessDialog = (props) => {
+const ReservationsDialog = (props) => {
     const { t } = useTranslation();
     const [topic, setTopic] = useState("");
     const [access, setAccess] = useState("private");
@@ -621,15 +643,15 @@ const AccessDialog = (props) => {
         }
     }, [editMode, props]);
     return (
-        <Dialog open={props.open} onClose={props.onCancel} maxWidth="xs" fullWidth fullScreen={fullScreen}>
-            <DialogTitle>{editMode ? t("prefs_access_dialog_title_edit") : t("prefs_access_dialog_title_add")}</DialogTitle>
+        <Dialog open={props.open} onClose={props.onCancel} maxWidth="sm" fullWidth fullScreen={fullScreen}>
+            <DialogTitle>{editMode ? t("prefs_reservations_dialog_title_edit") : t("prefs_reservations_dialog_title_add")}</DialogTitle>
             <DialogContent>
                 {!editMode && <TextField
                     autoFocus
                     margin="dense"
                     id="topic"
-                    label={t("prefs_access_dialog_topic_label")}
-                    aria-label={t("prefs_access_dialog_topic_label")}
+                    label={t("prefs_reservations_dialog_topic_label")}
+                    aria-label={t("prefs_reservations_dialog_topic_label")}
                     value={topic}
                     onChange={ev => setTopic(ev.target.value)}
                     type="url"
@@ -640,7 +662,7 @@ const AccessDialog = (props) => {
                     <Select
                         value={access}
                         onChange={(ev) => setAccess(ev.target.value)}
-                        aria-label={t("prefs_access_dialog_access_label")}
+                        aria-label={t("prefs_reservations_dialog_access_label")}
                         sx={{
                             marginTop: 1,
                             "& .MuiSelect-select": {
@@ -649,17 +671,21 @@ const AccessDialog = (props) => {
                             }
                         }}
                     >
-                        <MenuItem value="private">
+                        <MenuItem value="deny-all">
                             <ListItemIcon><LockIcon /></ListItemIcon>
-                            <ListItemText primary={t("prefs_access_table_perms_private")} />
+                            <ListItemText primary={t("prefs_reservations_table_everyone_deny_all")} />
+                        </MenuItem>
+                        <MenuItem value="read-only">
+                            <ListItemIcon><PublicOff /></ListItemIcon>
+                            <ListItemText primary={t("prefs_reservations_table_everyone_read_only")} />
                         </MenuItem>
-                        <MenuItem value="public-read">
+                        <MenuItem value="write-only">
                             <ListItemIcon><PublicOff /></ListItemIcon>
-                            <ListItemText primary={t("prefs_access_table_perms_public_read")} />
+                            <ListItemText primary={t("prefs_reservations_table_everyone_write_only")} />
                         </MenuItem>
-                        <MenuItem value="public">
+                        <MenuItem value="read-write">
                             <ListItemIcon><Public /></ListItemIcon>
-                            <ListItemText primary={t("prefs_access_table_perms_public")} />
+                            <ListItemText primary={t("prefs_reservations_table_everyone_read_write")} />
                         </MenuItem>
                     </Select>
                 </FormControl>