Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
91.94% |
57 / 62 |
|
50.00% |
5 / 10 |
CRAP | |
0.00% |
0 / 1 |
NetworkSessionProvider | |
91.94% |
57 / 62 |
|
50.00% |
5 / 10 |
29.44 | |
0.00% |
0 / 1 |
__construct | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
5.05 | |||
provideSessionInfo | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
12 | |||
parseAuthorization | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 | |||
newSessionInfoForUser | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
1 | |||
getAllowedUserRights | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
canAlwaysAutocreate | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
persistsSessionId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
canChangeUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
persistSession | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
unpersistSession | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * SessionProvider based on configured ip address and secret token |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | */ |
22 | |
23 | namespace MediaWiki\Extension\NetworkSession; |
24 | |
25 | use InvalidArgumentException; |
26 | use MediaWiki\Request\WebRequest; |
27 | use MediaWiki\Session\SessionBackend; |
28 | use MediaWiki\Session\SessionInfo; |
29 | use MediaWiki\Session\SessionProvider; |
30 | use MediaWiki\Session\UserInfo; |
31 | use MediaWiki\WikiMap\WikiMap; |
32 | use Wikimedia\IPUtils; |
33 | |
34 | /** |
35 | * This provider allows requests from specific ip addresses or ip address |
36 | * ranges, when they contain a pre-shared secret, to perform api requests. |
37 | * The set of user rights available to logins through this provider can |
38 | * be limited, for example to only grant 'read'. |
39 | * |
40 | * This is intended to work with applications supporting a wiki farm |
41 | * allowing them to perform api requests without managing logins for |
42 | * wikis that don't default to world readable. |
43 | */ |
44 | class NetworkSessionProvider extends SessionProvider { |
45 | public const AUTH_HEADER = 'Authorization'; |
46 | public const AUTH_SCHEME = 'NetworkSession'; |
47 | public const CAN_ALWAYS_AUTOCREATE_CONFIG_KEY = 'NetworkSessionProviderCanAlwaysAutocreate'; |
48 | public const USERS_CONFIG_KEY = 'NetworkSessionProviderUsers'; |
49 | public const USERS_RIGHTS_CONFIG_KEY = 'NetworkSessionProviderAllowedUserRights'; |
50 | |
51 | /** @var bool Whether the current request is an API request. */ |
52 | private bool $isApiRequest; |
53 | |
54 | /** |
55 | * @param array $params Keys include: |
56 | * - priority: (required) Set the priority |
57 | * - isApiRequest: Whether the current request is an API request. Should be only set in tests. |
58 | */ |
59 | public function __construct( array $params ) { |
60 | if ( !isset( $params['priority'] ) ) { |
61 | throw new InvalidArgumentException( __METHOD__ . ': priority must be specified' ); |
62 | } |
63 | if ( $params['priority'] < SessionInfo::MIN_PRIORITY || |
64 | $params['priority'] > SessionInfo::MAX_PRIORITY |
65 | ) { |
66 | throw new InvalidArgumentException( __METHOD__ . ': Invalid priority' ); |
67 | } |
68 | $this->priority = $params['priority']; |
69 | |
70 | $this->isApiRequest = $params['isApiRequest'] |
71 | ?? ( defined( 'MW_API' ) || defined( 'MW_REST_API' ) ); |
72 | } |
73 | |
74 | /** |
75 | * Provide session info for a request |
76 | * |
77 | * @param WebRequest $request |
78 | * @return SessionInfo|null |
79 | */ |
80 | public function provideSessionInfo( WebRequest $request ) { |
81 | $providedToken = $this->parseAuthorization( $request->getHeader( self::AUTH_HEADER ) ); |
82 | if ( $providedToken === null ) { |
83 | return null; |
84 | } |
85 | |
86 | if ( !$this->isApiRequest ) { |
87 | return $this->makeException( 'networksession-only-api-request' ); |
88 | } |
89 | |
90 | if ( $request->getProtocol() !== 'https' ) { |
91 | return $this->makeException( 'networksession-only-https' ); |
92 | } |
93 | |
94 | $ip = $request->getIP(); |
95 | $users = $this->config->get( self::USERS_CONFIG_KEY ); |
96 | $matchedUser = null; |
97 | foreach ( $users as $config ) { |
98 | if ( !is_array( $config['ip_ranges'] ?? null ) ) { |
99 | return $this->makeException( 'networksession-invalid-config-ip-ranges' ); |
100 | } |
101 | if ( !is_string( $config['token'] ?? null ) ) { |
102 | return $this->makeException( 'networksession-invalid-config-token' ); |
103 | } |
104 | if ( !is_string( $config['username'] ?? null ) ) { |
105 | return $this->makeException( 'networksession-invalid-config-username' ); |
106 | } |
107 | |
108 | if ( !hash_equals( $config['token'], $providedToken ) ) { |
109 | continue; |
110 | } |
111 | if ( !IPUtils::isInRanges( $ip, $config['ip_ranges'] ) ) { |
112 | continue; |
113 | } |
114 | if ( $matchedUser !== null ) { |
115 | return $this->makeException( 'networksession-invalid-config-multiple-matches' ); |
116 | } |
117 | $matchedUser = $config; |
118 | } |
119 | |
120 | if ( $matchedUser ) { |
121 | return $this->newSessionInfoForUser( $matchedUser ); |
122 | } else { |
123 | return $this->makeException( 'networksession-no-token-match' ); |
124 | } |
125 | } |
126 | |
127 | private function parseAuthorization( ?string $authorization ): ?string { |
128 | if ( $authorization === null ) { |
129 | return null; |
130 | } |
131 | $parts = explode( ' ', $authorization, 2 ); |
132 | if ( count( $parts ) === 2 && strcasecmp( self::AUTH_SCHEME, $parts[0] ) === 0 ) { |
133 | return $parts[1]; |
134 | } |
135 | return null; |
136 | } |
137 | |
138 | private function newSessionInfoForUser( array $user ): SessionInfo { |
139 | $id = $this->hashToSessionId( implode( '\n', [ |
140 | WikiMap::getCurrentWikiId(), |
141 | $user['username'], |
142 | $user['token'], |
143 | ] ) ); |
144 | return new SessionInfo( $this->priority, [ |
145 | 'provider' => $this, |
146 | 'id' => $id, |
147 | 'idIsSafe' => true, |
148 | 'userInfo' => UserInfo::newFromName( $user['username'], true ), |
149 | 'persisted' => false, |
150 | 'forceUse' => true, |
151 | ] ); |
152 | } |
153 | |
154 | /** |
155 | * Fetch the rights allowed to the user when the specified session is active. |
156 | * |
157 | * @param SessionBackend $backend |
158 | * @return null|string[] Allowed user rights, or null to allow all. |
159 | */ |
160 | public function getAllowedUserRights( SessionBackend $backend ) { |
161 | if ( $backend->getProvider() !== $this ) { |
162 | throw new InvalidArgumentException( 'Backend\'s provider isn\'t $this' ); |
163 | } |
164 | return $this->config->get( self::USERS_RIGHTS_CONFIG_KEY ); |
165 | } |
166 | |
167 | /** |
168 | * Declares if this provider is providing system users or regular users |
169 | * |
170 | * @return bool |
171 | */ |
172 | public function canAlwaysAutocreate(): bool { |
173 | return $this->config->get( self::CAN_ALWAYS_AUTOCREATE_CONFIG_KEY ); |
174 | } |
175 | |
176 | /** |
177 | * Indicate whether self::persistSession() can save arbitrary session IDs |
178 | * |
179 | * @return bool |
180 | */ |
181 | public function persistsSessionId() { |
182 | // session id is calculated, not persisted |
183 | return false; |
184 | } |
185 | |
186 | /** |
187 | * Indicate whether the user associated with the request can be changed |
188 | * |
189 | * @return bool |
190 | */ |
191 | public function canChangeUser() { |
192 | return false; |
193 | } |
194 | |
195 | /** |
196 | * Persist a session into a request/response |
197 | * |
198 | * @param SessionBackend $session Session to persist |
199 | * @param WebRequest $request Request into which to persist the session |
200 | */ |
201 | public function persistSession( SessionBackend $session, WebRequest $request ) { |
202 | // Nothing to persist |
203 | } |
204 | |
205 | /** |
206 | * Remove any persisted session from a request/response |
207 | * |
208 | * @param WebRequest $request Request from which to remove any session data |
209 | */ |
210 | public function unpersistSession( WebRequest $request ) { |
211 | // Nothing to unpersist |
212 | } |
213 | } |