Skip to content

Commit

Permalink
Refactor user and workspace permissions handling
Browse files Browse the repository at this point in the history
- Updated FormController to authorize form creation based on workspace context.
- Removed the `is_readonly` attribute from UserResource and integrated it into WorkspaceResource for better encapsulation.
- Refactored User model to eliminate the `getIsReadonlyAttribute` method, shifting readonly logic to the Workspace model.
- Adjusted FormPolicy and TemplatePolicy to utilize workspace readonly checks for user permissions.
- Updated various frontend components to reference workspace readonly status instead of user readonly status, enhancing clarity and consistency in permission handling.

These changes improve the management of user permissions in relation to workspaces, ensuring a more robust and maintainable authorization system.
  • Loading branch information
chiragchhatrala committed Dec 19, 2024
1 parent 4b35139 commit c05f987
Show file tree
Hide file tree
Showing 15 changed files with 41 additions and 40 deletions.
3 changes: 1 addition & 2 deletions api/app/Http/Controllers/Forms/FormController.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,9 @@ public function indexAll()

public function store(StoreFormRequest $request)
{
$this->authorize('create', Form::class);

$workspace = Workspace::findOrFail($request->get('workspace_id'));
$this->authorize('view', $workspace);
$this->authorize('create', [Form::class, $workspace]);

$formData = $this->formCleaner
->processRequest($request)
Expand Down
1 change: 0 additions & 1 deletion api/app/Http/Resources/UserResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ public function toArray($request)
'has_enterprise_subscription' => $this->has_enterprise_subscription,
'admin' => $this->admin,
'moderator' => $this->moderator,
'is_readonly' => $this->is_readonly,
'template_editor' => $this->template_editor,
'has_customer_id' => $this->has_customer_id,
'has_forms' => $this->has_forms,
Expand Down
1 change: 1 addition & 0 deletions api/app/Http/Resources/WorkspaceResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public function toArray($request)
{
return array_merge(parent::toArray($request), [
'max_file_size' => $this->max_file_size / 1000000,
'is_readonly' => $this->isReadonlyUser($request->user()),
]);
}
}
5 changes: 0 additions & 5 deletions api/app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,6 @@ public function getModeratorAttribute()
return in_array($this->email, config('opnform.moderator_emails')) || $this->admin;
}

public function getIsReadonlyAttribute()
{
return $this->workspaces()->where('role', self::ROLE_READONLY)->exists();
}

public function getTemplateEditorAttribute()
{
return $this->admin || in_array($this->email, config('opnform.template_editor_emails'));
Expand Down
10 changes: 9 additions & 1 deletion api/app/Models/Workspace.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,19 @@ public function owners()

public function billingOwners(): Collection
{
return $this->owners->filter(fn ($owner) => $owner->is_subscribed);
return $this->owners->filter(fn($owner) => $owner->is_subscribed);
}

public function forms()
{
return $this->hasMany(Form::class);
}

public function isReadonlyUser(User $user)
{
return $this->users()
->wherePivot('user_id', $user->id)
->wherePivot('role', User::ROLE_READONLY)
->exists();
}
}
7 changes: 4 additions & 3 deletions api/app/Policies/FormPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Models\Forms\Form;
use App\Models\User;
use App\Models\Workspace;
use Illuminate\Auth\Access\HandlesAuthorization;

class FormPolicy
Expand Down Expand Up @@ -35,17 +36,17 @@ public function view(User $user, Form $form)
*
* @return mixed
*/
public function create(User $user)
public function create(User $user, Workspace $workspace)
{
return !$user->is_readonly;
return !$workspace->isReadonlyUser($user);
}

/**
* Determine whether the user can perform write operations on the model.
*/
private function canPerformWriteOperation(User $user, Form $form): bool
{
return $user->ownsForm($form) && !$user->is_readonly;
return $user->ownsForm($form) && !$form->workspace->isReadonlyUser($user);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions api/app/Policies/TemplatePolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ class TemplatePolicy
*/
public function create(User $user)
{
return $user !== null && !$user->is_readonly;
return $user !== null;
}

/**
* Determine whether the user can perform write operations on the model.
*/
private function canPerformWriteOperation(User $user, Template $template): bool
{
return ($user->admin || $user->template_editor || $template->creator_id === $user->id) && !$user->is_readonly;
return $user->admin || $user->template_editor || $template->creator_id === $user->id;
}

public function update(User $user, Template $template)
Expand Down
3 changes: 2 additions & 1 deletion client/components/open/tables/OpenTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export default {
workingFormStore,
form: storeToRefs(workingFormStore).content,
user: useAuthStore().user,
workspace: useWorkspacesStore().getCurrent,
}
},
Expand Down Expand Up @@ -215,7 +216,7 @@ export default {
computed: {
hasActions() {
return !this.user.is_readonly
return !this.workspace.is_readonly
},
formData() {
return [...this.data].sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
Expand Down
3 changes: 2 additions & 1 deletion client/components/pages/forms/show/ExtraMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const authStore = useAuthStore()
const formsStore = useFormsStore()
const formEndpoint = "/open/forms/{id}"
const user = computed(() => authStore.user)
const workspace = computed(() => useWorkspacesStore().getCurrent)
const loadingDuplicate = ref(false)
const loadingDelete = ref(false)
Expand Down Expand Up @@ -131,7 +132,7 @@ const items = computed(() => {
}
}] : []
],
...user.value.is_readonly ? [] : [
...workspace.value.is_readonly ? [] : [
[
...props.isMainPage ? [{
label: 'Edit',
Expand Down
1 change: 1 addition & 0 deletions client/components/pages/settings/WorkSpaceUser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Workspace Members
</h4>
<UButton
v-if="!workspace.is_readonly"
label="Invite User"
icon="i-heroicons-user-plus-20-solid"
:loading="loading"
Expand Down
9 changes: 4 additions & 5 deletions client/pages/forms/[slug]/show.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
</h2>
<div class="flex">
<extra-menu
v-if="!user.is_readonly"
v-if="!workspace.is_readonly"
class="mr-2"
:form="form"
/>
Expand Down Expand Up @@ -99,7 +99,7 @@
</svg>
</v-button>
<v-button
v-if="!user.is_readonly"
v-if="!workspace.is_readonly"
class="text-white"
:to="{ name: 'forms-slug-edit', params: { slug: slug } }"
>
Expand Down Expand Up @@ -246,8 +246,6 @@ useOpnSeoMeta({
title: "Home",
})
const authStore = useAuthStore()
const user = computed(() => authStore.user)
const route = useRoute()
const formsStore = useFormsStore()
const workingFormStore = useWorkingFormStore()
Expand All @@ -257,6 +255,7 @@ const slug = useRoute().params.slug
formsStore.startLoading()
const form = computed(() => formsStore.getByKey(slug))
const workspace = computed(() => workspacesStore.getCurrent)
const loading = computed(() => formsStore.loading || workspacesStore.loading)
const displayClosesDate = computed(() => {
Expand All @@ -283,7 +282,7 @@ const tabsList = [
route: "forms-slug-show-submissions",
params: { 'slug': slug }
},
...user.value.is_readonly ? [] : [
...workspace.value.is_readonly ? [] : [
{
name: "Integrations",
route: "forms-slug-show-integrations",
Expand Down
5 changes: 2 additions & 3 deletions client/pages/forms/[slug]/show/share.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div class="mb-20">
<div class="mb-6 pb-6 border-b w-full flex flex-col sm:flex-row gap-2">
<regenerate-form-link
v-if="!user.is_readonly"
v-if="!workspace.is_readonly"
class="sm:w-1/2 flex"
:form="props.form"
/>
Expand Down Expand Up @@ -55,8 +55,7 @@ import RegenerateFormLink from "~/components/pages/forms/show/RegenerateFormLink
import AdvancedFormUrlSettings from "~/components/open/forms/components/AdvancedFormUrlSettings.vue"
import EmbedFormAsPopupModal from "~/components/pages/forms/show/EmbedFormAsPopupModal.vue"
const authStore = useAuthStore()
const user = computed(() => authStore.user)
const workspace = computed(() => useWorkspacesStore().getCurrent)
const props = defineProps({
form: { type: Object, required: true },
Expand Down
6 changes: 2 additions & 4 deletions client/pages/home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Your Forms
</h2>
<v-button
v-if="!user.is_readonly"
v-if="!workspace.is_readonly"
v-track.create_form_click
:to="{ name: 'forms-create' }"
>
Expand Down Expand Up @@ -87,7 +87,7 @@
again.
</div>
<v-button
v-if="forms.length === 0"
v-if="!workspace.is_readonly && forms.length === 0"
v-track.create_form_click
class="mt-4"
:to="{ name: 'forms-create' }"
Expand Down Expand Up @@ -247,8 +247,6 @@ useOpnSeoMeta({
"All of your OpnForm are here. Create new forms, or update your existing forms.",
})
const authStore = useAuthStore()
const user = computed(() => authStore.user)
const subscriptionModalStore = useSubscriptionModalStore()
const formsStore = useFormsStore()
const workspacesStore = useWorkspacesStore()
Expand Down
11 changes: 6 additions & 5 deletions client/pages/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,18 @@ useOpnSeoMeta({
const authStore = useAuthStore()
const user = computed(() => authStore.user)
const workspace = computed(() => useWorkspacesStore().getCurrent)
const tabsList = computed(() => {
const tabs = [
{
name: "Profile",
route: "settings-profile",
},
...user.value.is_readonly ? [] : [
{
name: "Workspace Settings",
route: "settings-workspace",
},
{
name: "Workspace Settings",
route: "settings-workspace",
},
...workspace.value.is_readonly ? [] : [
{
name: "Access Tokens",
route: "settings-access-tokens",
Expand Down
12 changes: 5 additions & 7 deletions client/pages/settings/workspace.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
You can switch to another workspace in top left corner of the page.</small>
</div>
<div class="w-full flex flex-wrap gap-2">
<WorkSpaceCustomDomains v-if="useFeatureFlag('custom_domains') && !loading" />
<WorkSpaceEmailSettings v-if="!loading" />
<template v-if="!workspace.is_readonly">
<WorkSpaceCustomDomains v-if="useFeatureFlag('custom_domains') && !loading" />
<WorkSpaceEmailSettings v-if="!loading" />
</template>
<UButton
label="New Workspace"
icon="i-heroicons-plus"
Expand Down Expand Up @@ -95,12 +97,10 @@
</template>

<script setup>
import {watch, ref} from "vue"
import {fetchAllWorkspaces} from "~/stores/workspaces.js"
const crisp = useCrisp()
const workspacesStore = useWorkspacesStore()
const workspaces = computed(() => workspacesStore.getAll)
const workspace = computed(() => workspacesStore.getCurrent)
const loading = computed(() => workspacesStore.loading)
useOpnSeoMeta({
Expand All @@ -116,8 +116,6 @@ const form = useForm({
})
const workspaceModal = ref(false)
const workspace = computed(() => workspacesStore.getCurrent)
onMounted(() => {
fetchAllWorkspaces()
})
Expand Down

0 comments on commit c05f987

Please sign in to comment.