Disallow changing provisioned user and tokens

This commit is contained in:
binwiederhier
2025-08-05 09:59:23 +02:00
parent c4c4916bc8
commit bcfb50b35a
5 changed files with 58 additions and 28 deletions

View File

@@ -85,6 +85,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis
response.Username = u.Name response.Username = u.Name
response.Role = string(u.Role) response.Role = string(u.Role)
response.SyncTopic = u.SyncTopic response.SyncTopic = u.SyncTopic
response.Provisioned = u.Provisioned
if u.Prefs != nil { if u.Prefs != nil {
if u.Prefs.Language != nil { if u.Prefs.Language != nil {
response.Language = *u.Prefs.Language response.Language = *u.Prefs.Language
@@ -139,11 +140,12 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis
lastOrigin = t.LastOrigin.String() lastOrigin = t.LastOrigin.String()
} }
response.Tokens = append(response.Tokens, &apiAccountTokenResponse{ response.Tokens = append(response.Tokens, &apiAccountTokenResponse{
Token: t.Value, Token: t.Value,
Label: t.Label, Label: t.Label,
LastAccess: t.LastAccess.Unix(), LastAccess: t.LastAccess.Unix(),
LastOrigin: lastOrigin, LastOrigin: lastOrigin,
Expires: t.Expires.Unix(), Expires: t.Expires.Unix(),
Provisioned: t.Provisioned,
}) })
} }
} }

View File

@@ -360,11 +360,12 @@ type apiAccountTokenUpdateRequest struct {
} }
type apiAccountTokenResponse struct { type apiAccountTokenResponse struct {
Token string `json:"token"` Token string `json:"token"`
Label string `json:"label,omitempty"` Label string `json:"label,omitempty"`
LastAccess int64 `json:"last_access,omitempty"` LastAccess int64 `json:"last_access,omitempty"`
LastOrigin string `json:"last_origin,omitempty"` LastOrigin string `json:"last_origin,omitempty"`
Expires int64 `json:"expires,omitempty"` // Unix timestamp Expires int64 `json:"expires,omitempty"` // Unix timestamp
Provisioned bool `json:"provisioned,omitempty"` // True if this token was provisioned by the server config
} }
type apiAccountPhoneNumberVerifyRequest struct { type apiAccountPhoneNumberVerifyRequest struct {
@@ -426,6 +427,7 @@ type apiAccountResponse struct {
Username string `json:"username"` Username string `json:"username"`
Role string `json:"role,omitempty"` Role string `json:"role,omitempty"`
SyncTopic string `json:"sync_topic,omitempty"` SyncTopic string `json:"sync_topic,omitempty"`
Provisioned bool `json:"provisioned,omitempty"`
Language string `json:"language,omitempty"` Language string `json:"language,omitempty"`
Notification *user.NotificationPrefs `json:"notification,omitempty"` Notification *user.NotificationPrefs `json:"notification,omitempty"`
Subscriptions []*user.Subscription `json:"subscriptions,omitempty"` Subscriptions []*user.Subscription `json:"subscriptions,omitempty"`

View File

@@ -212,6 +212,7 @@
"account_basics_phone_numbers_dialog_check_verification_button": "Confirm code", "account_basics_phone_numbers_dialog_check_verification_button": "Confirm code",
"account_basics_phone_numbers_dialog_channel_sms": "SMS", "account_basics_phone_numbers_dialog_channel_sms": "SMS",
"account_basics_phone_numbers_dialog_channel_call": "Call", "account_basics_phone_numbers_dialog_channel_call": "Call",
"account_basics_cannot_edit_or_delete_provisioned_user": "A provisioned user cannot be edited or deleted from the web app",
"account_usage_title": "Usage", "account_usage_title": "Usage",
"account_usage_of_limit": "of {{limit}}", "account_usage_of_limit": "of {{limit}}",
"account_usage_unlimited": "Unlimited", "account_usage_unlimited": "Unlimited",
@@ -291,6 +292,7 @@
"account_tokens_table_current_session": "Current browser session", "account_tokens_table_current_session": "Current browser session",
"account_tokens_table_copied_to_clipboard": "Access token copied", "account_tokens_table_copied_to_clipboard": "Access token copied",
"account_tokens_table_cannot_delete_or_edit": "Cannot edit or delete current session token", "account_tokens_table_cannot_delete_or_edit": "Cannot edit or delete current session token",
"account_tokens_table_cannot_delete_or_edit_provisioned_token": "Cannot edit or delete provisioned token",
"account_tokens_table_create_token_button": "Create access token", "account_tokens_table_create_token_button": "Create access token",
"account_tokens_table_last_origin_tooltip": "From IP address {{ip}}, click to lookup", "account_tokens_table_last_origin_tooltip": "From IP address {{ip}}, click to lookup",
"account_tokens_dialog_title_create": "Create access token", "account_tokens_dialog_title_create": "Create access token",

View File

@@ -31,14 +31,6 @@ export class TopicReservedError extends Error {
} }
} }
export class ProvisionedUserPasswordError extends Error {
static CODE = 40905; // errHTTPConflictTopicReserved
constructor() {
super("Cannot change the password of a provisioned user");
}
}
export class AccountCreateLimitReachedError extends Error { export class AccountCreateLimitReachedError extends Error {
static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation

View File

@@ -100,15 +100,13 @@ const Username = () => {
<Pref labelId={labelId} title={t("account_basics_username_title")} description={t("account_basics_username_description")}> <Pref labelId={labelId} title={t("account_basics_username_title")} description={t("account_basics_username_description")}>
<div aria-labelledby={labelId}> <div aria-labelledby={labelId}>
{session.username()} {session.username()}
{account?.role === Role.ADMIN ? ( {account?.role === Role.ADMIN && (
<> <>
{" "} {" "}
<Tooltip title={t("account_basics_username_admin_tooltip")}> <Tooltip title={t("account_basics_username_admin_tooltip")}>
<span style={{ cursor: "default" }}>👑</span> <span style={{ cursor: "default" }}>👑</span>
</Tooltip> </Tooltip>
</> </>
) : (
""
)} )}
</div> </div>
</Pref> </Pref>
@@ -119,6 +117,7 @@ const ChangePassword = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0); const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const { account } = useContext(AccountContext);
const labelId = "prefChangePassword"; const labelId = "prefChangePassword";
const handleDialogOpen = () => { const handleDialogOpen = () => {
@@ -136,9 +135,19 @@ const ChangePassword = () => {
<Typography color="gray" sx={{ float: "left", fontSize: "0.7rem", lineHeight: "3.5" }}> <Typography color="gray" sx={{ float: "left", fontSize: "0.7rem", lineHeight: "3.5" }}>
</Typography> </Typography>
<IconButton onClick={handleDialogOpen} aria-label={t("account_basics_password_description")}> {!account?.provisioned ? (
<EditIcon /> <IconButton onClick={handleDialogOpen} aria-label={t("account_basics_password_description")}>
</IconButton> <EditIcon />
</IconButton>
) : (
<Tooltip title={t("account_basics_cannot_edit_or_delete_provisioned_user")}>
<span>
<IconButton disabled>
<EditIcon />
</IconButton>
</span>
</Tooltip>
)}
</div> </div>
<ChangePasswordDialog key={`changePasswordDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} /> <ChangePasswordDialog key={`changePasswordDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} />
</Pref> </Pref>
@@ -888,7 +897,7 @@ const TokensTable = (props) => {
</div> </div>
</TableCell> </TableCell>
<TableCell align="right" sx={{ whiteSpace: "nowrap" }}> <TableCell align="right" sx={{ whiteSpace: "nowrap" }}>
{token.token !== session.token() && ( {token.token !== session.token() && !token.provisioned && (
<> <>
<IconButton onClick={() => handleEditClick(token)} aria-label={t("account_tokens_dialog_title_edit")}> <IconButton onClick={() => handleEditClick(token)} aria-label={t("account_tokens_dialog_title_edit")}>
<EditIcon /> <EditIcon />
@@ -910,6 +919,18 @@ const TokensTable = (props) => {
</span> </span>
</Tooltip> </Tooltip>
)} )}
{token.provisioned && (
<Tooltip title={t("account_tokens_table_cannot_delete_or_edit_provisioned_token")}>
<span>
<IconButton disabled>
<EditIcon />
</IconButton>
<IconButton disabled>
<CloseIcon />
</IconButton>
</span>
</Tooltip>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
@@ -1048,6 +1069,7 @@ const DeleteAccount = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0); const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const { account } = useContext(AccountContext);
const handleDialogOpen = () => { const handleDialogOpen = () => {
setDialogKey((prev) => prev + 1); setDialogKey((prev) => prev + 1);
@@ -1061,9 +1083,19 @@ const DeleteAccount = () => {
return ( return (
<Pref title={t("account_delete_title")} description={t("account_delete_description")}> <Pref title={t("account_delete_title")} description={t("account_delete_description")}>
<div> <div>
<Button fullWidth={false} variant="outlined" color="error" startIcon={<DeleteOutlineIcon />} onClick={handleDialogOpen}> {!account?.provisioned ? (
{t("account_delete_title")} <Button fullWidth={false} variant="outlined" color="error" startIcon={<DeleteOutlineIcon />} onClick={handleDialogOpen}>
</Button> {t("account_delete_title")}
</Button>
) : (
<Tooltip title={t("account_basics_cannot_edit_or_delete_provisioned_user")}>
<span>
<Button fullWidth={false} variant="outlined" color="error" startIcon={<DeleteOutlineIcon />} disabled>
{t("account_delete_title")}
</Button>
</span>
</Tooltip>
)}
</div> </div>
<DeleteAccountDialog key={`deleteAccountDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} /> <DeleteAccountDialog key={`deleteAccountDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} />
</Pref> </Pref>