]> BookStack Code Mirror - bookstack/blob - app/Auth/Access/Saml2Service.php
0b6cbe80594b1669b2b77820a24d19010c75cee9
[bookstack] / app / Auth / Access / Saml2Service.php
1 <?php namespace BookStack\Auth\Access;
2
3 use BookStack\Auth\Access;
4 use BookStack\Auth\Role;
5 use BookStack\Auth\User;
6 use BookStack\Auth\UserRepo;
7 use BookStack\Exceptions\SamlException;
8 use Illuminate\Contracts\Auth\Authenticatable;
9 use Illuminate\Database\Eloquent\Builder;
10 use Illuminate\Support\Facades\Auth;
11 use Illuminate\Support\Facades\Log;
12
13
14 /**
15  * Class Saml2Service
16  * Handles any app-specific SAML tasks.
17  * @package BookStack\Services
18  */
19 class Saml2Service
20 {
21     protected $config;
22     protected $userRepo;
23     protected $user;
24     protected $enabled;
25
26     /**
27      * Saml2Service constructor.
28      * @param \BookStack\Auth\UserRepo $userRepo
29      */
30     public function __construct(UserRepo $userRepo, User $user)
31     {
32         $this->config = config('services.saml');
33         $this->userRepo = $userRepo;
34         $this->user = $user;
35         $this->enabled = config('saml2_settings.enabled') === true;
36     }
37
38     /**
39      * Check if groups should be synced.
40      * @return bool
41      */
42     public function shouldSyncGroups()
43     {
44         return $this->enabled && $this->config['user_to_groups'] !== false;
45     }
46
47     /**
48      * Extract the details of a user from a SAML response.
49      * @param $samlID
50      * @param $samlAttributes
51      * @return array
52      */
53     public function getUserDetails($samlID, $samlAttributes)
54     {
55         $emailAttr = $this->config['email_attribute'];
56         $displayNameAttr = $this->config['display_name_attribute'];
57         $userNameAttr = $this->config['user_name_attribute'];
58
59         $email = $this->getSamlResponseAttribute($samlAttributes, $emailAttr, null);
60
61         if ($userNameAttr === null) {
62           $userName = $samlID;
63         } else {
64           $userName = $this->getSamlResponseAttribute($samlAttributes, $userNameAttr, $samlID);
65         }
66
67         $displayName = [];
68         foreach ($displayNameAttr as $dnAttr) {
69           $dnComponent = $this->getSamlResponseAttribute($samlAttributes, $dnAttr, null);
70           if ($dnComponent !== null) {
71             $displayName[] = $dnComponent;
72           }
73         }
74
75         if (count($displayName) == 0) {
76           $displayName = $userName;
77         } else {
78           $displayName = implode(' ', $displayName);
79         }
80
81         return [
82             'uid'   => $userName,
83             'name'  => $displayName,
84             'dn'    => $samlID,
85             'email' => $email,
86         ];
87     }
88
89     /**
90      * Get the groups a user is a part of from the SAML response.
91      * @param array $samlAttributes
92      * @return array
93      */
94     public function getUserGroups($samlAttributes)
95     {
96         $groupsAttr = $this->config['group_attribute'];
97         $userGroups = $samlAttributes[$groupsAttr];
98
99         if (!is_array($userGroups)) {
100             $userGroups = [];
101         }
102
103         return $userGroups;
104     }
105
106     /**
107      * Get a property from an SAML response.
108      * Handles properties potentially being an array.
109      * @param array $userDetails
110      * @param string $propertyKey
111      * @param $defaultValue
112      * @return mixed
113      */
114     protected function getSamlResponseAttribute(array $samlAttributes, string $propertyKey, $defaultValue)
115     {
116         if (isset($samlAttributes[$propertyKey])) {
117             $data = $samlAttributes[$propertyKey];
118             if (!is_array($data)) {
119               return $data;
120             } else if (count($data) == 0) {
121               return $defaultValue;
122             } else if (count($data) == 1) {
123               return $data[0];
124             } else {
125               return $data;
126             }
127         }
128
129         return $defaultValue;
130     }
131
132     protected function registerUser($userDetails) {
133
134         // Create an array of the user data to create a new user instance
135         $userData = [
136             'name' => $userDetails['name'],
137             'email' => $userDetails['email'],
138             'password' => str_random(30),
139             'external_auth_id' => $userDetails['uid'],
140             'email_confirmed' => true,
141         ];
142
143         $user = $this->user->forceCreate($userData);
144         $this->userRepo->attachDefaultRole($user);
145         $this->userRepo->downloadAndAssignUserAvatar($user);
146         return $user;
147     }
148
149     public function processLoginCallback($samlID, $samlAttributes) {
150
151         $userDetails = $this->getUserDetails($samlID, $samlAttributes);
152         $user = $this->user
153             ->where('external_auth_id', $userDetails['uid'])
154             ->first();
155
156         $isLoggedIn = auth()->check();
157
158         if (!$isLoggedIn) {
159             if ($user === null && config('services.saml.auto_register') === true) {
160                 $user = $this->registerUser($userDetails);
161             }
162
163             if ($user !== null) {
164                 auth()->login($user);
165             }
166         }
167
168         return $user;
169     }
170
171     /**
172      * Sync the SAML groups to the user roles for the current user
173      * @param \BookStack\Auth\User $user
174      * @param array $samlAttributes
175      */
176     public function syncGroups(User $user, array $samlAttributes)
177     {
178         $userSamlGroups = $this->getUserGroups($samlAttributes);
179
180         // Get the ids for the roles from the names
181         $samlGroupsAsRoles = $this->matchSamlGroupsToSystemsRoles($userSamlGroups);
182
183         // Sync groups
184         if ($this->config['remove_from_groups']) {
185             $user->roles()->sync($samlGroupsAsRoles);
186             $this->userRepo->attachDefaultRole($user);
187         } else {
188             $user->roles()->syncWithoutDetaching($samlGroupsAsRoles);
189         }
190     }
191
192     /**
193      * Match an array of group names from SAML to BookStack system roles.
194      * Formats group names to be lower-case and hyphenated.
195      * @param array $groupNames
196      * @return \Illuminate\Support\Collection
197      */
198     protected function matchSamlGroupsToSystemsRoles(array $groupNames)
199     {
200         foreach ($groupNames as $i => $groupName) {
201             $groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));
202         }
203
204         $roles = Role::query()->where(function (Builder $query) use ($groupNames) {
205             $query->whereIn('name', $groupNames);
206             foreach ($groupNames as $groupName) {
207                 $query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%');
208             }
209         })->get();
210
211         $matchedRoles = $roles->filter(function (Role $role) use ($groupNames) {
212             return $this->roleMatchesGroupNames($role, $groupNames);
213         });
214
215         return $matchedRoles->pluck('id');
216     }
217
218     /**
219      * Check a role against an array of group names to see if it matches.
220      * Checked against role 'external_auth_id' if set otherwise the name of the role.
221      * @param \BookStack\Auth\Role $role
222      * @param array $groupNames
223      * @return bool
224      */
225     protected function roleMatchesGroupNames(Role $role, array $groupNames)
226     {
227         if ($role->external_auth_id) {
228             $externalAuthIds = explode(',', strtolower($role->external_auth_id));
229             foreach ($externalAuthIds as $externalAuthId) {
230                 if (in_array(trim($externalAuthId), $groupNames)) {
231                     return true;
232                 }
233             }
234             return false;
235         }
236
237         $roleName = str_replace(' ', '-', trim(strtolower($role->display_name)));
238         return in_array($roleName, $groupNames);
239     }
240
241 }