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.Role = string(u.Role)
response.SyncTopic = u.SyncTopic
response.Provisioned = u.Provisioned
if u.Prefs != nil {
if u.Prefs.Language != nil {
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()
}
response.Tokens = append(response.Tokens, &apiAccountTokenResponse{
Token: t.Value,
Label: t.Label,
LastAccess: t.LastAccess.Unix(),
LastOrigin: lastOrigin,
Expires: t.Expires.Unix(),
Token: t.Value,
Label: t.Label,
LastAccess: t.LastAccess.Unix(),
LastOrigin: lastOrigin,
Expires: t.Expires.Unix(),
Provisioned: t.Provisioned,
})
}
}

View File

@@ -360,11 +360,12 @@ type apiAccountTokenUpdateRequest struct {
}
type apiAccountTokenResponse struct {
Token string `json:"token"`
Label string `json:"label,omitempty"`
LastAccess int64 `json:"last_access,omitempty"`
LastOrigin string `json:"last_origin,omitempty"`
Expires int64 `json:"expires,omitempty"` // Unix timestamp
Token string `json:"token"`
Label string `json:"label,omitempty"`
LastAccess int64 `json:"last_access,omitempty"`
LastOrigin string `json:"last_origin,omitempty"`
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 {
@@ -426,6 +427,7 @@ type apiAccountResponse struct {
Username string `json:"username"`
Role string `json:"role,omitempty"`
SyncTopic string `json:"sync_topic,omitempty"`
Provisioned bool `json:"provisioned,omitempty"`
Language string `json:"language,omitempty"`
Notification *user.NotificationPrefs `json:"notification,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_channel_sms": "SMS",
"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_of_limit": "of {{limit}}",
"account_usage_unlimited": "Unlimited",
@@ -291,6 +292,7 @@
"account_tokens_table_current_session": "Current browser session",
"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_provisioned_token": "Cannot edit or delete provisioned 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_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 {
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")}>
<div aria-labelledby={labelId}>
{session.username()}
{account?.role === Role.ADMIN ? (
{account?.role === Role.ADMIN && (
<>
{" "}
<Tooltip title={t("account_basics_username_admin_tooltip")}>
<span style={{ cursor: "default" }}>👑</span>
</Tooltip>
</>
) : (
""
)}
</div>
</Pref>
@@ -119,6 +117,7 @@ const ChangePassword = () => {
const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
const { account } = useContext(AccountContext);
const labelId = "prefChangePassword";
const handleDialogOpen = () => {
@@ -136,9 +135,19 @@ const ChangePassword = () => {
<Typography color="gray" sx={{ float: "left", fontSize: "0.7rem", lineHeight: "3.5" }}>
</Typography>
<IconButton onClick={handleDialogOpen} aria-label={t("account_basics_password_description")}>
<EditIcon />
</IconButton>
{!account?.provisioned ? (
<IconButton onClick={handleDialogOpen} aria-label={t("account_basics_password_description")}>
<EditIcon />
</IconButton>
) : (
<Tooltip title={t("account_basics_cannot_edit_or_delete_provisioned_user")}>
<span>
<IconButton disabled>
<EditIcon />
</IconButton>
</span>
</Tooltip>
)}
</div>
<ChangePasswordDialog key={`changePasswordDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} />
</Pref>
@@ -888,7 +897,7 @@ const TokensTable = (props) => {
</div>
</TableCell>
<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")}>
<EditIcon />
@@ -910,6 +919,18 @@ const TokensTable = (props) => {
</span>
</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>
</TableRow>
))}
@@ -1048,6 +1069,7 @@ const DeleteAccount = () => {
const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
const { account } = useContext(AccountContext);
const handleDialogOpen = () => {
setDialogKey((prev) => prev + 1);
@@ -1061,9 +1083,19 @@ const DeleteAccount = () => {
return (
<Pref title={t("account_delete_title")} description={t("account_delete_description")}>
<div>
<Button fullWidth={false} variant="outlined" color="error" startIcon={<DeleteOutlineIcon />} onClick={handleDialogOpen}>
{t("account_delete_title")}
</Button>
{!account?.provisioned ? (
<Button fullWidth={false} variant="outlined" color="error" startIcon={<DeleteOutlineIcon />} onClick={handleDialogOpen}>
{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>
<DeleteAccountDialog key={`deleteAccountDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} />
</Pref>