Dynamic OauthDriver scope (#544)

* Dynamic OauthDriver scope

* support migration for mysql

* Refactor default scopes for integrations

* Small UI changes

* fix flet select tooltip

* fix linter

* Fix google token size in DB

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Chirag Chhatrala 2024-08-29 16:58:02 +05:30 committed by GitHub
parent 89513e3b4a
commit da0ea04475
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 130 additions and 15 deletions

View File

@ -93,6 +93,7 @@ class OAuthController extends Controller
$oauthProvider->update([ $oauthProvider->update([
'access_token' => $socialiteUser->token, 'access_token' => $socialiteUser->token,
'refresh_token' => $socialiteUser->refreshToken, 'refresh_token' => $socialiteUser->refreshToken,
'scopes' => $socialiteUser->approvedScopes
]); ]);
return $oauthProvider->user; return $oauthProvider->user;
@ -139,6 +140,7 @@ class OAuthController extends Controller
'refresh_token' => $socialiteUser->refreshToken, 'refresh_token' => $socialiteUser->refreshToken,
'name' => $socialiteUser->getName(), 'name' => $socialiteUser->getName(),
'email' => $socialiteUser->getEmail(), 'email' => $socialiteUser->getEmail(),
'scopes' => $socialiteUser->approvedScopes
] ]
); );
return $user; return $user;

View File

@ -26,8 +26,10 @@ class OAuthProviderController extends Controller
$userId = Auth::id(); $userId = Auth::id();
cache()->put("oauth-intention:{$userId}", $request->input('intention'), 60 * 5); cache()->put("oauth-intention:{$userId}", $request->input('intention'), 60 * 5);
// Connecting an account for integrations purposes
// Adding full scopes to the driver
return response()->json([ return response()->json([
'url' => $service->getDriver()->getRedirectUrl(), 'url' => $service->getDriver()->fullScopes()->getRedirectUrl(),
]); ]);
} }
@ -47,6 +49,7 @@ class OAuthProviderController extends Controller
'refresh_token' => $driverUser->refreshToken, 'refresh_token' => $driverUser->refreshToken,
'name' => $driverUser->getName(), 'name' => $driverUser->getName(),
'email' => $driverUser->getEmail(), 'email' => $driverUser->getEmail(),
'scopes' => $driverUser->approvedScopes
] ]
); );

View File

@ -32,6 +32,7 @@ class OAuthProviderResource extends JsonResource
fn () => OAuthProviderUserResource::make($this->resource->user), fn () => OAuthProviderUserResource::make($this->resource->user),
null, null,
), ),
'scopes' => $this->resource->scopes
]; ];
} }
} }

View File

@ -7,7 +7,14 @@ use Laravel\Socialite\Contracts\User;
interface OAuthDriver interface OAuthDriver
{ {
public function getRedirectUrl(): string; public function getRedirectUrl(): string;
public function setRedirectUrl($url): self; public function setRedirectUrl(string $url): self;
public function setScopes(array $scopes): self;
public function getUser(): User; public function getUser(): User;
public function canCreateUser(): bool; public function canCreateUser(): bool;
/**
* Set up all the scopes required by OpnForm for various integrations.
* This method configures the necessary permissions for the current OAuth driver.
*/
public function fullScopes(): self;
} }

View File

@ -11,6 +11,7 @@ use Laravel\Socialite\Two\GoogleProvider;
class OAuthGoogleDriver implements OAuthDriver class OAuthGoogleDriver implements OAuthDriver
{ {
private ?string $redirectUrl = null; private ?string $redirectUrl = null;
private ?array $scopes = [];
protected GoogleProvider $provider; protected GoogleProvider $provider;
@ -22,7 +23,7 @@ class OAuthGoogleDriver implements OAuthDriver
public function getRedirectUrl(): string public function getRedirectUrl(): string
{ {
return $this->provider return $this->provider
->scopes([Sheets::DRIVE_FILE]) ->scopes($this->scopes ?? [])
->stateless() ->stateless()
->redirectUrl($this->redirectUrl ?? config('services.google.redirect')) ->redirectUrl($this->redirectUrl ?? config('services.google.redirect'))
->with([ ->with([
@ -46,10 +47,20 @@ class OAuthGoogleDriver implements OAuthDriver
return true; return true;
} }
public function setRedirectUrl($url): OAuthDriver public function setRedirectUrl(string $url): OAuthDriver
{ {
$this->redirectUrl = $url; $this->redirectUrl = $url;
return $this; return $this;
} }
public function setScopes(array $scopes): OAuthDriver
{
$this->scopes = $scopes;
return $this;
}
public function fullScopes(): OAuthDriver
{
return $this->setScopes([Sheets::DRIVE_FILE]);
}
} }

View File

@ -30,7 +30,8 @@ class OAuthProvider extends Model
* @var array * @var array
*/ */
protected $hidden = [ protected $hidden = [
'access_token', 'refresh_token', 'access_token',
'refresh_token',
]; ];
protected function casts() protected function casts()
@ -38,6 +39,7 @@ class OAuthProvider extends Model
return [ return [
'provider' => OAuthProviderService::class, 'provider' => OAuthProviderService::class,
'token_expires_at' => 'datetime', 'token_expires_at' => 'datetime',
'scopes' => 'array'
]; ];
} }

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
$driver = DB::getDriverName();
Schema::table('oauth_providers', function (Blueprint $table) use ($driver) {
if ($driver === 'mysql') {
$table->json('scopes')->default(new Expression('(JSON_OBJECT())'));
} else {
$table->json('scopes')->default('{}');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('oauth_providers', function (Blueprint $table) {
$table->dropColumn('scopes');
});
}
};

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('oauth_providers', function (Blueprint $table) {
$table->text('access_token')->change();
$table->text('refresh_token')->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('oauth_providers', function (Blueprint $table) {
$table->string('access_token')->change();
$table->string('refresh_token')->change();
});
}
};

View File

@ -33,6 +33,9 @@
theme.FlatSelectInput.spacing.vertical, theme.FlatSelectInput.spacing.vertical,
theme.FlatSelectInput.fontSize, theme.FlatSelectInput.fontSize,
theme.FlatSelectInput.option, theme.FlatSelectInput.option,
{
'!cursor-not-allowed !bg-gray-200': disableOptions.includes(option[optionKey]),
},
]" ]"
@click="onSelect(option[optionKey])" @click="onSelect(option[optionKey])"
> >
@ -50,9 +53,15 @@
:theme="theme" :theme="theme"
/> />
</template> </template>
<p class="flex-grow"> <UTooltip
{{ option[displayKey] }} :text="disableOptionsTooltip"
</p> :prevent="!disableOptions.includes(option[optionKey])"
class="w-full"
>
<p class="flex-grow">
{{ option[displayKey] }}
</p>
</UTooltip>
</div> </div>
</template> </template>
<div <div
@ -98,7 +107,9 @@ export default {
emitKey: {type: String, default: "value"}, emitKey: {type: String, default: "value"},
displayKey: {type: String, default: "name"}, displayKey: {type: String, default: "name"},
loading: {type: Boolean, default: false}, loading: {type: Boolean, default: false},
multiple: {type: Boolean, default: false}, multiple: { type: Boolean, default: false },
disableOptions: { type: Array, default: () => [] },
disableOptionsTooltip: {type: String, default: "Not allowed"},
}, },
setup(props, context) { setup(props, context) {
return { return {
@ -111,7 +122,7 @@ export default {
computed: {}, computed: {},
methods: { methods: {
onSelect(value) { onSelect(value) {
if (this.disabled) { if (this.disabled || this.disableOptions.includes(value)) {
return return
} }

View File

@ -4,12 +4,17 @@
:integration="props.integration" :integration="props.integration"
:form="form" :form="form"
> >
<div class="my-5"> <div class="mb-4">
<select-input <p class="text-gray-500 mb-4">
Adds new entry to spreadsheets on each form submission.
</p>
<FlatSelectInput
v-if="providers.length" v-if="providers.length"
v-model="integrationData.oauth_id" v-model="integrationData.oauth_id"
name="provider" name="provider"
:options="providers" :options="providers"
:disable-options="disableProviders"
disable-options-tooltip="Re-connect account to fix permissions"
display-key="email" display-key="email"
option-key="id" option-key="id"
emit-key="id" emit-key="id"
@ -19,8 +24,8 @@
<template #help> <template #help>
<InputHelp> <InputHelp>
<span> <span>
Add an entry to spreadsheets on each form submission.
<NuxtLink <NuxtLink
class="text-blue-500"
:to="{ name: 'settings-connections' }" :to="{ name: 'settings-connections' }"
> >
Click here Click here
@ -29,7 +34,7 @@
</span> </span>
</InputHelp> </InputHelp>
</template> </template>
</select-input> </FlatSelectInput>
<v-button <v-button
v-else v-else
@ -44,6 +49,7 @@
</template> </template>
<script setup> <script setup>
import FlatSelectInput from '~/components/forms/FlatSelectInput.vue'
import IntegrationWrapper from './components/IntegrationWrapper.vue' import IntegrationWrapper from './components/IntegrationWrapper.vue'
const props = defineProps({ const props = defineProps({
@ -55,6 +61,7 @@ const props = defineProps({
const providersStore = useOAuthProvidersStore() const providersStore = useOAuthProvidersStore()
const providers = computed(() => providersStore.getAll.filter(provider => provider.provider == 'google')) const providers = computed(() => providersStore.getAll.filter(provider => provider.provider == 'google'))
const disableProviders = computed(() => providersStore.getAll.filter(provider => !provider.scopes.includes(providersStore.googleDrivePermission)).map((provider) => provider.id))
function connect () { function connect () {
providersStore.connect('google', true) providersStore.connect('google', true)

View File

@ -8,7 +8,7 @@
<small class="text-gray-600">Manage your external connections.</small> <small class="text-gray-600">Manage your external connections.</small>
</div> </div>
<UButton <UButton
label="Connect new account" label="Connect account"
icon="i-heroicons-plus" icon="i-heroicons-plus"
:loading="loading" :loading="loading"
@click="providerModal = true" @click="providerModal = true"

View File

@ -7,6 +7,8 @@ export const useOAuthProvidersStore = defineStore("oauth_providers", () => {
const contentStore = useContentStore() const contentStore = useContentStore()
const alert = useAlert() const alert = useAlert()
const googleDrivePermission = 'https://www.googleapis.com/auth/drive.file'
const services = computed(() => { const services = computed(() => {
return [ return [
{ {
@ -92,6 +94,7 @@ export const useOAuthProvidersStore = defineStore("oauth_providers", () => {
return { return {
...contentStore, ...contentStore,
googleDrivePermission,
services, services,
getService, getService,
fetchOAuthProviders, fetchOAuthProviders,