mirror of
https://github.com/yingziwu/mastodon.git
synced 2026-02-04 03:25:14 +00:00
Merge tag 'v4.2.27'
This commit is contained in:
commit
336c698c79
11 changed files with 70 additions and 41 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -63,3 +63,6 @@ yarn-debug.log
|
|||
|
||||
# Ignore Docker option files
|
||||
docker-compose.override.yml
|
||||
|
||||
# Ignore dotenv .local files
|
||||
.env*.local
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.2.27] - 2025-10-13
|
||||
|
||||
### Security
|
||||
|
||||
- Update dependencies `rack` and `uri`
|
||||
- Fix streaming server connection not being closed on user suspension (by @ThisIsMissEm, [GHSA-r2fh-jr9c-9pxh](https://github.com/mastodon/mastodon/security/advisories/GHSA-r2fh-jr9c-9pxh))
|
||||
- Fix password change through admin CLI not invalidating existing sessions and access tokens (by @ThisIsMissEm, [GHSA-f3q3-rmf7-9655](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q3-rmf7-9655))
|
||||
- Fix streaming server allowing access to public timelines even without the `read` or `read:statuses` OAuth scopes (by @ThisIsMissEm, [GHSA-7gwh-mw97-qjgp](https://github.com/mastodon/mastodon/security/advisories/GHSA-7gwh-mw97-qjgp))
|
||||
|
||||
## [4.2.26] - 2025-09-23
|
||||
|
||||
### Security
|
||||
|
|
|
|||
|
|
@ -502,7 +502,7 @@ GEM
|
|||
validate_email
|
||||
validate_url
|
||||
webfinger (~> 1.2)
|
||||
openssl (3.1.0)
|
||||
openssl (3.1.2)
|
||||
openssl-signature_algorithm (1.3.0)
|
||||
openssl (> 2.0)
|
||||
orm_adapter (0.5.0)
|
||||
|
|
@ -533,7 +533,7 @@ GEM
|
|||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.17)
|
||||
rack (2.2.20)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (2.0.2)
|
||||
|
|
@ -769,7 +769,7 @@ GEM
|
|||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (2.4.2)
|
||||
uri (0.12.4)
|
||||
uri (0.12.5)
|
||||
validate_email (0.1.6)
|
||||
activemodel (>= 3.0)
|
||||
mail (>= 2.2.5)
|
||||
|
|
|
|||
|
|
@ -254,6 +254,10 @@ class Account < ApplicationRecord
|
|||
update!(suspended_at: date, suspension_origin: origin)
|
||||
create_canonical_email_block! if block_email
|
||||
end
|
||||
|
||||
# This terminates all connections for the given account with the streaming
|
||||
# server:
|
||||
redis.publish("timeline:system:#{id}", Oj.dump(event: :kill)) if local?
|
||||
end
|
||||
|
||||
def unsuspend!
|
||||
|
|
|
|||
|
|
@ -180,6 +180,10 @@ class User < ApplicationRecord
|
|||
|
||||
def disable!
|
||||
update!(disabled: true)
|
||||
|
||||
# This terminates all connections for the given account with the streaming
|
||||
# server:
|
||||
redis.publish("timeline:system:#{account.id}", Oj.dump(event: :kill))
|
||||
end
|
||||
|
||||
def enable!
|
||||
|
|
@ -382,17 +386,22 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def reset_password!
|
||||
# First, change password to something random, this revokes sessions and on-going access:
|
||||
change_password!(SecureRandom.hex)
|
||||
|
||||
# Finally, send a reset password prompt to the user
|
||||
send_reset_password_instructions
|
||||
end
|
||||
|
||||
def change_password!(new_password)
|
||||
# First, change password to something random and deactivate all sessions
|
||||
transaction do
|
||||
update(password: SecureRandom.hex)
|
||||
update(password: new_password)
|
||||
session_activations.destroy_all
|
||||
end
|
||||
|
||||
# Then, remove all authorized applications and connected push subscriptions
|
||||
revoke_access!
|
||||
|
||||
# Finally, send a reset password prompt to the user
|
||||
send_reset_password_instructions
|
||||
end
|
||||
|
||||
protected
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ services:
|
|||
|
||||
web:
|
||||
build: .
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.26
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.27
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
||||
|
|
@ -77,7 +77,7 @@ services:
|
|||
|
||||
streaming:
|
||||
build: .
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.26
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.27
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: node ./streaming
|
||||
|
|
@ -95,7 +95,7 @@ services:
|
|||
|
||||
sidekiq:
|
||||
build: .
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.26
|
||||
image: ghcr.io/mastodon/mastodon:v4.2.27
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec sidekiq
|
||||
|
|
|
|||
|
|
@ -170,14 +170,17 @@ module Mastodon::CLI
|
|||
user.role_id = nil
|
||||
end
|
||||
|
||||
password = SecureRandom.hex if options[:reset_password]
|
||||
user.password = password if options[:reset_password]
|
||||
user.email = options[:email] if options[:email]
|
||||
user.disabled = false if options[:enable]
|
||||
user.disabled = true if options[:disable]
|
||||
user.approved = true if options[:approve]
|
||||
user.otp_required_for_login = false if options[:disable_2fa]
|
||||
|
||||
# Password changes are a little different, as we also need to ensure
|
||||
# sessions, subscriptions, and access tokens are revoked after changing:
|
||||
password = SecureRandom.hex if options[:reset_password]
|
||||
user.change_password!(password) if options[:reset_password]
|
||||
|
||||
if user.save
|
||||
user.confirm if options[:confirm]
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ module Mastodon
|
|||
end
|
||||
|
||||
def patch
|
||||
26
|
||||
27
|
||||
end
|
||||
|
||||
def default_prerelease
|
||||
|
|
|
|||
|
|
@ -314,12 +314,20 @@ describe Mastodon::CLI::Accounts do
|
|||
context 'with --reset-password option' do
|
||||
let(:options) { { reset_password: true } }
|
||||
|
||||
it 'returns a new password for the user' do
|
||||
allow(SecureRandom).to receive(:hex).and_return('new_password')
|
||||
let(:user) { Fabricate(:user, password: original_password) }
|
||||
let(:original_password) { 'foobar12345' }
|
||||
let(:new_password) { 'new_password12345' }
|
||||
|
||||
expect { cli.invoke(:modify, arguments, options) }.to output(
|
||||
a_string_including('new_password')
|
||||
).to_stdout
|
||||
it 'returns a new password for the user' do
|
||||
allow(SecureRandom).to receive(:hex).and_return(new_password)
|
||||
allow(Account).to receive(:find_local).and_return(user.account)
|
||||
allow(user).to receive(:change_password!).and_call_original
|
||||
|
||||
expect { cli.invoke(:modify, arguments, options) }
|
||||
.to output(a_string_including(new_password)).to_stdout
|
||||
|
||||
expect(user).to have_received(:change_password!).with(new_password)
|
||||
expect(user.reload).to_not be_external_or_valid_password(original_password)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -448,12 +448,15 @@ RSpec.describe User do
|
|||
|
||||
let(:current_sign_in_at) { Time.zone.now }
|
||||
|
||||
before do
|
||||
user.disable!
|
||||
end
|
||||
|
||||
it 'disables user' do
|
||||
allow(redis).to receive(:publish)
|
||||
|
||||
user.disable!
|
||||
|
||||
expect(user).to have_attributes(disabled: true)
|
||||
|
||||
expect(redis)
|
||||
.to have_received(:publish).with("timeline:system:#{user.account.id}", Oj.dump(event: :kill)).once
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -152,17 +152,6 @@ const redisConfigFromEnv = (env) => {
|
|||
};
|
||||
};
|
||||
|
||||
const PUBLIC_CHANNELS = [
|
||||
'public',
|
||||
'public:media',
|
||||
'public:local',
|
||||
'public:local:media',
|
||||
'public:remote',
|
||||
'public:remote:media',
|
||||
'hashtag',
|
||||
'hashtag:local',
|
||||
];
|
||||
|
||||
// Used for priming the counters/gauges for the various metrics that are
|
||||
// per-channel
|
||||
const CHANNEL_NAMES = [
|
||||
|
|
@ -171,7 +160,14 @@ const CHANNEL_NAMES = [
|
|||
'user:notification',
|
||||
'list',
|
||||
'direct',
|
||||
...PUBLIC_CHANNELS
|
||||
'public',
|
||||
'public:media',
|
||||
'public:local',
|
||||
'public:local:media',
|
||||
'public:remote',
|
||||
'public:remote:media',
|
||||
'hashtag',
|
||||
'hashtag:local'
|
||||
];
|
||||
|
||||
const startServer = async () => {
|
||||
|
|
@ -459,7 +455,7 @@ const startServer = async () => {
|
|||
return;
|
||||
}
|
||||
|
||||
client.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes, devices.device_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id LEFT OUTER JOIN devices ON oauth_access_tokens.id = devices.access_token_id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => {
|
||||
client.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes, devices.device_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id INNER JOIN accounts ON accounts.id = users.account_id LEFT OUTER JOIN devices ON oauth_access_tokens.id = devices.access_token_id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL AND users.disabled IS FALSE AND accounts.suspended_at IS NULL LIMIT 1', [token], (err, result) => {
|
||||
done();
|
||||
|
||||
if (err) {
|
||||
|
|
@ -548,12 +544,6 @@ const startServer = async () => {
|
|||
const checkScopes = (req, channelName) => new Promise((resolve, reject) => {
|
||||
log.silly(req.requestId, `Checking OAuth scopes for ${channelName}`);
|
||||
|
||||
// When accessing public channels, no scopes are needed
|
||||
if (PUBLIC_CHANNELS.includes(channelName)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// The `read` scope has the highest priority, if the token has it
|
||||
// then it can access all streams
|
||||
const requiredScopes = ['read'];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue