Skip to content

Commit

Permalink
Readonly User (#637)
Browse files Browse the repository at this point in the history
* Readonly User

* Refactor FormPolicy and TemplatePolicy to centralize write operation logic

- Introduced a private method `canPerformWriteOperation` in both FormPolicy and TemplatePolicy to encapsulate the logic for determining if a user can perform write operations on the respective models.
- Updated the `update`, `delete`, `restore`, and `forceDelete` methods in FormPolicy to use the new method for improved readability and maintainability.
- Simplified the `update` and `delete` methods in TemplatePolicy to leverage the centralized write operation logic.

This refactoring enhances code clarity and reduces duplication across policy classes.

* Refactor user and workspace permissions handling

- 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.

* Fix isReadonlyUser

* fix pint

---------

Co-authored-by: Julien Nahum <[email protected]>
  • Loading branch information
chiragchhatrala and JhumanJ authored Dec 30, 2024
1 parent 9a2d7b9 commit 2366f95
Show file tree
Hide file tree
Showing 18 changed files with 102 additions and 54 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
13 changes: 7 additions & 6 deletions api/app/Http/Controllers/WorkspaceUserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function addUser(Request $request, $workspaceId)

$this->validate($request, [
'email' => 'required|email',
'role' => 'required|in:admin,user',
'role' => 'required|in:' . implode(',', User::ROLES),
]);

$user = User::where('email', $request->email)->first();
Expand Down Expand Up @@ -62,10 +62,11 @@ private function inviteUser(Workspace $workspace, string $email, string $role)
{
if (
UserInvite::where('email', $email)
->where('workspace_id', $workspace->id)
->notExpired()
->pending()
->exists()) {
->where('workspace_id', $workspace->id)
->notExpired()
->pending()
->exists()
) {
return $this->success([
'message' => 'User has already been invited.'
]);
Expand All @@ -86,7 +87,7 @@ public function updateUserRole(Request $request, $workspaceId, $userId)
$this->authorize('adminAction', $workspace);

$this->validate($request, [
'role' => 'required|in:admin,user',
'role' => 'required|in:' . implode(',', User::ROLES),
]);

$workspace->users()->sync([
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()),
]);
}
}
7 changes: 7 additions & 0 deletions api/app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ class User extends Authenticatable implements JWTSubject

public const ROLE_ADMIN = 'admin';
public const ROLE_USER = 'user';
public const ROLE_READONLY = 'readonly';

public const ROLES = [
self::ROLE_ADMIN,
self::ROLE_USER,
self::ROLE_READONLY,
];

/**
* The attributes that are mass assignable.
Expand Down
8 changes: 8 additions & 0 deletions api/app/Models/Workspace.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,12 @@ public function forms()
{
return $this->hasMany(Form::class);
}

public function isReadonlyUser(?User $user)
{
return $user ? $this->users()
->wherePivot('user_id', $user->id)
->wherePivot('role', User::ROLE_READONLY)
->exists() : false;
}
}
21 changes: 15 additions & 6 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,9 +36,17 @@ public function view(User $user, Form $form)
*
* @return mixed
*/
public function create(User $user)
public function create(User $user, Workspace $workspace)
{
return true;
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) && !$form->workspace->isReadonlyUser($user);
}

/**
Expand All @@ -47,7 +56,7 @@ public function create(User $user)
*/
public function update(User $user, Form $form)
{
return $user->ownsForm($form);
return $this->canPerformWriteOperation($user, $form);
}

/**
Expand All @@ -57,7 +66,7 @@ public function update(User $user, Form $form)
*/
public function delete(User $user, Form $form)
{
return $user->ownsForm($form);
return $this->canPerformWriteOperation($user, $form);
}

/**
Expand All @@ -67,7 +76,7 @@ public function delete(User $user, Form $form)
*/
public function restore(User $user, Form $form)
{
return $user->ownsForm($form);
return $this->canPerformWriteOperation($user, $form);
}

/**
Expand All @@ -77,6 +86,6 @@ public function restore(User $user, Form $form)
*/
public function forceDelete(User $user, Form $form)
{
return $user->ownsForm($form);
return $this->canPerformWriteOperation($user, $form);
}
}
20 changes: 8 additions & 12 deletions api/app/Policies/TemplatePolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,27 @@ class TemplatePolicy

/**
* Determine whether the user can create models.
*
* @return \Illuminate\Auth\Access\Response|bool
*/
public function create(User $user)
{
return $user !== null;
}

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

/**
* Determine whether the user can delete the model.
*
* @return mixed
*/
public function update(User $user, Template $template)
{
return $this->canPerformWriteOperation($user, $template);
}

public function delete(User $user, Template $template)
{
return $user->admin || $user->template_editor || $template->creator_id === $user->id;
return $this->canPerformWriteOperation($user, $template);
}
}
7 changes: 6 additions & 1 deletion client/components/open/tables/OpenTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
</p>
</resizable-th>
<th
v-if="hasActions"
class="n-table-cell p-0 relative"
style="width: 100px"
>
Expand Down Expand Up @@ -181,14 +182,15 @@ export default {
return {
workingFormStore,
form: storeToRefs(workingFormStore).content,
user: useAuthStore().user,
workspace: useWorkspacesStore().getCurrent,
}
},
data() {
return {
tableHash: null,
skip: false,
hasActions: true,
internalColumns: [],
rafId: null,
fieldComponents: {
Expand All @@ -213,6 +215,9 @@ export default {
},
computed: {
hasActions() {
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/admin/AddUserToWorkspace.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ const workspacesStore = useWorkspacesStore()
const roleOptions = [
{name: "User", value: "user"},
{name: "Admin", value: "admin"}
{name: "Admin", value: "admin"},
{name: "Read Only", value: "readonly"}
]
const newUser = ref("")
Expand Down
3 changes: 2 additions & 1 deletion client/components/pages/admin/EditWorkSpaceUser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
:label="'New Role for '+props.user.name"
:options="[
{ name: 'User', value: 'user' },
{ name: 'Admin', value: 'admin' }
{ name: 'Admin', value: 'admin' },
{ name: 'Read Only', value: 'readonly' },
]"
option-key="value"
display-key="name"
Expand Down
3 changes: 3 additions & 0 deletions client/components/pages/auth/components/RegisterForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
/>

<select-input
v-if="!disableEmail"
name="hear_about_us"
:options="hearAboutUsOptions"
:form="form"
Expand Down Expand Up @@ -169,6 +170,7 @@ export default {
form: useForm({
name: "",
email: "",
hear_about_us: "",
password: "",
password_confirmation: "",
agree_terms: false,
Expand Down Expand Up @@ -216,6 +218,7 @@ export default {
if (this.$route.query?.invite_token) {
if (this.$route.query?.email) {
this.form.email = this.$route.query?.email
this.form.hear_about_us = 'invite'
this.disableEmail = true
}
this.form.invite_token = this.$route.query?.invite_token
Expand Down
14 changes: 10 additions & 4 deletions client/components/pages/forms/show/ExtraMenu.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<template>
<div v-if="form">
<div v-if="form">
<div
v-if="loadingDuplicate || loadingDelete"
class="pr-4 pt-2"
>
<Loader class="h-6 w-6 mx-auto" />
</div>
<UDropdown v-else :items="items">
<UDropdown
v-else
:items="items"
>
<v-button
color="white"
>
Expand Down Expand Up @@ -99,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 @@ -128,8 +132,9 @@ const items = computed(() => {
}
}] : []
],
[
...props.isMainPage ? [{
...workspace.value.is_readonly ? [] : [
[
...props.isMainPage ? [{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid',
to: { name: 'forms-slug-edit', params: { slug: props.form.slug } }
Expand Down Expand Up @@ -166,6 +171,7 @@ const items = computed(() => {
class: 'text-red-800 hover:bg-red-50 hover:text-red-600 group',
iconClass: 'text-red-900 group-hover:text-red-800'
}
]
]
].filter((group) => group.length > 0)
})
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
15 changes: 10 additions & 5 deletions client/pages/forms/[slug]/show.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
</h2>
<div class="flex">
<extra-menu
v-if="!workspace.is_readonly"
class="mr-2"
:form="form"
/>
Expand Down Expand Up @@ -98,6 +99,7 @@
</svg>
</v-button>
<v-button
v-if="!workspace.is_readonly"
class="text-white"
:to="{ name: 'forms-slug-edit', params: { slug: slug } }"
>
Expand Down Expand Up @@ -253,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 @@ -279,11 +282,13 @@ const tabsList = [
route: "forms-slug-show-submissions",
params: { 'slug': slug }
},
{
name: "Integrations",
route: "forms-slug-show-integrations",
params: { 'slug': slug }
},
...workspace.value.is_readonly ? [] : [
{
name: "Integrations",
route: "forms-slug-show-integrations",
params: { 'slug': slug }
},
],
{
name: "Analytics",
route: "forms-slug-show-stats",
Expand Down
3 changes: 3 additions & 0 deletions client/pages/forms/[slug]/show/share.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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="!workspace.is_readonly"
class="sm:w-1/2 flex"
:form="props.form"
/>
Expand Down Expand Up @@ -54,6 +55,8 @@ 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 workspace = computed(() => useWorkspacesStore().getCurrent)
const props = defineProps({
form: { type: Object, required: true },
})
Expand Down
3 changes: 2 additions & 1 deletion client/pages/home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Your Forms
</h2>
<v-button
v-if="!workspace.is_readonly"
v-track.create_form_click
:to="{ name: 'forms-create' }"
>
Expand Down Expand Up @@ -86,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
Loading

0 comments on commit 2366f95

Please sign in to comment.