Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 48 additions & 19 deletions src/Components/GoogleProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,17 @@ public function enableGroupFetching()
{
$this->fetchGroups = true;
$this->scopes = array_unique(array_merge($this->scopes, [
'https://www.googleapis.com/auth/admin.directory.group.readonly',
'https://www.googleapis.com/auth/cloud-identity.groups.readonly',
]));

return $this;
}

/**
* Get the user's groups from Google Admin Directory API.
* Get the user's groups from Google Cloud Identity API.
*
* Requires domain-wide delegation or admin privileges.
* Uses the searchDirectGroups endpoint which allows any Google Workspace
* user to query their own group memberships via a standard OAuth 2.0 token.
*
* @param string $token
* @param string $userEmail
Expand All @@ -58,32 +59,60 @@ public function enableGroupFetching()
protected function getUserGroups($token, $userEmail)
{
try {
$url = "https://admin.googleapis.com/admin/directory/v1/groups?"
. http_build_query(['userKey' => $userEmail]);
$groups = [];
$pageToken = null;

$response = $this->getHttpClient()->get($url, [
'headers' => ['Authorization' => 'Bearer ' . $token]
]);
do {
$query = [
'query' => "member_key_id == '" . $userEmail . "'",
'page_size' => 1000,
];

$data = json_decode($response->getBody()->getContents(), true);
$groups = [];
if ($pageToken) {
$query['page_token'] = $pageToken;
}

if (isset($data['groups']) && is_array($data['groups'])) {
foreach ($data['groups'] as $group) {
$groups[] = [
'id' => $group['id'] ?? null,
'email' => $group['email'] ?? null,
'name' => $group['name'] ?? null,
];
$response = $this->getHttpClient()->get(
'https://cloudidentity.googleapis.com/v1/groups/-/memberships:searchDirectGroups',
[
'headers' => ['Authorization' => 'Bearer ' . $token],
'query' => $query,
]
);

$data = json_decode($response->getBody()->getContents(), true);

\Log::info('Google OAuth: Cloud Identity API response', [
'user_email' => $userEmail,
'membership_count' => isset($data['memberships']) ? count($data['memberships']) : 0,
'raw_keys' => array_keys($data ?? []),
]);

if (isset($data['memberships']) && is_array($data['memberships'])) {
foreach ($data['memberships'] as $membership) {
$groups[] = [
'id' => $membership['group'] ?? null,
'email' => $membership['groupKey']['id'] ?? null,
'name' => $membership['displayName'] ?? null,
];
}
}
}

$pageToken = $data['nextPageToken'] ?? null;
} while ($pageToken);

\Log::info('Google OAuth: Groups fetched successfully', [
'user_email' => $userEmail,
'group_count' => count($groups),
'group_emails' => array_column($groups, 'email'),
]);

return $groups;

} catch (\GuzzleHttp\Exception\ClientException $e) {
$response = $e->getResponse();
$body = $response ? $response->getBody()->getContents() : 'No response body';
\Log::warning('Google OAuth: Failed to fetch groups from Admin Directory API', [
\Log::warning('Google OAuth: Failed to fetch groups from Cloud Identity API', [
'status' => $response ? $response->getStatusCode() : 'unknown',
'error' => $body
]);
Expand Down
2 changes: 1 addition & 1 deletion src/Models/GoogleOAuthConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ protected static function prepareConfigSchemaField(array &$schema)
$schema['type'] = 'boolean';
$schema['default'] = false;
$schema['description'] = 'Enable mapping of Google group memberships to DreamFactory roles. ' .
'Requires Admin SDK API enabled and domain-wide delegation configured in Google Workspace.';
'Requires Cloud Identity API enabled in Google Cloud Console. No admin privileges required.';
break;
}
}
Expand Down
46 changes: 46 additions & 0 deletions src/Services/Google.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use DreamFactory\Core\OAuth\Models\RoleGoogle;
use Laravel\Socialite\Contracts\User as OAuthUserContract;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;

class Google extends BaseOAuthService
{
Expand Down Expand Up @@ -55,19 +56,32 @@ public function getProviderName()
protected function getRoleByGroup(array $groups)
{
if (!$this->mapGroupToRole || empty($groups)) {
Log::info('Google OAuth: getRoleByGroup early return', [
'map_group_to_role' => $this->mapGroupToRole,
'groups_empty' => empty($groups),
]);
return null;
}

foreach ($groups as $group) {
$groupEmail = Arr::get($group, 'email');
if (!empty($groupEmail)) {
$role = $this->findRoleByGroupEmail($groupEmail);
Log::info('Google OAuth: Group match attempt', [
'group_email' => $groupEmail,
'match_found' => !empty($role),
'role_id' => $role->role_id ?? null,
]);
if (!empty($role)) {
return $role->role_id;
}
}
}

Log::warning('Google OAuth: No group matched any configured role mapping', [
'user_groups' => array_column($groups, 'email'),
]);

return null;
}

Expand Down Expand Up @@ -132,25 +146,57 @@ public function createShadowOAuthUser(OAuthUserContract $OAuthUser)
// Priority: Group mapping > App role map > Default role
$roleToApply = null;

Log::info('Google OAuth: Role assignment starting', [
'user_id' => $user->id,
'user_email' => $user->email,
'map_group_to_role' => $this->mapGroupToRole,
'default_role' => $this->getDefaultRole(),
'service_id' => $this->getServiceId(),
]);

if ($this->mapGroupToRole) {
$userRaw = $OAuthUser->getRaw();
$groups = Arr::get($userRaw, 'groups', []);

Log::info('Google OAuth: Group data from OAuth user', [
'group_count' => count($groups),
'group_emails' => array_column($groups, 'email'),
'raw_keys' => array_keys($userRaw),
]);

$roleToApply = $this->getRoleByGroup($groups);

Log::info('Google OAuth: Group-to-role lookup result', [
'role_from_group' => $roleToApply,
'configured_mappings' => RoleGoogle::all()->toArray(),
]);
}

if (empty($roleToApply)) {
if (!empty($defaultRole = $this->getDefaultRole())) {
$roleToApply = $defaultRole;
Log::info('Google OAuth: Falling back to default role', ['role_id' => $defaultRole]);
}
}

// Always refresh role assignments on login to reflect current group membership
if (!empty($roleToApply)) {
Log::info('Google OAuth: Applying role', [
'user_id' => $user->id,
'role_id' => $roleToApply,
'source' => ($this->mapGroupToRole && $roleToApply !== $this->getDefaultRole()) ? 'group_mapping' : 'default_role',
]);
\DB::table('user_to_app_to_role')->where('user_id', $user->id)->delete();
User::applyDefaultUserAppRole($user, $roleToApply);
} elseif (!empty($serviceId = $this->getServiceId())) {
Log::info('Google OAuth: Applying service app role map', ['service_id' => $serviceId]);
\DB::table('user_to_app_to_role')->where('user_id', $user->id)->delete();
User::applyAppRoleMapByService($user, $serviceId);
} else {
Log::warning('Google OAuth: No role applied! User will have no permissions.', [
'user_id' => $user->id,
'user_email' => $user->email,
]);
}

return $user;
Expand Down