Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
CentralAuthSessionManager
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 8
462
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 makeSessionKey
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 makeTokenKey
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getSessionStore
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getTokenStore
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCentralSession
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getCentralSessionById
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 setCentralSession
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Extension\CentralAuth;
22
23use BagOStuff;
24use CachedBagOStuff;
25use IBufferingStatsdDataFactory;
26use MediaWiki\Config\ServiceOptions;
27use MediaWiki\Session\Session;
28use MediaWiki\Session\SessionManager;
29use MWCryptRand;
30use ObjectCache;
31use Wikimedia\Stats\StatsFactory;
32
33class CentralAuthSessionManager {
34    /**
35     * @internal Only public for service wiring use
36     */
37    public const CONSTRUCTOR_OPTIONS = [
38        'CentralAuthDatabase',
39        'CentralAuthSessionCacheType',
40        'SessionCacheType',
41    ];
42
43    /** @var BagOStuff|null Session cache */
44    private $sessionStore = null;
45
46    /** @var BagOStuff|null Token cache */
47    private $tokenStore = null;
48
49    /** @var ServiceOptions */
50    private $options;
51
52    /** @var IBufferingStatsdDataFactory */
53    private $statsdDataFactory;
54    private StatsFactory $statsFactory;
55
56    /**
57     * @param ServiceOptions $options
58     * @param IBufferingStatsdDataFactory $statsdDataFactory
59     * @param StatsFactory $statsFactory
60     * @param BagOStuff $microStash
61     */
62    public function __construct(
63        ServiceOptions $options,
64        IBufferingStatsdDataFactory $statsdDataFactory,
65        StatsFactory $statsFactory,
66        BagOStuff $microStash
67    ) {
68        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
69        $this->options = $options;
70        $this->statsdDataFactory = $statsdDataFactory;
71        $this->statsFactory = $statsFactory;
72        $this->tokenStore = $microStash;
73    }
74
75    /**
76     * @param string $keygroup
77     * @param string ...$components
78     * @return string The global session key (with proper escaping)
79     */
80    public function makeSessionKey( string $keygroup, ...$components ): string {
81        return $this->getSessionStore()->makeGlobalKey(
82            $keygroup, $this->options->get( 'CentralAuthDatabase' ), ...$components
83        );
84    }
85
86    /**
87     * @param string $keygroup
88     * @param string ...$components
89     * @return string The global token key (with proper escaping)
90     */
91    public function makeTokenKey( string $keygroup, ...$components ): string {
92        return $this->getTokenStore()->makeGlobalKey(
93            $keygroup, $this->options->get( 'CentralAuthDatabase' ), ...$components
94        );
95    }
96
97    /**
98     * Get a cache for storage of central sessions
99     * @return BagOStuff
100     */
101    public function getSessionStore(): BagOStuff {
102        if ( !$this->sessionStore ) {
103            $sessionCacheType = $this->options->get( 'CentralAuthSessionCacheType' )
104                ?? $this->options->get( 'SessionCacheType' );
105            $cache = ObjectCache::getInstance( $sessionCacheType );
106            $this->sessionStore = $cache instanceof CachedBagOStuff
107                ? $cache : new CachedBagOStuff( $cache );
108        }
109
110        return $this->sessionStore;
111    }
112
113    /**
114     * Get a cache for storage of temporary cross-site tokens
115     * @return BagOStuff
116     */
117    public function getTokenStore(): BagOStuff {
118        return $this->tokenStore;
119    }
120
121    /**
122     * Get the central session data
123     * @param Session|null $session
124     * @return array
125     */
126    public function getCentralSession( $session = null ) {
127        if ( !$session ) {
128            $session = SessionManager::getGlobalSession();
129        }
130        $id = $session->get( 'CentralAuth::centralSessionId' );
131
132        if ( $id !== null ) {
133            return $this->getCentralSessionById( $id );
134        } else {
135            return [];
136        }
137    }
138
139    /**
140     * Get the central session data
141     * @param string $id
142     * @return array
143     */
144    public function getCentralSessionById( $id ) {
145        $key = $this->makeSessionKey( 'session', $id );
146
147        $stime = microtime( true );
148        $data = $this->getSessionStore()->get( $key ) ?: [];
149        $real = microtime( true ) - $stime;
150
151        // Stay backward compatible with the dashboard feeding on
152        // this data. NOTE: $real is in second with microsecond-level
153        // precision. This is reconciled on the grafana dashboard.
154        $this->statsdDataFactory->timing( 'centralauth.session.read', $real );
155
156        $this->statsFactory->withComponent( 'CentralAuth' )
157            ->getTiming( 'session_read_seconds' )
158            ->observe( $real * 1000 );
159
160        return $data;
161    }
162
163    /**
164     * Set data in the central session
165     * @param array $data
166     * @param bool|string $reset Reset the session ID. If a string, this is the new ID.
167     * @param Session|null $session
168     * @return string|null Session ID
169     */
170    public function setCentralSession( array $data, $reset = false, $session = null ) {
171        $keepKeys = [ 'user' => true, 'token' => true, 'expiry' => true ];
172
173        $session ??= SessionManager::getGlobalSession();
174        $id = $session->get( 'CentralAuth::centralSessionId' );
175
176        if ( $reset || $id === null ) {
177            $id = is_string( $reset ) ? $reset : MWCryptRand::generateHex( 32 );
178        }
179        $data['sessionId'] = $id;
180
181        $sessionStore = $this->getSessionStore();
182        $key = $this->makeSessionKey( 'session', $id );
183
184        // Copy certain keys from the existing session, if any (T124821)
185        $existing = $sessionStore->get( $key );
186
187        if ( is_array( $existing ) ) {
188            $data += array_intersect_key( $existing, $keepKeys );
189        }
190
191        $isDirty = ( $data !== $existing );
192        if ( $isDirty || !isset( $data['expiry'] ) || $data['expiry'] < time() + 32100 ) {
193            $data['expiry'] = time() + $sessionStore::TTL_DAY;
194            $stime = microtime( true );
195            $sessionStore->set(
196                $key,
197                $data,
198                $sessionStore::TTL_DAY
199            );
200            $real = microtime( true ) - $stime;
201            // Stay backward compatible with the dashboard feeding on
202            // this data. NOTE: $real is in second with microsecond-level
203            // precision. This is reconciled on the grafana dashboard.
204            $this->statsdDataFactory->timing( 'centralauth.session.write', $real );
205
206            $this->statsFactory->withComponent( 'CentralAuth' )
207                ->getTiming( 'session_write_seconds' )
208                ->observe( $real * 1000 );
209        }
210
211        if ( $session ) {
212            $session->set( 'CentralAuth::centralSessionId', $id );
213        }
214
215        return $id;
216    }
217}