| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | <?php | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | namespace Tests\Auth; | 
					
						
							| 
									
										
										
										
											2019-11-17 21:26:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  | use BookStack\Auth\Role; | 
					
						
							|  |  |  | use BookStack\Auth\User; | 
					
						
							| 
									
										
										
										
											2020-04-04 08:16:05 +08:00
										 |  |  | use Tests\TestCase; | 
					
						
							| 
									
										
										
										
											2019-11-17 21:26:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-22 21:17:14 +08:00
										 |  |  | class Saml2Test extends TestCase | 
					
						
							| 
									
										
										
										
											2019-11-17 21:26:43 +08:00
										 |  |  | { | 
					
						
							| 
									
										
										
										
											2021-10-31 04:29:59 +08:00
										 |  |  |     protected function setUp(): void | 
					
						
							| 
									
										
										
										
											2019-11-17 21:26:43 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         parent::setUp(); | 
					
						
							|  |  |  |         // Set default config for SAML2
 | 
					
						
							|  |  |  |         config()->set([ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'auth.method'                                   => 'saml2', | 
					
						
							|  |  |  |             'auth.defaults.guard'                           => 'saml2', | 
					
						
							|  |  |  |             'saml2.name'                                    => 'SingleSignOn-Testing', | 
					
						
							|  |  |  |             'saml2.email_attribute'                         => 'email', | 
					
						
							|  |  |  |             'saml2.display_name_attributes'                 => ['first_name', 'last_name'], | 
					
						
							|  |  |  |             'saml2.external_id_attribute'                   => 'uid', | 
					
						
							|  |  |  |             'saml2.user_to_groups'                          => false, | 
					
						
							|  |  |  |             'saml2.group_attribute'                         => 'user_groups', | 
					
						
							|  |  |  |             'saml2.remove_from_groups'                      => false, | 
					
						
							|  |  |  |             'saml2.onelogin_overrides'                      => null, | 
					
						
							|  |  |  |             'saml2.onelogin.idp.entityId'                   => 'http://saml.local/saml2/idp/metadata.php', | 
					
						
							|  |  |  |             'saml2.onelogin.idp.singleSignOnService.url'    => 'http://saml.local/saml2/idp/SSOService.php', | 
					
						
							|  |  |  |             'saml2.onelogin.idp.singleLogoutService.url'    => 'http://saml.local/saml2/idp/SingleLogoutService.php', | 
					
						
							|  |  |  |             'saml2.autoload_from_metadata'                  => false, | 
					
						
							|  |  |  |             'saml2.onelogin.idp.x509cert'                   => $this->testCert, | 
					
						
							|  |  |  |             'saml2.onelogin.debug'                          => false, | 
					
						
							| 
									
										
										
										
											2021-05-08 20:07:25 +08:00
										 |  |  |             'saml2.onelogin.security.requestedAuthnContext' => true, | 
					
						
							| 
									
										
										
										
											2019-11-17 21:26:43 +08:00
										 |  |  |         ]); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_metadata_endpoint_displays_xml_as_expected() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $req = $this->get('/saml2/metadata'); | 
					
						
							|  |  |  |         $req->assertHeader('Content-Type', 'text/xml; charset=UTF-8'); | 
					
						
							|  |  |  |         $req->assertSee('md:EntityDescriptor'); | 
					
						
							|  |  |  |         $req->assertSee(url('/saml2/acs')); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_onelogin_overrides_functions_as_expected() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $json = '{"sp": {"assertionConsumerService": {"url": "https://example.com/super-cats"}}, "contactPerson": {"technical": {"givenName": "Barry Scott", "emailAddress": "barry@example.com"}}}'; | 
					
						
							|  |  |  |         config()->set(['saml2.onelogin_overrides' => $json]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $req = $this->get('/saml2/metadata'); | 
					
						
							|  |  |  |         $req->assertSee('https://example.com/super-cats'); | 
					
						
							|  |  |  |         $req->assertSee('md:ContactPerson'); | 
					
						
							| 
									
										
										
										
											2021-10-27 05:04:18 +08:00
										 |  |  |         $req->assertSee('<md:GivenName>Barry Scott</md:GivenName>', false); | 
					
						
							| 
									
										
										
										
											2019-11-17 21:26:43 +08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     public function test_login_option_shows_on_login_page() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $req = $this->get('/login'); | 
					
						
							|  |  |  |         $req->assertSeeText('SingleSignOn-Testing'); | 
					
						
							| 
									
										
										
										
											2020-02-02 20:00:41 +08:00
										 |  |  |         $req->assertElementExists('form[action$="/saml2/login"][method=POST] button'); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_login() | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2020-02-02 20:00:41 +08:00
										 |  |  |         $req = $this->post('/saml2/login'); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |         $redirect = $req->headers->get('location'); | 
					
						
							|  |  |  |         $this->assertStringStartsWith('http://saml.local/saml2/idp/SSOService.php', $redirect, 'Login redirects to SSO location'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         config()->set(['saml2.onelogin.strict' => false]); | 
					
						
							|  |  |  |         $this->assertFalse($this->isAuthenticated()); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $acsPost = $this->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]); | 
					
						
							|  |  |  |         $redirect = $acsPost->headers->get('Location'); | 
					
						
							|  |  |  |         $acsId = explode('?id=', $redirect)[1]; | 
					
						
							|  |  |  |         $this->assertTrue(strlen($acsId) > 12); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $this->assertStringContainsString('/saml2/acs?id=', $redirect); | 
					
						
							|  |  |  |         $this->assertTrue(cache()->has('saml2_acs:' . $acsId)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $acsGet = $this->get($redirect); | 
					
						
							|  |  |  |         $acsGet->assertRedirect('/'); | 
					
						
							|  |  |  |         $this->assertFalse(cache()->has('saml2_acs:' . $acsId)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $this->assertTrue($this->isAuthenticated()); | 
					
						
							|  |  |  |         $this->assertDatabaseHas('users', [ | 
					
						
							|  |  |  |             'email'            => 'user@example.com', | 
					
						
							|  |  |  |             'external_auth_id' => 'user', | 
					
						
							|  |  |  |             'email_confirmed'  => false, | 
					
						
							|  |  |  |             'name'             => 'Barry Scott', | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_acs_process_id_randomly_generated() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $acsPost = $this->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]); | 
					
						
							|  |  |  |         $redirectA = $acsPost->headers->get('Location'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $acsPost = $this->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]); | 
					
						
							|  |  |  |         $redirectB = $acsPost->headers->get('Location'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $this->assertFalse($redirectA === $redirectB); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_process_acs_endpoint_cant_be_called_with_invalid_id() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $resp = $this->get('/saml2/acs'); | 
					
						
							|  |  |  |         $resp->assertRedirect('/login'); | 
					
						
							|  |  |  |         $this->followRedirects($resp)->assertSeeText('Login using SingleSignOn-Testing failed, system did not provide successful authorization'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $resp = $this->get('/saml2/acs?id=abc123'); | 
					
						
							|  |  |  |         $resp->assertRedirect('/login'); | 
					
						
							|  |  |  |         $this->followRedirects($resp)->assertSeeText('Login using SingleSignOn-Testing failed, system did not provide successful authorization'); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_group_role_sync_on_login() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         config()->set([ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'saml2.onelogin.strict'    => false, | 
					
						
							|  |  |  |             'saml2.user_to_groups'     => true, | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |             'saml2.remove_from_groups' => false, | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-31 04:29:59 +08:00
										 |  |  |         $memberRole = Role::factory()->create(['external_auth_id' => 'member']); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |         $adminRole = Role::getSystemRole('admin'); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]); | 
					
						
							|  |  |  |         $user = User::query()->where('external_auth_id', '=', 'user')->first(); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $userRoleIds = $user->roles()->pluck('id'); | 
					
						
							|  |  |  |         $this->assertContains($memberRole->id, $userRoleIds, 'User was assigned to member role'); | 
					
						
							|  |  |  |         $this->assertContains($adminRole->id, $userRoleIds, 'User was assigned to admin role'); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_group_role_sync_removal_option_works_as_expected() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         config()->set([ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'saml2.onelogin.strict'    => false, | 
					
						
							|  |  |  |             'saml2.user_to_groups'     => true, | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |             'saml2.remove_from_groups' => true, | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]); | 
					
						
							|  |  |  |         $user = User::query()->where('external_auth_id', '=', 'user')->first(); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-31 04:29:59 +08:00
										 |  |  |         $randomRole = Role::factory()->create(['external_auth_id' => 'random']); | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $user->attachRole($randomRole); | 
					
						
							|  |  |  |         $this->assertContains($randomRole->id, $user->roles()->pluck('id')); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         auth()->logout(); | 
					
						
							|  |  |  |         $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]); | 
					
						
							|  |  |  |         $this->assertNotContains($randomRole->id, $user->roles()->pluck('id')); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-02 20:00:41 +08:00
										 |  |  |     public function test_logout_link_directs_to_saml_path() | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         config()->set([ | 
					
						
							|  |  |  |             'saml2.onelogin.strict' => false, | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-02 20:00:41 +08:00
										 |  |  |         $resp = $this->actingAs($this->getEditor())->get('/'); | 
					
						
							| 
									
										
										
										
											2021-11-15 05:13:24 +08:00
										 |  |  |         $resp->assertElementContains('form[action$="/saml2/logout"] button', 'Logout'); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_logout_sls_flow() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         config()->set([ | 
					
						
							|  |  |  |             'saml2.onelogin.strict' => false, | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $handleLogoutResponse = function () { | 
					
						
							|  |  |  |             $this->assertTrue($this->isAuthenticated()); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             $req = $this->get('/saml2/sls'); | 
					
						
							|  |  |  |             $req->assertRedirect('/'); | 
					
						
							|  |  |  |             $this->assertFalse($this->isAuthenticated()); | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-15 05:13:24 +08:00
										 |  |  |         $req = $this->post('/saml2/logout'); | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $redirect = $req->headers->get('location'); | 
					
						
							|  |  |  |         $this->assertStringStartsWith('http://saml.local/saml2/idp/SingleLogoutService.php', $redirect); | 
					
						
							|  |  |  |         $this->withGet(['SAMLResponse' => $this->sloResponseData], $handleLogoutResponse); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_logout_sls_flow_when_sls_not_configured() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         config()->set([ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'saml2.onelogin.strict'                      => false, | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |             'saml2.onelogin.idp.singleLogoutService.url' => null, | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]); | 
					
						
							|  |  |  |         $this->assertTrue($this->isAuthenticated()); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-15 05:13:24 +08:00
										 |  |  |         $req = $this->post('/saml2/logout'); | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $req->assertRedirect('/'); | 
					
						
							|  |  |  |         $this->assertFalse($this->isAuthenticated()); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_dump_user_details_option_works() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         config()->set([ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'saml2.onelogin.strict'   => false, | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |             'saml2.dump_user_details' => true, | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]); | 
					
						
							|  |  |  |         $acsPost->assertJsonStructure([ | 
					
						
							|  |  |  |             'id_from_idp', | 
					
						
							|  |  |  |             'attrs_from_idp'      => [], | 
					
						
							|  |  |  |             'attrs_after_parsing' => [], | 
					
						
							|  |  |  |         ]); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_saml_routes_are_only_active_if_saml_enabled() | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2020-02-02 20:00:41 +08:00
										 |  |  |         config()->set(['auth.method' => 'standard']); | 
					
						
							| 
									
										
										
										
											2021-11-15 05:13:24 +08:00
										 |  |  |         $getRoutes = ['/metadata', '/sls']; | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |         foreach ($getRoutes as $route) { | 
					
						
							|  |  |  |             $req = $this->get('/saml2' . $route); | 
					
						
							| 
									
										
										
										
											2020-02-02 21:10:21 +08:00
										 |  |  |             $this->assertPermissionError($req); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-15 05:13:24 +08:00
										 |  |  |         $postRoutes = ['/login', '/acs', '/logout']; | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |         foreach ($postRoutes as $route) { | 
					
						
							|  |  |  |             $req = $this->post('/saml2' . $route); | 
					
						
							| 
									
										
										
										
											2020-02-02 21:10:21 +08:00
										 |  |  |             $this->assertPermissionError($req); | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-02 21:10:21 +08:00
										 |  |  |     public function test_forgot_password_routes_inaccessible() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $resp = $this->get('/password/email'); | 
					
						
							|  |  |  |         $this->assertPermissionError($resp); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $resp = $this->post('/password/email'); | 
					
						
							|  |  |  |         $this->assertPermissionError($resp); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $resp = $this->get('/password/reset/abc123'); | 
					
						
							|  |  |  |         $this->assertPermissionError($resp); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $resp = $this->post('/password/reset'); | 
					
						
							|  |  |  |         $this->assertPermissionError($resp); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_standard_login_routes_inaccessible() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $resp = $this->post('/login'); | 
					
						
							|  |  |  |         $this->assertPermissionError($resp); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-15 05:13:24 +08:00
										 |  |  |         $resp = $this->post('/logout'); | 
					
						
							| 
									
										
										
										
											2020-02-02 21:10:21 +08:00
										 |  |  |         $this->assertPermissionError($resp); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_user_invite_routes_inaccessible() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $resp = $this->get('/register/invite/abc123'); | 
					
						
							|  |  |  |         $this->assertPermissionError($resp); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $resp = $this->post('/register/invite/abc123'); | 
					
						
							|  |  |  |         $this->assertPermissionError($resp); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_user_register_routes_inaccessible() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $resp = $this->get('/register'); | 
					
						
							|  |  |  |         $this->assertPermissionError($resp); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $resp = $this->post('/register'); | 
					
						
							|  |  |  |         $this->assertPermissionError($resp); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-03 01:31:00 +08:00
										 |  |  |     public function test_email_domain_restriction_active_on_new_saml_login() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $this->setSettings([ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'registration-restrict' => 'testing.com', | 
					
						
							| 
									
										
										
										
											2020-02-03 01:31:00 +08:00
										 |  |  |         ]); | 
					
						
							|  |  |  |         config()->set([ | 
					
						
							|  |  |  |             'saml2.onelogin.strict' => false, | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]); | 
					
						
							|  |  |  |         $acsPost->assertSeeText('That email domain does not have access to this application'); | 
					
						
							|  |  |  |         $this->assertFalse(auth()->check()); | 
					
						
							|  |  |  |         $this->assertDatabaseMissing('users', ['email' => 'user@example.com']); | 
					
						
							| 
									
										
										
										
											2020-02-03 01:31:00 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-05 00:54:50 +08:00
										 |  |  |     public function test_group_sync_functions_when_email_confirmation_required() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         setting()->put('registration-confirmation', 'true'); | 
					
						
							|  |  |  |         config()->set([ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'saml2.onelogin.strict'    => false, | 
					
						
							|  |  |  |             'saml2.user_to_groups'     => true, | 
					
						
							| 
									
										
										
										
											2020-08-05 00:54:50 +08:00
										 |  |  |             'saml2.remove_from_groups' => false, | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-31 04:29:59 +08:00
										 |  |  |         $memberRole = Role::factory()->create(['external_auth_id' => 'member']); | 
					
						
							| 
									
										
										
										
											2020-08-05 00:54:50 +08:00
										 |  |  |         $adminRole = Role::getSystemRole('admin'); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]); | 
					
						
							| 
									
										
										
										
											2020-09-06 00:26:48 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $this->assertEquals('http://localhost/register/confirm', url()->current()); | 
					
						
							|  |  |  |         $acsPost->assertSee('Please check your email and click the confirmation button to access BookStack.'); | 
					
						
							|  |  |  |         /** @var User $user */ | 
					
						
							|  |  |  |         $user = User::query()->where('external_auth_id', '=', 'user')->first(); | 
					
						
							| 
									
										
										
										
											2020-08-05 00:54:50 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $userRoleIds = $user->roles()->pluck('id'); | 
					
						
							|  |  |  |         $this->assertContains($memberRole->id, $userRoleIds, 'User was assigned to member role'); | 
					
						
							|  |  |  |         $this->assertContains($adminRole->id, $userRoleIds, 'User was assigned to admin role'); | 
					
						
							|  |  |  |         $this->assertFalse(boolval($user->email_confirmed), 'User email remains unconfirmed'); | 
					
						
							| 
									
										
										
										
											2020-08-05 00:54:50 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-08 04:18:59 +08:00
										 |  |  |         $this->assertNull(auth()->user()); | 
					
						
							| 
									
										
										
										
											2020-08-05 00:54:50 +08:00
										 |  |  |         $homeGet = $this->get('/'); | 
					
						
							| 
									
										
										
										
											2021-08-08 04:18:59 +08:00
										 |  |  |         $homeGet->assertRedirect('/login'); | 
					
						
							| 
									
										
										
										
											2020-08-05 00:54:50 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-26 23:43:06 +08:00
										 |  |  |     public function test_login_where_existing_non_saml_user_shows_warning() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $this->post('/saml2/login'); | 
					
						
							|  |  |  |         config()->set(['saml2.onelogin.strict' => false]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Make the user pre-existing in DB with different auth_id
 | 
					
						
							|  |  |  |         User::query()->forceCreate([ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'email'            => 'user@example.com', | 
					
						
							| 
									
										
										
										
											2020-09-26 23:43:06 +08:00
										 |  |  |             'external_auth_id' => 'old_system_user_id', | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |             'email_confirmed'  => false, | 
					
						
							|  |  |  |             'name'             => 'Barry Scott', | 
					
						
							| 
									
										
										
										
											2020-09-26 23:43:06 +08:00
										 |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 20:30:45 +08:00
										 |  |  |         $acsPost = $this->followingRedirects()->post('/saml2/acs', ['SAMLResponse' => $this->acsPostData]); | 
					
						
							|  |  |  |         $this->assertFalse($this->isAuthenticated()); | 
					
						
							|  |  |  |         $this->assertDatabaseHas('users', [ | 
					
						
							|  |  |  |             'email'            => 'user@example.com', | 
					
						
							|  |  |  |             'external_auth_id' => 'old_system_user_id', | 
					
						
							|  |  |  |         ]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $acsPost->assertSee('A user with the email user@example.com already exists but with different credentials'); | 
					
						
							| 
									
										
										
										
											2020-09-26 23:43:06 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-08 20:07:25 +08:00
										 |  |  |     public function test_login_request_contains_expected_default_authncontext() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $authReq = $this->getAuthnRequest(); | 
					
						
							|  |  |  |         $this->assertStringContainsString('samlp:RequestedAuthnContext Comparison="exact"', $authReq); | 
					
						
							|  |  |  |         $this->assertStringContainsString('<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>', $authReq); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_false_idp_authncontext_option_does_not_pass_authncontext_in_saml_request() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         config()->set(['saml2.onelogin.security.requestedAuthnContext' => false]); | 
					
						
							|  |  |  |         $authReq = $this->getAuthnRequest(); | 
					
						
							|  |  |  |         $this->assertStringNotContainsString('samlp:RequestedAuthnContext', $authReq); | 
					
						
							|  |  |  |         $this->assertStringNotContainsString('<saml:AuthnContextClassRef>', $authReq); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function test_array_idp_authncontext_option_passes_value_as_authncontextclassref_in_request() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         config()->set(['saml2.onelogin.security.requestedAuthnContext' => ['urn:federation:authentication:windows', 'urn:federation:authentication:linux']]); | 
					
						
							|  |  |  |         $authReq = $this->getAuthnRequest(); | 
					
						
							|  |  |  |         $this->assertStringContainsString('samlp:RequestedAuthnContext', $authReq); | 
					
						
							|  |  |  |         $this->assertStringContainsString('<saml:AuthnContextClassRef>urn:federation:authentication:windows</saml:AuthnContextClassRef>', $authReq); | 
					
						
							|  |  |  |         $this->assertStringContainsString('<saml:AuthnContextClassRef>urn:federation:authentication:linux</saml:AuthnContextClassRef>', $authReq); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     protected function getAuthnRequest(): string | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $req = $this->post('/saml2/login'); | 
					
						
							|  |  |  |         $location = $req->headers->get('Location'); | 
					
						
							|  |  |  |         $query = explode('?', $location)[1]; | 
					
						
							|  |  |  |         $params = []; | 
					
						
							|  |  |  |         parse_str($query, $params); | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-08 20:07:25 +08:00
										 |  |  |         return gzinflate(base64_decode($params['SAMLRequest'])); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |     protected function withGet(array $options, callable $callback) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         return $this->withGlobal($_GET, $options, $callback); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     protected function withGlobal(array &$global, array $options, callable $callback) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $original = []; | 
					
						
							|  |  |  |         foreach ($options as $key => $val) { | 
					
						
							|  |  |  |             $original[$key] = $global[$key] ?? null; | 
					
						
							|  |  |  |             $global[$key] = $val; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $callback(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         foreach ($options as $key => $val) { | 
					
						
							|  |  |  |             $val = $original[$key]; | 
					
						
							|  |  |  |             if ($val) { | 
					
						
							|  |  |  |                 $global[$key] = $val; | 
					
						
							|  |  |  |             } else { | 
					
						
							|  |  |  |                 unset($global[$key]); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * The post data for a callback for single-sign-in. | 
					
						
							|  |  |  |      * Provides the following attributes: | 
					
						
							|  |  |  |      * array:5 [ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * "uid" => array:1 [ | 
					
						
							|  |  |  |      * 0 => "user" | 
					
						
							|  |  |  |      * ] | 
					
						
							|  |  |  |      * "first_name" => array:1 [ | 
					
						
							|  |  |  |      * 0 => "Barry" | 
					
						
							|  |  |  |      * ] | 
					
						
							|  |  |  |      * "last_name" => array:1 [ | 
					
						
							|  |  |  |      * 0 => "Scott" | 
					
						
							|  |  |  |      * ] | 
					
						
							|  |  |  |      * "email" => array:1 [ | 
					
						
							|  |  |  |      * 0 => "user@example.com" | 
					
						
							|  |  |  |      * ] | 
					
						
							|  |  |  |      * "user_groups" => array:2 [ | 
					
						
							|  |  |  |      * 0 => "member" | 
					
						
							|  |  |  |      * 1 => "admin" | 
					
						
							|  |  |  |      * ] | 
					
						
							|  |  |  |      * ]. | 
					
						
							| 
									
										
										
										
											2019-11-18 03:15:37 +08:00
										 |  |  |      */ | 
					
						
							|  |  |  |     protected $acsPostData = 'PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNGRkNDU2NGRjNzk0MDYxZWYxYmFhMDQ2N2Q3OTAyOGNlZDNjZTU0YmVlIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxOS0xMS0xN1QxNzo1MzozOVoiIERlc3RpbmF0aW9uPSJodHRwOi8vYm9va3N0YWNrLmxvY2FsL3NhbWwyL2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl82YTBmNGYzOTkzMDQwZjE5ODdmZDM3MDY4YjUyOTYyMjlhZDUzNjFjIj48c2FtbDpJc3N1ZXI+aHR0cDovL3NhbWwubG9jYWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNfNGRkNDU2NGRjNzk0MDYxZWYxYmFhMDQ2N2Q3OTAyOGNlZDNjZTU0YmVlIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz48ZHM6RGlnZXN0VmFsdWU+dm1oL1M3NU5mK2crZWNESkN6QWJaV0tKVmx1ZzdCZnNDKzlhV05lSXJlUT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+dnJhZ0tKWHNjVm5UNjJFaEk3bGk4MERUWHNOTGJOc3lwNWZ2QnU4WjFYSEtFUVA3QWpPNkcxcVBwaGpWQ2dRMzd6TldVVTZvUytQeFA3UDlHeG5xL3hKejRUT3lHcHJ5N1RoK2pIcHc0YWVzQTdrTmp6VU51UmU2c1ltWTlrRXh2VjMvTmJRZjROMlM2Y2RhRHIzWFRodllVVDcxYzQwNVVHOFJpQjJaY3liWHIxZU1yWCtXUDBnU2Qrc0F2RExqTjBJc3pVWlVUNThadFpEVE1ya1ZGL0pIbFBFQ04vVW1sYVBBeitTcUJ4c25xTndZK1oxYUt3MnlqeFRlNnUxM09Kb29OOVN1REowNE0rK2F3RlY3NkI4cXEyTzMxa3FBbDJibm1wTGxtTWdRNFEraUlnL3dCc09abTV1clphOWJObDNLVEhtTVBXbFpkbWhsLzgvMy9IT1RxN2thWGs3cnlWRHRLcFlsZ3FUajNhRUpuL0dwM2o4SFp5MUVialRiOTRRT1ZQMG5IQzB1V2hCaE13TjdzVjFrUSsxU2NjUlpUZXJKSGlSVUQvR0srTVg3M0YrbzJVTFRIL1Z6Tm9SM2o4N2hOLzZ1UC9JeG5aM1RudGR1MFZPZS9ucEdVWjBSMG9SWFhwa2JTL2poNWk1ZjU0RXN4eXZ1VEM5NHdKaEM8L2RzOlNpZ25hdHVyZVZhbHVlPgo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlFYXpDQ0F0T2dBd0lCQWdJVWU3YTA4OENucjRpem1ybkJFbng1cTNIVE12WXdEUVlKS29aSWh2Y05BUUVMQlFBd1JURUxNQWtHQTFVRUJoTUNSMEl4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeElUQWZCZ05WQkFvTUdFbHVkR1Z5Ym1WMElGZHBaR2RwZEhNZ1VIUjVJRXgwWkRBZUZ3MHhPVEV4TVRZeE1qRTNNVFZhRncweU9URXhNVFV4TWpFM01UVmFNRVV4Q3pBSkJnTlZCQVlUQWtkQ01STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbGNtNWxkQ0JYYVdSbmFYUnpJRkIwZVNCTWRHUXdnZ0dpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCandBd2dnR0tBb0lCZ1FEekxlOUZmZHlwbFR4SHA0U3VROWdRdFpUM3QrU0RmdkVMNzJwcENmRlp3NytCNXM1Qi9UNzNhWHBvUTNTNTNwR0kxUklXQ2dlMmlDVVEydHptMjdhU05IMGl1OWFKWWNVUVovUklUcWQwYXl5RGtzMU5BMlBUM1RXNnQzbTdLVjVyZTRQME5iK1lEZXV5SGRreitqY010cG44Q21Cb1QwSCtza2hhMGhpcUlOa2prUlBpSHZMSFZHcCt0SFVFQS9JNm1ONGFCL1VFeFNUTHM3OU5zTFVmdGVxcXhlOSt0dmRVYVRveURQcmhQRmpPTnMrOU5LQ2t6SUM2dmN2N0o2QXR1S0c2bkVUK3pCOXlPV2d0R1lRaWZYcVFBMnk1ZEw4MUJCMHE1dU1hQkxTMnBxM2FQUGp6VTJGMytFeXNqeVNXVG5Da2ZrN0M1U3NDWFJ1OFErVTk1dHVucE5md2Y1b2xFNldhczQ4Tk1NK1B3VjdpQ05NUGtOemxscTZQQ2lNK1A4RHJNU2N6elVaWlFVU3Y2ZFN3UENvK1lTVmltRU0wT2czWEpUaU5oUTVBTmxhSW42Nkt3NWdmb0JmdWlYbXlJS2lTRHlBaURZbUZhZjQzOTV3V3dMa1RSK2N3OFdmamFIc3dLWlRvbW4xTVIzT0pzWTJVSjBlUkJZTStZU3NDQXdFQUFhTlRNRkV3SFFZRFZSME9CQllFRkltcDJDWUNHZmNiN3c5MUgvY1NoVENrWHdSL01COEdBMVVkSXdRWU1CYUFGSW1wMkNZQ0dmY2I3dzkxSC9jU2hUQ2tYd1IvTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0dCQUErZy9DN3VMOWxuK1crcUJrbkxXODFrb2pZZmxnUEsxSTFNSEl3bk12bC9aVEhYNGRSWEtEcms3S2NVcTFLanFhak5WNjZmMWNha3AwM0lpakJpTzBYaTFnWFVaWUxvQ2lOR1V5eXA5WGxvaUl5OVh3MlBpV25ydzAreVp5dlZzc2JlaFhYWUpsNFJpaEJqQld1bDlSNHdNWUxPVVNKRGUyV3hjVUJoSm54eU5ScytQMHhMU1FYNkIybjZueG9Ea280cDA3czhaS1hRa2VpWjJpd0ZkVHh6UmtHanRoTVV2NzA0bnpzVkdCVDBEQ1B0ZlNhTzVLSlpXMXJDczN5aU10aG5CeHE0cUVET1FKRklsKy9MRDcxS2JCOXZaY1c1SnVhdnpCRm1rS0dOcm8vNkcxSTdlbDQ2SVI0d2lqVHlORkNZVXVEOWR0aWduTm1wV3ROOE9XK3B0aUwvanRUeVNXdWtqeXMwcyt2TG44M0NWdmpCMGRKdFZBSVlPZ1hGZ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     protected $sloResponseData = 'fZHRa8IwEMb/lZJ3bdJa04a2MOYYglOY4sNe5JKms9gmpZfC/vxF3ZjC8OXgLvl938ddjtC1vVjZTzu6d429NaiDr641KC5PBRkHIyxgg8JAp1E4JbZPbysRTanoB+ussi25QR4TgKgH11hDguWiIIeawTxOaK1iPYt5XcczHUlJeVRlMklBJjOuM1qDVCTY6wE9WRAv5HHEUS8NOjDOjyjLJoxNGN+xVESpSNgHCRYaXWPAXaijc70IQ2ntyUPqNG2tgjY8Z45CbNFLmt8V7GxBNuuX1eZ1uT7EcZJKAE4TJhXPaMxlVlFffPKKJnXE5ryusoiU+VlMXJIN5Y/feXRn1VR92GkHFTiY9sc+D2+p/HqRrQM34n33bCsd7KEd9eMd4+W32I5KaUQSlleHP9Hwv6uX3w=='; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     protected $testCert = 'MIIEazCCAtOgAwIBAgIUe7a088Cnr4izmrnBEnx5q3HTMvYwDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xOTExMTYxMjE3MTVaFw0yOTExMTUxMjE3MTVaMEUxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDzLe9FfdyplTxHp4SuQ9gQtZT3t+SDfvEL72ppCfFZw7+B5s5B/T73aXpoQ3S53pGI1RIWCge2iCUQ2tzm27aSNH0iu9aJYcUQZ/RITqd0ayyDks1NA2PT3TW6t3m7KV5re4P0Nb+YDeuyHdkz+jcMtpn8CmBoT0H+skha0hiqINkjkRPiHvLHVGp+tHUEA/I6mN4aB/UExSTLs79NsLUfteqqxe9+tvdUaToyDPrhPFjONs+9NKCkzIC6vcv7J6AtuKG6nET+zB9yOWgtGYQifXqQA2y5dL81BB0q5uMaBLS2pq3aPPjzU2F3+EysjySWTnCkfk7C5SsCXRu8Q+U95tunpNfwf5olE6Was48NMM+PwV7iCNMPkNzllq6PCiM+P8DrMSczzUZZQUSv6dSwPCo+YSVimEM0Og3XJTiNhQ5ANlaIn66Kw5gfoBfuiXmyIKiSDyAiDYmFaf4395wWwLkTR+cw8WfjaHswKZTomn1MR3OJsY2UJ0eRBYM+YSsCAwEAAaNTMFEwHQYDVR0OBBYEFImp2CYCGfcb7w91H/cShTCkXwR/MB8GA1UdIwQYMBaAFImp2CYCGfcb7w91H/cShTCkXwR/MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggGBAA+g/C7uL9ln+W+qBknLW81kojYflgPK1I1MHIwnMvl/ZTHX4dRXKDrk7KcUq1KjqajNV66f1cakp03IijBiO0Xi1gXUZYLoCiNGUyyp9XloiIy9Xw2PiWnrw0+yZyvVssbehXXYJl4RihBjBWul9R4wMYLOUSJDe2WxcUBhJnxyNRs+P0xLSQX6B2n6nxoDko4p07s8ZKXQkeiZ2iwFdTxzRkGjthMUv704nzsVGBT0DCPtfSaO5KJZW1rCs3yiMthnBxq4qEDOQJFIl+/LD71KbB9vZcW5JuavzBFmkKGNro/6G1I7el46IR4wijTyNFCYUuD9dtignNmpWtN8OW+ptiL/jtTySWukjys0s+vLn83CVvjB0dJtVAIYOgXFdIuii66gczwwM/LGiOExJn0dTNzsJ/IYhpxL4FBEuP0pskY0o0aUlJ2LS2j+wSQTRKsBgMjyrUrekle2ODStStn3eabjIx0/FHlpFr0jNIm/oMP7kwjtUX4zaNe47QI4Gg=='; | 
					
						
							| 
									
										
										
										
											2019-11-17 21:26:43 +08:00
										 |  |  | } |