VYPR
High severityNVD Advisory· Published Aug 29, 2024· Updated Aug 29, 2024

Insufficient permission checks in the language settings in Kirby CMS

CVE-2024-41964

Description

Kirby is a CMS targeting designers and editors. Kirby allows to restrict the permissions of specific user roles. Users of that role can only perform permitted actions. Permissions for creating and deleting languages have already existed and could be configured, but were not enforced by Kirby's frontend or backend code. A permission for updating existing languages has not existed before the patched versions. So disabling the languages.* wildcard permission for a role could not have prohibited updates to existing language definitions. The missing permission checks allowed attackers with Panel access to manipulate the language definitions. The problem has been patched in Kirby 3.6.6.6, Kirby 3.7.5.5, Kirby 3.8.4.4, Kirby 3.9.8.2, Kirby 3.10.1.1, and Kirby 4.3.1. Please update to one of these or a later version to fix the vulnerability. There are no known workarounds for this vulnerability.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
getkirby/cmsPackagist
< 3.6.6.63.6.6.6
getkirby/cmsPackagist
>= 3.7.0, < 3.7.5.53.7.5.5
getkirby/cmsPackagist
>= 3.8.0, < 3.8.4.43.8.4.4
getkirby/cmsPackagist
>= 3.9.0, < 3.9.8.23.9.8.2
getkirby/cmsPackagist
>= 3.10.0, < 3.10.1.13.10.1.1
getkirby/cmsPackagist
>= 4.0.0, < 4.3.14.3.1

Affected products

1

Patches

6
1dbc9215c97a

Fix language permissions #6497

https://github.com/getkirby/kirbyAhmet BoraAug 19, 2024via ghsa
6 files changed · +199 5
  • i18n/translations/en.json+3 0 modified
    @@ -100,9 +100,12 @@
       "error.form.notSaved": "The form could not be saved",
     
       "error.language.code": "Please enter a valid code for the language",
    +  "error.language.create.permission": "You are not allowed to create a language",
    +  "error.language.delete.permission": "You are not allowed to delete the language",
       "error.language.duplicate": "The language already exists",
       "error.language.name": "Please enter a valid name for the language",
       "error.language.notFound": "The language could not be found",
    +  "error.language.update.permission": "You are not allowed to update the language",
     
       "error.layout.validation.block": "There's an error in block {blockIndex} in layout {layoutIndex}",
       "error.layout.validation.settings": "There's an error in layout {index} settings",
    
  • panel/src/components/Views/LanguagesView.vue+21 3 modified
    @@ -7,6 +7,7 @@
             <k-button-group slot="left">
               <k-button
                 :text="$t('language.create')"
    +						:disabled="!$permissions.languages.create"
                 icon="add"
                 @click="$dialog('languages/create')"
               />
    @@ -30,14 +31,23 @@
                   v-if="secondaryLanguages.length"
                   :items="secondaryLanguages"
                 />
    -            <k-empty v-else icon="globe" @click="$dialog('languages/create')">
    +						<k-empty
    +							v-else
    +							icon="globe"
    +							:disabled="!$permissions.languages.create"
    +							@click="$dialog('languages/create')"
    +						>
                   {{ $t("languages.secondary.empty") }}
                 </k-empty>
               </section>
             </template>
     
             <template v-else-if="languages.length === 0">
    -          <k-empty icon="globe" @click="$dialog('languages/create')">
    +					<k-empty
    +						icon="globe"
    +						:disabled="!$permissions.languages.create"
    +						@click="$dialog('languages/create')"
    +					>
                 {{ $t("languages.empty") }}
               </k-empty>
             </template>
    @@ -66,20 +76,28 @@ export default {
               icon: "globe"
             },
             link: () => {
    +					if (!this.$permissions.languages.update) {
    +						return null;
    +					}
    +
               this.$dialog(`languages/${language.id}/update`);
             },
             options: [
               {
                 icon: "edit",
                 text: this.$t("edit"),
    +						disabled: !this.$permissions.languages.update,
                 click() {
                   this.$dialog(`languages/${language.id}/update`);
                 }
               },
               {
                 icon: "trash",
                 text: this.$t("delete"),
    -            disabled: language.default && this.languages.length !== 1,
    +						disabled: (
    +							(language.default && this.languages.length !== 1) ||
    +							!this.$permissions.languages.delete
    +						),
                 click() {
                   this.$dialog(`languages/${language.id}/delete`);
                 }
    
  • src/Cms/Language.php+28 1 modified
    @@ -201,8 +201,17 @@ protected static function converter(string $from, string $to): bool
          */
         public static function create(array $props)
         {
    +        $kirby = App::instance();
    +        $user  = $kirby->user();
    +
    +        if (
    +            $user === null ||
    +            $user->role()->permissions()->for('languages', 'create') === false
    +        ) {
    +            throw new PermissionException(['key' => 'language.create.permission']);
    +        }
    +
             $props['code'] = Str::slug($props['code'] ?? null);
    -        $kirby         = App::instance();
             $languages     = $kirby->languages();
     
             // make the first language the default language
    @@ -238,10 +247,18 @@ public static function create(array $props)
         public function delete(): bool
         {
             $kirby     = App::instance();
    +        $user      = $kirby->user();
             $languages = $kirby->languages();
             $code      = $this->code();
             $isLast    = $languages->count() === 1;
     
    +        if (
    +            $user === null ||
    +            $user->role()->permissions()->for('languages', 'delete') === false
    +        ) {
    +            throw new PermissionException(['key' => 'language.delete.permission']);
    +        }
    +
             if (F::remove($this->root()) !== true) {
                 throw new Exception('The language could not be deleted');
             }
    @@ -648,6 +665,16 @@ public function url(): string
          */
         public function update(array $props = null)
         {
    +        $kirby = App::instance();
    +        $user  = $kirby->user();
    +
    +        if (
    +            $user === null ||
    +            $user->role()->permissions()->for('languages', 'update') === false
    +        ) {
    +            throw new PermissionException(['key' => 'language.update.permission']);
    +        }
    +
             // don't change the language code
             unset($props['code']);
     
    
  • src/Cms/Permissions.php+2 1 modified
    @@ -44,7 +44,8 @@ class Permissions
             ],
             'languages' => [
                 'create' => true,
    -            'delete' => true
    +            'delete' => true,
    +            'update' => true
             ],
             'pages' => [
                 'changeSlug'     => true,
    
  • tests/Cms/Languages/LanguagesTest.php+2 0 modified
    @@ -108,6 +108,8 @@ public function testMultipleDefault()
     
         public function testCreate()
         {
    +        $this->app->impersonate('kirby');
    +
             $language = $this->app->languages()->create([
                 'code' => 'tr'
             ]);
    
  • tests/Cms/Languages/LanguageTest.php+143 0 modified
    @@ -3,6 +3,7 @@
     namespace Kirby\Cms;
     
     use Kirby\Data\Data;
    +use Kirby\Exception\PermissionException;
     use Kirby\Filesystem\Dir;
     use Kirby\Filesystem\F;
     use PHPUnit\Framework\TestCase;
    @@ -471,6 +472,8 @@ public function testBaseUrl($kirbyUrl, $url, $expected)
     
         public function testCreate()
         {
    +        $this->app->impersonate('kirby');
    +
             $language = Language::create([
                 'code' => 'en'
             ]);
    @@ -482,19 +485,112 @@ public function testCreate()
             $this->assertSame('/en', $language->url());
         }
     
    +    /**
    +     * @covers ::create
    +     */
    +    public function testCreateNoPermissions()
    +    {
    +        $app = $this->app->clone([
    +            'blueprints' => [
    +                'users/editor' => [
    +                    'name' => 'editor',
    +                    'permissions' => [
    +                        'languages' => [
    +                            'create' => false
    +                        ]
    +                    ]
    +                ],
    +            ],
    +            'users' => [
    +                ['email' => 'test@getkirby.com', 'role' => 'editor']
    +            ]
    +        ]);
    +
    +        $this->expectException(PermissionException::class);
    +        $this->expectExceptionMessage('You are not allowed to create a language');
    +
    +        $app->impersonate('test@getkirby.com');
    +        Language::create([
    +            'code' => 'en'
    +        ]);
    +    }
    +
    +    /**
    +     * @covers ::create
    +     */
    +    public function testCreateWithoutLoggedUser()
    +    {
    +        $this->expectException(PermissionException::class);
    +        $this->expectExceptionMessage('You are not allowed to create a language');
    +
    +        Language::create([
    +            'code' => 'en'
    +        ]);
    +    }
    +
         public function testDelete()
         {
    +        $this->app->impersonate('kirby');
    +
             $language = Language::create([
                 'code' => 'en'
             ]);
     
             $this->assertTrue($language->delete());
         }
     
    +    /**
    +     * @covers ::delete
    +     */
    +    public function testDeleteNoPermissions()
    +    {
    +        $app = $this->app->clone([
    +            'blueprints' => [
    +                'users/editor' => [
    +                    'name' => 'editor',
    +                    'permissions' => [
    +                        'languages' => [
    +                            'create' => true,
    +                            'delete' => false
    +                        ]
    +                    ]
    +                ],
    +            ],
    +            'users' => [
    +                ['email' => 'test@getkirby.com', 'role' => 'editor']
    +            ]
    +        ]);
    +
    +        $this->expectException(PermissionException::class);
    +        $this->expectExceptionMessage('You are not allowed to delete the language');
    +
    +        $app->impersonate('test@getkirby.com');
    +        $language = Language::create(['code' => 'en']);
    +        $language->delete();
    +    }
    +
    +    /**
    +     * @covers ::delete
    +     */
    +    public function testDeleteWithoutLoggedUser()
    +    {
    +        $this->app->impersonate('kirby');
    +        $language = Language::create(['code' => 'en']);
    +
    +        $this->expectException(PermissionException::class);
    +        $this->expectExceptionMessage('You are not allowed to delete the language');
    +
    +        // unimpersonate and test the method
    +        $this->app->impersonate();
    +        $language->delete();
    +    }
    +
         public function testUpdate()
         {
             Dir::make($contentDir = $this->fixtures . '/content');
     
    +        $this->app->impersonate('kirby');
    +
             $language = Language::create([
                 'code' => 'en'
             ]);
    @@ -503,4 +599,51 @@ public function testUpdate()
     
             $this->assertSame('English', $language->name());
         }
    +
    +    /**
    +     * @covers ::update
    +     */
    +    public function testUpdateNoPermissions()
    +    {
    +        $app = $this->app->clone([
    +            'blueprints' => [
    +                'users/editor' => [
    +                    'name'        => 'editor',
    +                    'permissions' => [
    +                        'languages' => [
    +                            'create' => true,
    +                            'update' => false
    +                        ]
    +                    ]
    +                ],
    +            ],
    +            'users'      => [
    +                ['email' => 'test@getkirby.com', 'role' => 'editor']
    +            ]
    +        ]);
    +
    +        $this->expectException(PermissionException::class);
    +        $this->expectExceptionMessage('You are not allowed to update the language');
    +
    +        $app->impersonate('test@getkirby.com');
    +
    +        $language = Language::create(['code' => 'en']);
    +        $language->update(['name' => 'English']);
    +    }
    +
    +    /**
    +     * @covers ::update
    +     */
    +    public function testUpdateWithoutLoggedUser()
    +    {
    +        $this->app->impersonate('kirby');
    +        $language = Language::create(['code' => 'en']);
    +
    +        $this->expectException(PermissionException::class);
    +        $this->expectExceptionMessage('You are not allowed to update the language');
    +
    +        // unimpersonate and test the method
    +        $this->app->impersonate();
    +        $language->update(['name' => 'English']);
    +    }
     }
    
83fce5017597

Fix language permissions #6497

https://github.com/getkirby/kirbyAhmet BoraAug 19, 2024via ghsa
6 files changed · +199 5
  • i18n/translations/en.json+3 0 modified
    @@ -103,9 +103,12 @@
       "error.form.notSaved": "The form could not be saved",
     
       "error.language.code": "Please enter a valid code for the language",
    +  "error.language.create.permission": "You are not allowed to create a language",
    +  "error.language.delete.permission": "You are not allowed to delete the language",
       "error.language.duplicate": "The language already exists",
       "error.language.name": "Please enter a valid name for the language",
       "error.language.notFound": "The language could not be found",
    +  "error.language.update.permission": "You are not allowed to update the language",
     
       "error.layout.validation.block": "There's an error in block {blockIndex} in layout {layoutIndex}",
       "error.layout.validation.settings": "There's an error in layout {index} settings",
    
  • panel/src/components/Views/LanguagesView.vue+21 3 modified
    @@ -7,6 +7,7 @@
     				<k-button-group slot="left">
     					<k-button
     						:text="$t('language.create')"
    +						:disabled="!$permissions.languages.create"
     						icon="add"
     						@click="$dialog('languages/create')"
     					/>
    @@ -30,14 +31,23 @@
     							v-if="secondaryLanguages.length"
     							:items="secondaryLanguages"
     						/>
    -						<k-empty v-else icon="globe" @click="$dialog('languages/create')">
    +						<k-empty
    +							v-else
    +							icon="globe"
    +							:disabled="!$permissions.languages.create"
    +							@click="$dialog('languages/create')"
    +						>
     							{{ $t("languages.secondary.empty") }}
     						</k-empty>
     					</section>
     				</template>
     
     				<template v-else-if="languages.length === 0">
    -					<k-empty icon="globe" @click="$dialog('languages/create')">
    +					<k-empty
    +						icon="globe"
    +						:disabled="!$permissions.languages.create"
    +						@click="$dialog('languages/create')"
    +					>
     						{{ $t("languages.empty") }}
     					</k-empty>
     				</template>
    @@ -66,20 +76,28 @@ export default {
     					icon: "globe"
     				},
     				link: () => {
    +					if (!this.$permissions.languages.update) {
    +						return null;
    +					}
    +
     					this.$dialog(`languages/${language.id}/update`);
     				},
     				options: [
     					{
     						icon: "edit",
     						text: this.$t("edit"),
    +						disabled: !this.$permissions.languages.update,
     						click() {
     							this.$dialog(`languages/${language.id}/update`);
     						}
     					},
     					{
     						icon: "trash",
     						text: this.$t("delete"),
    -						disabled: language.default && this.languages.length !== 1,
    +						disabled: (
    +							(language.default && this.languages.length !== 1) ||
    +							!this.$permissions.languages.delete
    +						),
     						click() {
     							this.$dialog(`languages/${language.id}/delete`);
     						}
    
  • src/Cms/Language.php+28 1 modified
    @@ -201,6 +201,16 @@ protected static function converter(string $from, string $to): bool
     	 */
     	public static function create(array $props)
     	{
    +		$kirby = App::instance();
    +		$user  = $kirby->user();
    +
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'create') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.create.permission']);
    +		}
    +
     		$props['code'] = Str::slug($props['code'] ?? null);
     		$kirby         = App::instance();
     		$languages     = $kirby->languages();
    @@ -238,10 +248,18 @@ public static function create(array $props)
     	public function delete(): bool
     	{
     		$kirby     = App::instance();
    +		$user      = $kirby->user();
     		$languages = $kirby->languages();
     		$code      = $this->code();
     		$isLast    = $languages->count() === 1;
     
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'delete') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.delete.permission']);
    +		}
    +
     		if (F::remove($this->root()) !== true) {
     			throw new Exception('The language could not be deleted');
     		}
    @@ -648,13 +666,22 @@ public function url(): string
     	 */
     	public function update(array $props = null)
     	{
    +		$kirby = App::instance();
    +		$user  = $kirby->user();
    +
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'update') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.update.permission']);
    +		}
    +
     		// don't change the language code
     		unset($props['code']);
     
     		// make sure the slug is nice and clean
     		$props['slug'] = Str::slug($props['slug'] ?? null);
     
    -		$kirby   = App::instance();
     		$updated = $this->clone($props);
     
     		// validate the updated language
    
  • src/Cms/Permissions.php+2 1 modified
    @@ -44,7 +44,8 @@ class Permissions
     		],
     		'languages' => [
     			'create' => true,
    -			'delete' => true
    +			'delete' => true,
    +			'update' => true
     		],
     		'pages' => [
     			'changeSlug'     => true,
    
  • tests/Cms/Languages/LanguagesTest.php+2 0 modified
    @@ -108,6 +108,8 @@ public function testMultipleDefault()
     
     	public function testCreate()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = $this->app->languages()->create([
     			'code' => 'tr'
     		]);
    
  • tests/Cms/Languages/LanguageTest.php+143 0 modified
    @@ -3,6 +3,7 @@
     namespace Kirby\Cms;
     
     use Kirby\Data\Data;
    +use Kirby\Exception\PermissionException;
     use Kirby\Filesystem\Dir;
     use Kirby\Filesystem\F;
     use PHPUnit\Framework\TestCase;
    @@ -471,6 +472,8 @@ public function testBaseUrl($kirbyUrl, $url, $expected)
     
     	public function testCreate()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
    @@ -482,19 +485,112 @@ public function testCreate()
     		$this->assertSame('/en', $language->url());
     	}
     
    +	/**
    +	 * @covers ::create
    +	 */
    +	public function testCreateNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name' => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => false
    +						]
    +					]
    +				],
    +			],
    +			'users' => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to create a language');
    +
    +		$app->impersonate('test@getkirby.com');
    +		Language::create([
    +			'code' => 'en'
    +		]);
    +	}
    +
    +	/**
    +	 * @covers ::create
    +	 */
    +	public function testCreateWithoutLoggedUser()
    +	{
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to create a language');
    +
    +		Language::create([
    +			'code' => 'en'
    +		]);
    +	}
    +
     	public function testDelete()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
     
     		$this->assertTrue($language->delete());
     	}
     
    +	/**
    +	 * @covers ::delete
    +	 */
    +	public function testDeleteNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name' => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => true,
    +							'delete' => false
    +						]
    +					]
    +				],
    +			],
    +			'users' => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to delete the language');
    +
    +		$app->impersonate('test@getkirby.com');
    +		$language = Language::create(['code' => 'en']);
    +		$language->delete();
    +	}
    +
    +	/**
    +	 * @covers ::delete
    +	 */
    +	public function testDeleteWithoutLoggedUser()
    +	{
    +		$this->app->impersonate('kirby');
    +		$language = Language::create(['code' => 'en']);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to delete the language');
    +
    +		// unimpersonate and test the method
    +		$this->app->impersonate();
    +		$language->delete();
    +	}
    +
     	public function testUpdate()
     	{
     		Dir::make($contentDir = $this->fixtures . '/content');
     
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
    @@ -503,4 +599,51 @@ public function testUpdate()
     
     		$this->assertSame('English', $language->name());
     	}
    +
    +	/**
    +	 * @covers ::update
    +	 */
    +	public function testUpdateNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name'        => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => true,
    +							'update' => false
    +						]
    +					]
    +				],
    +			],
    +			'users'      => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to update the language');
    +
    +		$app->impersonate('test@getkirby.com');
    +
    +		$language = Language::create(['code' => 'en']);
    +		$language->update(['name' => 'English']);
    +	}
    +
    +	/**
    +	 * @covers ::update
    +	 */
    +	public function testUpdateWithoutLoggedUser()
    +	{
    +		$this->app->impersonate('kirby');
    +		$language = Language::create(['code' => 'en']);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to update the language');
    +
    +		// unimpersonate and test the method
    +		$this->app->impersonate();
    +		$language->update(['name' => 'English']);
    +	}
     }
    
e647a177c756

Fix language permissions #6497

https://github.com/getkirby/kirbyAhmet BoraAug 19, 2024via ghsa
6 files changed · +208 9
  • i18n/translations/en.json+3 0 modified
    @@ -106,9 +106,12 @@
     	"error.form.notSaved": "The form could not be saved",
     
     	"error.language.code": "Please enter a valid code for the language",
    +	"error.language.create.permission": "You are not allowed to create a language",
    +	"error.language.delete.permission": "You are not allowed to delete the language",
     	"error.language.duplicate": "The language already exists",
     	"error.language.name": "Please enter a valid name for the language",
     	"error.language.notFound": "The language could not be found",
    +	"error.language.update.permission": "You are not allowed to update the language",
     
     	"error.layout.validation.block": "There's an error on the \"{field}\" field in block {blockIndex} using the \"{fieldset}\" block type in layout {layoutIndex}",
     	"error.layout.validation.settings": "There's an error in layout {index} settings",
    
  • panel/src/components/Views/LanguagesView.vue+21 3 modified
    @@ -7,6 +7,7 @@
     				<k-button-group slot="left">
     					<k-button
     						:text="$t('language.create')"
    +						:disabled="!$permissions.languages.create"
     						icon="add"
     						@click="$dialog('languages/create')"
     					/>
    @@ -30,14 +31,23 @@
     							v-if="secondaryLanguages.length"
     							:items="secondaryLanguages"
     						/>
    -						<k-empty v-else icon="globe" @click="$dialog('languages/create')">
    +						<k-empty
    +							v-else
    +							icon="globe"
    +							:disabled="!$permissions.languages.create"
    +							@click="$dialog('languages/create')"
    +						>
     							{{ $t("languages.secondary.empty") }}
     						</k-empty>
     					</section>
     				</template>
     
     				<template v-else-if="languages.length === 0">
    -					<k-empty icon="globe" @click="$dialog('languages/create')">
    +					<k-empty
    +						icon="globe"
    +						:disabled="!$permissions.languages.create"
    +						@click="$dialog('languages/create')"
    +					>
     						{{ $t("languages.empty") }}
     					</k-empty>
     				</template>
    @@ -66,20 +76,28 @@ export default {
     					icon: "globe"
     				},
     				link: () => {
    +					if (!this.$permissions.languages.update) {
    +						return null;
    +					}
    +
     					this.$dialog(`languages/${language.id}/update`);
     				},
     				options: [
     					{
     						icon: "edit",
     						text: this.$t("edit"),
    +						disabled: !this.$permissions.languages.update,
     						click() {
     							this.$dialog(`languages/${language.id}/update`);
     						}
     					},
     					{
     						icon: "trash",
     						text: this.$t("delete"),
    -						disabled: language.default && this.languages.length !== 1,
    +						disabled: (
    +							(language.default && this.languages.length !== 1) ||
    +							!this.$permissions.languages.delete
    +						),
     						click() {
     							this.$dialog(`languages/${language.id}/delete`);
     						}
    
  • src/Cms/Language.php+28 2 modified
    @@ -201,8 +201,17 @@ protected static function converter(string $from, string $to): bool
     	 */
     	public static function create(array $props)
     	{
    +		$kirby = App::instance();
    +		$user  = $kirby->user();
    +
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'create') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.create.permission']);
    +		}
    +
     		$props['code'] = Str::slug($props['code'] ?? null);
    -		$kirby         = App::instance();
     		$languages     = $kirby->languages();
     
     		// make the first language the default language
    @@ -256,10 +265,18 @@ public static function create(array $props)
     	public function delete(): bool
     	{
     		$kirby     = App::instance();
    +		$user      = $kirby->user();
     		$languages = $kirby->languages();
     		$code      = $this->code();
     		$isLast    = $languages->count() === 1;
     
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'delete') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.delete.permission']);
    +		}
    +
     		// trigger before hook
     		$kirby->trigger('language.delete:before', [
     			'language' => $this
    @@ -672,13 +689,22 @@ public function url(): string
     	 */
     	public function update(array $props = null)
     	{
    +		$kirby = App::instance();
    +		$user  = $kirby->user();
    +
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'update') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.update.permission']);
    +		}
    +
     		// don't change the language code
     		unset($props['code']);
     
     		// make sure the slug is nice and clean
     		$props['slug'] = Str::slug($props['slug'] ?? null);
     
    -		$kirby   = App::instance();
     		$updated = $this->clone($props);
     
     		// validate the updated language
    
  • src/Cms/Permissions.php+2 1 modified
    @@ -44,7 +44,8 @@ class Permissions
     		],
     		'languages' => [
     			'create' => true,
    -			'delete' => true
    +			'delete' => true,
    +			'update' => true
     		],
     		'pages' => [
     			'changeSlug'     => true,
    
  • tests/Cms/Languages/LanguagesTest.php+2 0 modified
    @@ -108,6 +108,8 @@ public function testMultipleDefault()
     
     	public function testCreate()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = $this->app->languages()->create([
     			'code' => 'tr'
     		]);
    
  • tests/Cms/Languages/LanguageTest.php+152 3 modified
    @@ -4,6 +4,7 @@
     
     use Kirby\Data\Data;
     use Kirby\Exception\InvalidArgumentException;
    +use Kirby\Exception\PermissionException;
     use Kirby\Filesystem\Dir;
     use Kirby\Filesystem\F;
     use PHPUnit\Framework\TestCase;
    @@ -472,6 +473,8 @@ public function testBaseUrl($kirbyUrl, $url, $expected)
     
     	public function testCreate()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
    @@ -485,6 +488,8 @@ public function testCreate()
     
     	public function testDelete()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
    @@ -496,6 +501,8 @@ public function testUpdate()
     	{
     		Dir::make($contentDir = $this->fixtures . '/content');
     
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
    @@ -505,12 +512,55 @@ public function testUpdate()
     		$this->assertSame('English', $language->name());
     	}
     
    +	/**
    +	 * @covers ::create
    +	 */
    +	public function testCreateNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name' => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => false
    +						]
    +					]
    +				],
    +			],
    +			'users' => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to create a language');
    +
    +		$app->impersonate('test@getkirby.com');
    +		Language::create([
    +			'code' => 'en'
    +		]);
    +	}
    +
    +	/**
    +	 * @covers ::create
    +	 */
    +	public function testCreateWithoutLoggedUser()
    +	{
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to create a language');
    +
    +		Language::create([
    +			'code' => 'en'
    +		]);
    +	}
    +
     	public function testCreateHooks()
     	{
     		$calls = 0;
     		$phpunit = $this;
     
    -		new App([
    +		$app = $this->app->clone([
     			'roots' => [
     				'index' => $this->fixtures = __DIR__ . '/fixtures/CreateHooksTest',
     			],
    @@ -530,6 +580,8 @@ public function testCreateHooks()
     			]
     		]);
     
    +		$app->impersonate('kirby');
    +
     		Language::create([
     			'code' => 'de'
     		]);
    @@ -545,7 +597,7 @@ public function testUpdateHooks()
     		$this->fixtures = __DIR__ . '/fixtures/UpdateHooksTest';
     		Dir::make($this->fixtures . '/content');
     
    -		new App([
    +		$app = $this->app->clone([
     			'roots' => [
     				'index' => $this->fixtures,
     			],
    @@ -573,18 +625,113 @@ public function testUpdateHooks()
     			]
     		]);
     
    +		$app->impersonate('kirby');
    +
     		$language = Language::create(['code' => 'en']);
     		$language->update(['name' => 'English']);
     
     		$this->assertSame(2, $calls);
     	}
     
    +	/**
    +	 * @covers ::update
    +	 */
    +	public function testUpdateNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name'        => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => true,
    +							'update' => false
    +						]
    +					]
    +				],
    +			],
    +			'users'      => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to update the language');
    +
    +		$app->impersonate('test@getkirby.com');
    +
    +		$language = Language::create(['code' => 'en']);
    +		$language->update(['name' => 'English']);
    +	}
    +
    +	/**
    +	 * @covers ::update
    +	 */
    +	public function testUpdateWithoutLoggedUser()
    +	{
    +		$this->app->impersonate('kirby');
    +		$language = Language::create(['code' => 'en']);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to update the language');
    +
    +		// unimpersonate and test the method
    +		$this->app->impersonate();
    +		$language->update(['name' => 'English']);
    +	}
    +
    +	/**
    +	 * @covers ::delete
    +	 */
    +	public function testDeleteNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name' => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => true,
    +							'delete' => false
    +						]
    +					]
    +				],
    +			],
    +			'users' => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to delete the language');
    +
    +		$app->impersonate('test@getkirby.com');
    +		$language = Language::create(['code' => 'en']);
    +		$language->delete();
    +	}
    +
    +	/**
    +	 * @covers ::delete
    +	 */
    +	public function testDeleteWithoutLoggedUser()
    +	{
    +		$this->app->impersonate('kirby');
    +		$language = Language::create(['code' => 'en']);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to delete the language');
    +
    +		// unimpersonate and test the method
    +		$this->app->impersonate();
    +		$language->delete();
    +	}
    +
     	public function testDeleteHooks()
     	{
     		$calls = 0;
     		$phpunit = $this;
     
    -		new App([
    +		$app =new App([
     			'roots' => [
     				'index' => $this->fixtures = __DIR__ . '/fixtures/DeleteHooksTest',
     			],
    @@ -604,6 +751,8 @@ public function testDeleteHooks()
     			]
     		]);
     
    +		$app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en',
     			'name' => 'English'
    
38636655b054

Fix language permissions #6497

https://github.com/getkirby/kirbyAhmet BoraAug 19, 2024via ghsa
6 files changed · +208 9
  • i18n/translations/en.json+3 0 modified
    @@ -103,9 +103,12 @@
     	"error.form.notSaved": "The form could not be saved",
     
     	"error.language.code": "Please enter a valid code for the language",
    +	"error.language.create.permission": "You are not allowed to create a language",
    +	"error.language.delete.permission": "You are not allowed to delete the language",
     	"error.language.duplicate": "The language already exists",
     	"error.language.name": "Please enter a valid name for the language",
     	"error.language.notFound": "The language could not be found",
    +	"error.language.update.permission": "You are not allowed to update the language",
     
     	"error.layout.validation.block": "There's an error on the \"{field}\" field in block {blockIndex} using the \"{fieldset}\" block type in layout {layoutIndex}",
     	"error.layout.validation.settings": "There's an error in layout {index} settings",
    
  • panel/src/components/Views/LanguagesView.vue+21 3 modified
    @@ -7,6 +7,7 @@
     				<k-button-group slot="left">
     					<k-button
     						:text="$t('language.create')"
    +						:disabled="!$permissions.languages.create"
     						icon="add"
     						@click="$dialog('languages/create')"
     					/>
    @@ -30,14 +31,23 @@
     							v-if="secondaryLanguages.length"
     							:items="secondaryLanguages"
     						/>
    -						<k-empty v-else icon="globe" @click="$dialog('languages/create')">
    +						<k-empty
    +							v-else
    +							icon="globe"
    +							:disabled="!$permissions.languages.create"
    +							@click="$dialog('languages/create')"
    +						>
     							{{ $t("languages.secondary.empty") }}
     						</k-empty>
     					</section>
     				</template>
     
     				<template v-else-if="languages.length === 0">
    -					<k-empty icon="globe" @click="$dialog('languages/create')">
    +					<k-empty
    +						icon="globe"
    +						:disabled="!$permissions.languages.create"
    +						@click="$dialog('languages/create')"
    +					>
     						{{ $t("languages.empty") }}
     					</k-empty>
     				</template>
    @@ -66,20 +76,28 @@ export default {
     					icon: "globe"
     				},
     				link: () => {
    +					if (!this.$permissions.languages.update) {
    +						return null;
    +					}
    +
     					this.$dialog(`languages/${language.id}/update`);
     				},
     				options: [
     					{
     						icon: "edit",
     						text: this.$t("edit"),
    +						disabled: !this.$permissions.languages.update,
     						click() {
     							this.$dialog(`languages/${language.id}/update`);
     						}
     					},
     					{
     						icon: "trash",
     						text: this.$t("delete"),
    -						disabled: language.default && this.languages.length !== 1,
    +						disabled: (
    +							(language.default && this.languages.length !== 1) ||
    +							!this.$permissions.languages.delete
    +						),
     						click() {
     							this.$dialog(`languages/${language.id}/delete`);
     						}
    
  • src/Cms/Language.php+28 2 modified
    @@ -201,8 +201,17 @@ protected static function converter(string $from, string $to): bool
     	 */
     	public static function create(array $props)
     	{
    +		$kirby = App::instance();
    +		$user  = $kirby->user();
    +
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'create') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.create.permission']);
    +		}
    +
     		$props['code'] = Str::slug($props['code'] ?? null);
    -		$kirby         = App::instance();
     		$languages     = $kirby->languages();
     
     		// make the first language the default language
    @@ -256,10 +265,18 @@ public static function create(array $props)
     	public function delete(): bool
     	{
     		$kirby     = App::instance();
    +		$user      = $kirby->user();
     		$languages = $kirby->languages();
     		$code      = $this->code();
     		$isLast    = $languages->count() === 1;
     
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'delete') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.delete.permission']);
    +		}
    +
     		// trigger before hook
     		$kirby->trigger('language.delete:before', [
     			'language' => $this
    @@ -676,13 +693,22 @@ public function url(): string
     	 */
     	public function update(array $props = null)
     	{
    +		$kirby = App::instance();
    +		$user  = $kirby->user();
    +
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'update') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.update.permission']);
    +		}
    +
     		// don't change the language code
     		unset($props['code']);
     
     		// make sure the slug is nice and clean
     		$props['slug'] = Str::slug($props['slug'] ?? null);
     
    -		$kirby   = App::instance();
     		$updated = $this->clone($props);
     
     		// validate the updated language
    
  • src/Cms/Permissions.php+2 1 modified
    @@ -44,7 +44,8 @@ class Permissions
     		],
     		'languages' => [
     			'create' => true,
    -			'delete' => true
    +			'delete' => true,
    +			'update' => true
     		],
     		'pages' => [
     			'changeSlug'     => true,
    
  • tests/Cms/Languages/LanguagesTest.php+2 0 modified
    @@ -108,6 +108,8 @@ public function testMultipleDefault()
     
     	public function testCreate()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = $this->app->languages()->create([
     			'code' => 'tr'
     		]);
    
  • tests/Cms/Languages/LanguageTest.php+152 3 modified
    @@ -4,6 +4,7 @@
     
     use Kirby\Data\Data;
     use Kirby\Exception\InvalidArgumentException;
    +use Kirby\Exception\PermissionException;
     use Kirby\Filesystem\Dir;
     use Kirby\Filesystem\F;
     use PHPUnit\Framework\TestCase;
    @@ -472,6 +473,8 @@ public function testBaseUrl($kirbyUrl, $url, $expected)
     
     	public function testCreate()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
    @@ -485,6 +488,8 @@ public function testCreate()
     
     	public function testDelete()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
    @@ -496,6 +501,8 @@ public function testUpdate()
     	{
     		Dir::make($contentDir = $this->fixtures . '/content');
     
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
    @@ -505,12 +512,55 @@ public function testUpdate()
     		$this->assertSame('English', $language->name());
     	}
     
    +	/**
    +	 * @covers ::create
    +	 */
    +	public function testCreateNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name' => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => false
    +						]
    +					]
    +				],
    +			],
    +			'users' => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to create a language');
    +
    +		$app->impersonate('test@getkirby.com');
    +		Language::create([
    +			'code' => 'en'
    +		]);
    +	}
    +
    +	/**
    +	 * @covers ::create
    +	 */
    +	public function testCreateWithoutLoggedUser()
    +	{
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to create a language');
    +
    +		Language::create([
    +			'code' => 'en'
    +		]);
    +	}
    +
     	public function testCreateHooks()
     	{
     		$calls = 0;
     		$phpunit = $this;
     
    -		new App([
    +		$app = $this->app->clone([
     			'roots' => [
     				'index' => $this->fixtures = __DIR__ . '/fixtures/CreateHooksTest',
     			],
    @@ -530,6 +580,8 @@ public function testCreateHooks()
     			]
     		]);
     
    +		$app->impersonate('kirby');
    +
     		Language::create([
     			'code' => 'de'
     		]);
    @@ -545,7 +597,7 @@ public function testUpdateHooks()
     		$this->fixtures = __DIR__ . '/fixtures/UpdateHooksTest';
     		Dir::make($this->fixtures . '/content');
     
    -		new App([
    +		$app = $this->app->clone([
     			'roots' => [
     				'index' => $this->fixtures,
     			],
    @@ -573,18 +625,113 @@ public function testUpdateHooks()
     			]
     		]);
     
    +		$app->impersonate('kirby');
    +
     		$language = Language::create(['code' => 'en']);
     		$language->update(['name' => 'English']);
     
     		$this->assertSame(2, $calls);
     	}
     
    +	/**
    +	 * @covers ::update
    +	 */
    +	public function testUpdateNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name'        => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => true,
    +							'update' => false
    +						]
    +					]
    +				],
    +			],
    +			'users'      => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to update the language');
    +
    +		$app->impersonate('test@getkirby.com');
    +
    +		$language = Language::create(['code' => 'en']);
    +		$language->update(['name' => 'English']);
    +	}
    +
    +	/**
    +	 * @covers ::update
    +	 */
    +	public function testUpdateWithoutLoggedUser()
    +	{
    +		$this->app->impersonate('kirby');
    +		$language = Language::create(['code' => 'en']);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to update the language');
    +
    +		// unimpersonate and test the method
    +		$this->app->impersonate();
    +		$language->update(['name' => 'English']);
    +	}
    +
    +	/**
    +	 * @covers ::delete
    +	 */
    +	public function testDeleteNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name' => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => true,
    +							'delete' => false
    +						]
    +					]
    +				],
    +			],
    +			'users' => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to delete the language');
    +
    +		$app->impersonate('test@getkirby.com');
    +		$language = Language::create(['code' => 'en']);
    +		$language->delete();
    +	}
    +
    +	/**
    +	 * @covers ::delete
    +	 */
    +	public function testDeleteWithoutLoggedUser()
    +	{
    +		$this->app->impersonate('kirby');
    +		$language = Language::create(['code' => 'en']);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to delete the language');
    +
    +		// unimpersonate and test the method
    +		$this->app->impersonate();
    +		$language->delete();
    +	}
    +
     	public function testDeleteHooks()
     	{
     		$calls = 0;
     		$phpunit = $this;
     
    -		new App([
    +		$app =new App([
     			'roots' => [
     				'index' => $this->fixtures = __DIR__ . '/fixtures/DeleteHooksTest',
     			],
    @@ -604,6 +751,8 @@ public function testDeleteHooks()
     			]
     		]);
     
    +		$app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en',
     			'name' => 'English'
    
af9b0a58dea6

Fix language permissions #6497

https://github.com/getkirby/kirbyAhmet BoraAug 19, 2024via ghsa
6 files changed · +208 9
  • i18n/translations/en.json+3 0 modified
    @@ -106,9 +106,12 @@
     	"error.form.notSaved": "The form could not be saved",
     
     	"error.language.code": "Please enter a valid code for the language",
    +	"error.language.create.permission": "You are not allowed to create a language",
    +	"error.language.delete.permission": "You are not allowed to delete the language",
     	"error.language.duplicate": "The language already exists",
     	"error.language.name": "Please enter a valid name for the language",
     	"error.language.notFound": "The language could not be found",
    +	"error.language.update.permission": "You are not allowed to update the language",
     
     	"error.layout.validation.block": "There's an error on the \"{field}\" field in block {blockIndex} using the \"{fieldset}\" block type in layout {layoutIndex}",
     	"error.layout.validation.settings": "There's an error in layout {index} settings",
    
  • panel/src/components/Views/LanguagesView.vue+21 3 modified
    @@ -7,6 +7,7 @@
     				<k-button-group slot="left">
     					<k-button
     						:text="$t('language.create')"
    +						:disabled="!$permissions.languages.create"
     						icon="add"
     						@click="$dialog('languages/create')"
     					/>
    @@ -30,14 +31,23 @@
     							v-if="secondaryLanguages.length"
     							:items="secondaryLanguages"
     						/>
    -						<k-empty v-else icon="globe" @click="$dialog('languages/create')">
    +						<k-empty
    +							v-else
    +							icon="globe"
    +							:disabled="!$permissions.languages.create"
    +							@click="$dialog('languages/create')"
    +						>
     							{{ $t("languages.secondary.empty") }}
     						</k-empty>
     					</section>
     				</template>
     
     				<template v-else-if="languages.length === 0">
    -					<k-empty icon="globe" @click="$dialog('languages/create')">
    +					<k-empty
    +						icon="globe"
    +						:disabled="!$permissions.languages.create"
    +						@click="$dialog('languages/create')"
    +					>
     						{{ $t("languages.empty") }}
     					</k-empty>
     				</template>
    @@ -66,20 +76,28 @@ export default {
     					icon: "globe"
     				},
     				link: () => {
    +					if (!this.$permissions.languages.update) {
    +						return null;
    +					}
    +
     					this.$dialog(`languages/${language.id}/update`);
     				},
     				options: [
     					{
     						icon: "edit",
     						text: this.$t("edit"),
    +						disabled: !this.$permissions.languages.update,
     						click() {
     							this.$dialog(`languages/${language.id}/update`);
     						}
     					},
     					{
     						icon: "trash",
     						text: this.$t("delete"),
    -						disabled: language.default && this.languages.length !== 1,
    +						disabled: (
    +							(language.default && this.languages.length !== 1) ||
    +							!this.$permissions.languages.delete
    +						),
     						click() {
     							this.$dialog(`languages/${language.id}/delete`);
     						}
    
  • src/Cms/Language.php+28 2 modified
    @@ -201,8 +201,17 @@ protected static function converter(string $from, string $to): bool
     	 */
     	public static function create(array $props)
     	{
    +		$kirby = App::instance();
    +		$user  = $kirby->user();
    +
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'create') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.create.permission']);
    +		}
    +
     		$props['code'] = Str::slug($props['code'] ?? null);
    -		$kirby         = App::instance();
     		$languages     = $kirby->languages();
     
     		// make the first language the default language
    @@ -256,10 +265,18 @@ public static function create(array $props)
     	public function delete(): bool
     	{
     		$kirby     = App::instance();
    +		$user      = $kirby->user();
     		$languages = $kirby->languages();
     		$code      = $this->code();
     		$isLast    = $languages->count() === 1;
     
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'delete') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.delete.permission']);
    +		}
    +
     		// trigger before hook
     		$kirby->trigger('language.delete:before', [
     			'language' => $this
    @@ -672,13 +689,22 @@ public function url(): string
     	 */
     	public function update(array $props = null)
     	{
    +		$kirby = App::instance();
    +		$user  = $kirby->user();
    +
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'update') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.update.permission']);
    +		}
    +
     		// don't change the language code
     		unset($props['code']);
     
     		// make sure the slug is nice and clean
     		$props['slug'] = Str::slug($props['slug'] ?? null);
     
    -		$kirby   = App::instance();
     		$updated = $this->clone($props);
     
     		// validate the updated language
    
  • src/Cms/Permissions.php+2 1 modified
    @@ -44,7 +44,8 @@ class Permissions
     		],
     		'languages' => [
     			'create' => true,
    -			'delete' => true
    +			'delete' => true,
    +			'update' => true
     		],
     		'pages' => [
     			'changeSlug'     => true,
    
  • tests/Cms/Languages/LanguagesTest.php+2 0 modified
    @@ -108,6 +108,8 @@ public function testMultipleDefault()
     
     	public function testCreate()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = $this->app->languages()->create([
     			'code' => 'tr'
     		]);
    
  • tests/Cms/Languages/LanguageTest.php+152 3 modified
    @@ -4,6 +4,7 @@
     
     use Kirby\Data\Data;
     use Kirby\Exception\InvalidArgumentException;
    +use Kirby\Exception\PermissionException;
     use Kirby\Filesystem\Dir;
     use Kirby\Filesystem\F;
     use PHPUnit\Framework\TestCase;
    @@ -472,6 +473,8 @@ public function testBaseUrl($kirbyUrl, $url, $expected)
     
     	public function testCreate()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
    @@ -485,6 +488,8 @@ public function testCreate()
     
     	public function testDelete()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
    @@ -496,6 +501,8 @@ public function testUpdate()
     	{
     		Dir::make($contentDir = $this->fixtures . '/content');
     
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
    @@ -505,12 +512,55 @@ public function testUpdate()
     		$this->assertSame('English', $language->name());
     	}
     
    +	/**
    +	 * @covers ::create
    +	 */
    +	public function testCreateNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name' => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => false
    +						]
    +					]
    +				],
    +			],
    +			'users' => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to create a language');
    +
    +		$app->impersonate('test@getkirby.com');
    +		Language::create([
    +			'code' => 'en'
    +		]);
    +	}
    +
    +	/**
    +	 * @covers ::create
    +	 */
    +	public function testCreateWithoutLoggedUser()
    +	{
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to create a language');
    +
    +		Language::create([
    +			'code' => 'en'
    +		]);
    +	}
    +
     	public function testCreateHooks()
     	{
     		$calls = 0;
     		$phpunit = $this;
     
    -		new App([
    +		$app = $this->app->clone([
     			'roots' => [
     				'index' => $this->fixtures = __DIR__ . '/fixtures/CreateHooksTest',
     			],
    @@ -530,6 +580,8 @@ public function testCreateHooks()
     			]
     		]);
     
    +		$app->impersonate('kirby');
    +
     		Language::create([
     			'code' => 'de'
     		]);
    @@ -545,7 +597,7 @@ public function testUpdateHooks()
     		$this->fixtures = __DIR__ . '/fixtures/UpdateHooksTest';
     		Dir::make($this->fixtures . '/content');
     
    -		new App([
    +		$app = $this->app->clone([
     			'roots' => [
     				'index' => $this->fixtures,
     			],
    @@ -573,18 +625,113 @@ public function testUpdateHooks()
     			]
     		]);
     
    +		$app->impersonate('kirby');
    +
     		$language = Language::create(['code' => 'en']);
     		$language->update(['name' => 'English']);
     
     		$this->assertSame(2, $calls);
     	}
     
    +	/**
    +	 * @covers ::update
    +	 */
    +	public function testUpdateNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name'        => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => true,
    +							'update' => false
    +						]
    +					]
    +				],
    +			],
    +			'users'      => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to update the language');
    +
    +		$app->impersonate('test@getkirby.com');
    +
    +		$language = Language::create(['code' => 'en']);
    +		$language->update(['name' => 'English']);
    +	}
    +
    +	/**
    +	 * @covers ::update
    +	 */
    +	public function testUpdateWithoutLoggedUser()
    +	{
    +		$this->app->impersonate('kirby');
    +		$language = Language::create(['code' => 'en']);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to update the language');
    +
    +		// unimpersonate and test the method
    +		$this->app->impersonate();
    +		$language->update(['name' => 'English']);
    +	}
    +
    +	/**
    +	 * @covers ::delete
    +	 */
    +	public function testDeleteNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name' => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => true,
    +							'delete' => false
    +						]
    +					]
    +				],
    +			],
    +			'users' => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to delete the language');
    +
    +		$app->impersonate('test@getkirby.com');
    +		$language = Language::create(['code' => 'en']);
    +		$language->delete();
    +	}
    +
    +	/**
    +	 * @covers ::delete
    +	 */
    +	public function testDeleteWithoutLoggedUser()
    +	{
    +		$this->app->impersonate('kirby');
    +		$language = Language::create(['code' => 'en']);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to delete the language');
    +
    +		// unimpersonate and test the method
    +		$this->app->impersonate();
    +		$language->delete();
    +	}
    +
     	public function testDeleteHooks()
     	{
     		$calls = 0;
     		$phpunit = $this;
     
    -		new App([
    +		$app =new App([
     			'roots' => [
     				'index' => $this->fixtures = __DIR__ . '/fixtures/DeleteHooksTest',
     			],
    @@ -604,6 +751,8 @@ public function testDeleteHooks()
     			]
     		]);
     
    +		$app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en',
     			'name' => 'English'
    
ab95d172667c

Fix language permissions #6497

https://github.com/getkirby/kirbyAhmet BoraJul 4, 2024via ghsa
8 files changed · +242 19
  • config/areas/languages/views.php+11 5 modified
    @@ -12,12 +12,17 @@
     			return App::instance()->option('languages.variables', true) !== false;
     		},
     		'action'  => function (string $code) {
    +			$kirby        = App::instance();
     			$language     = Find::language($code);
     			$link         = '/languages/' . $language->code();
     			$strings      = [];
    -			$foundation   = App::instance()->defaultLanguage()->translations();
    +			$foundation   = $kirby->defaultLanguage()->translations();
     			$translations = $language->translations();
     
    +			// TODO: update following line and adapt for update and delete options
    +			// when new `languageVariables.*` permissions available
    +			$canUpdate = $kirby->user()?->role()->permissions()->for('languages', 'update') === true;
    +
     			ksort($foundation);
     
     			foreach ($foundation as $key => $value) {
    @@ -26,13 +31,14 @@
     					'value'   => $translations[$key] ?? null,
     					'options' => [
     						[
    -							'click' => 'update',
    -							'icon'  => 'edit',
    -							'text'  => I18n::translate('edit'),
    +							'click'    => 'update',
    +							'disabled' => $canUpdate === false,
    +							'icon'     => 'edit',
    +							'text'     => I18n::translate('edit'),
     						],
     						[
     							'click'    => 'delete',
    -							'disabled' => $language->isDefault() === false,
    +							'disabled' => $canUpdate === false || $language->isDefault() === false,
     							'icon'     => 'trash',
     							'text'     => I18n::translate('delete'),
     						]
    
  • i18n/translations/en.json+3 0 modified
    @@ -126,9 +126,12 @@
     	"error.form.notSaved": "The form could not be saved",
     
     	"error.language.code": "Please enter a valid code for the language",
    +	"error.language.create.permission": "You are not allowed to create a language",
    +	"error.language.delete.permission": "You are not allowed to delete the language",
     	"error.language.duplicate": "The language already exists",
     	"error.language.name": "Please enter a valid name for the language",
     	"error.language.notFound": "The language could not be found",
    +	"error.language.update.permission": "You are not allowed to update the language",
     
     	"error.layout.validation.block": "There's an error on the \"{field}\" field in block {blockIndex} using the \"{fieldset}\" block type in layout {layoutIndex}",
     	"error.layout.validation.settings": "There's an error in layout {index} settings",
    
  • panel/src/components/Views/Languages/LanguagesView.vue+15 3 modified
    @@ -5,6 +5,7 @@
     
     			<k-button-group slot="buttons">
     				<k-button
    +					:disabled="!$panel.permissions.languages.create"
     					:text="$t('language.create')"
     					icon="add"
     					size="sm"
    @@ -24,14 +25,23 @@
     					v-if="secondaryLanguages.length"
     					:items="secondaryLanguages"
     				/>
    -				<k-empty v-else icon="translate" @click="$dialog('languages/create')">
    +				<k-empty
    +					v-else
    +					icon="translate"
    +					:disabled="!$panel.permissions.languages.create"
    +					@click="$dialog('languages/create')"
    +				>
     					{{ $t("languages.secondary.empty") }}
     				</k-empty>
     			</k-section>
     		</template>
     
     		<template v-else-if="languages.length === 0">
    -			<k-empty icon="translate" @click="$dialog('languages/create')">
    +			<k-empty
    +				icon="translate"
    +				:disabled="!$panel.permissions.languages.create"
    +				@click="$dialog('languages/create')"
    +			>
     				{{ $t("languages.empty") }}
     			</k-empty>
     		</template>
    @@ -79,12 +89,14 @@ export default {
     					{
     						icon: "cog",
     						text: this.$t("settings"),
    +						disabled: !this.$panel.permissions.languages.update,
     						click: () => this.$dialog(`languages/${language.id}/update`)
     					},
     					{
    +						when: language.deletable,
     						icon: "trash",
     						text: this.$t("delete"),
    -						disabled: language.deletable === false,
    +						disabled: !this.$panel.permissions.languages.delete,
     						click: () => this.$dialog(`languages/${language.id}/delete`)
     					}
     				]
    
  • panel/src/components/Views/Languages/LanguageView.vue+26 2 modified
    @@ -4,7 +4,7 @@
     			<k-prev-next :prev="prev" :next="next" />
     		</template>
     
    -		<k-header :editable="true" @edit="update()">
    +		<k-header :editable="canUpdate" @edit="update()">
     			{{ name }}
     
     			<template #buttons>
    @@ -18,6 +18,7 @@
     						variant="filled"
     					/>
     					<k-button
    +						:disabled="!canUpdate"
     						:title="$t('settings')"
     						icon="cog"
     						size="sm"
    @@ -26,6 +27,7 @@
     					/>
     					<k-button
     						v-if="deletable"
    +						:disabled="!$panel.permissions.languages.delete"
     						:title="$t('delete')"
     						icon="trash"
     						size="sm"
    @@ -42,8 +44,12 @@
     
     		<k-section
     			:buttons="[
    +				/**
    +				 * @todo update disabled prop when new `languageVariables.*` permissions available
    +				 */
     				{
     					click: createTranslation,
    +					disabled: !canUpdate,
     					icon: 'add',
     					text: $t('add')
     				}
    @@ -63,13 +69,14 @@
     							mobile: true
     						}
     					}"
    +					:disabled="!canUpdate"
     					:rows="translations"
     					@cell="updateTranslation"
     					@option="option"
     				/>
     			</template>
     			<template v-else>
    -				<k-empty icon="translate" @click="createTranslation">
    +				<k-empty :disabled="!canUpdate" icon="translate" @click="createTranslation">
     					{{ $t("language.variables.empty") }}
     				</k-empty>
     			</template>
    @@ -95,11 +102,24 @@ export default {
     		translations: Array,
     		url: String
     	},
    +	computed: {
    +		canUpdate() {
    +			return this.$panel.permissions.languages.update;
    +		}
    +	},
     	methods: {
     		createTranslation() {
    +			if (!this.canUpdate) {
    +				return;
    +			}
    +
     			this.$dialog(`languages/${this.id}/translations/create`);
     		},
     		option(option, row) {
    +			if (!this.canUpdate) {
    +				return;
    +			}
    +
     			// for the compatibility of the encoded url in different environments,
     			// it is also encoded with base64 to reduce special characters
     			this.$dialog(
    @@ -121,6 +141,10 @@ export default {
     			});
     		},
     		updateTranslation({ row }) {
    +			if (!this.canUpdate) {
    +				return;
    +			}
    +
     			// for the compatibility of the encoded url in different environments,
     			// it is also encoded with base64 to reduce special characters
     			this.$dialog(
    
  • src/Cms/Language.php+29 2 modified
    @@ -6,6 +6,7 @@
     use Kirby\Exception\Exception;
     use Kirby\Exception\InvalidArgumentException;
     use Kirby\Exception\LogicException;
    +use Kirby\Exception\PermissionException;
     use Kirby\Filesystem\F;
     use Kirby\Toolkit\Locale;
     use Kirby\Toolkit\Str;
    @@ -145,8 +146,17 @@ public function code(): string
     	 */
     	public static function create(array $props): static
     	{
    +		$kirby = App::instance();
    +		$user  = $kirby->user();
    +
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'create') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.create.permission']);
    +		}
    +
     		$props['code'] = Str::slug($props['code'] ?? null);
    -		$kirby         = App::instance();
     		$languages     = $kirby->languages();
     
     		// make the first language the default language
    @@ -204,8 +214,16 @@ public static function create(array $props): static
     	public function delete(): bool
     	{
     		$kirby = App::instance();
    +		$user  = $kirby->user();
     		$code  = $this->code();
     
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'delete') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.delete.permission']);
    +		}
    +
     		if ($this->isDeletable() === false) {
     			throw new Exception('The language cannot be deleted');
     		}
    @@ -497,13 +515,22 @@ public function url(): string
     	 */
     	public function update(array $props = null): static
     	{
    +		$kirby = App::instance();
    +		$user  = $kirby->user();
    +
    +		if (
    +			$user === null ||
    +			$user->role()->permissions()->for('languages', 'update') === false
    +		) {
    +			throw new PermissionException(['key' => 'language.update.permission']);
    +		}
    +
     		// don't change the language code
     		unset($props['code']);
     
     		// make sure the slug is nice and clean
     		$props['slug'] = Str::slug($props['slug'] ?? null);
     
    -		$kirby   = App::instance();
     		$updated = $this->clone($props);
     
     		if (isset($props['translations']) === true) {
    
  • src/Cms/Permissions.php+2 1 modified
    @@ -41,7 +41,8 @@ class Permissions
     		],
     		'languages' => [
     			'create' => true,
    -			'delete' => true
    +			'delete' => true,
    +			'update' => true
     		],
     		'pages' => [
     			'access'     	 => true,
    
  • tests/Cms/Languages/LanguagesTest.php+2 0 modified
    @@ -143,6 +143,8 @@ public function testMultipleDefault()
     
     	public function testCreate()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = $this->app->languages()->create([
     			'code' => 'tr'
     		]);
    
  • tests/Cms/Languages/LanguageTest.php+154 6 modified
    @@ -5,6 +5,7 @@
     use Kirby\Data\Data;
     use Kirby\Exception\InvalidArgumentException;
     use Kirby\Exception\LogicException;
    +use Kirby\Exception\PermissionException;
     use Kirby\Filesystem\Dir;
     use Kirby\Filesystem\F;
     use Kirby\TestCase;
    @@ -88,6 +89,8 @@ public function testBaseUrl($kirbyUrl, $url, $expected)
     	 */
     	public function testCreate()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
    @@ -99,6 +102,49 @@ public function testCreate()
     		$this->assertSame('/en', $language->url());
     	}
     
    +	/**
    +	 * @covers ::create
    +	 */
    +	public function testCreateNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name' => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => false
    +						]
    +					]
    +				],
    +			],
    +			'users' => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to create a language');
    +
    +		$app->impersonate('test@getkirby.com');
    +		Language::create([
    +			'code' => 'en'
    +		]);
    +	}
    +
    +	/**
    +	 * @covers ::create
    +	 */
    +	public function testCreateWithoutLoggedUser()
    +	{
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to create a language');
    +
    +		Language::create([
    +			'code' => 'en'
    +		]);
    +	}
    +
     	/**
     	 * @covers ::create
     	 */
    @@ -107,10 +153,7 @@ public function testCreateHooks()
     		$calls = 0;
     		$phpunit = $this;
     
    -		new App([
    -			'roots' => [
    -				'index' => static::TMP,
    -			],
    +		$app = $this->app->clone([
     			'hooks' => [
     				'language.create:before' => function (Language $language, array $input) use ($phpunit, &$calls) {
     					$phpunit->assertInstanceOf(Language::class, $language);
    @@ -127,6 +170,8 @@ public function testCreateHooks()
     			]
     		]);
     
    +		$app->impersonate('kirby');
    +
     		Language::create([
     			'code' => 'de'
     		]);
    @@ -150,10 +195,58 @@ public function testCodeAndId()
     	 */
     	public function testDelete()
     	{
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create(['code' => 'en']);
     		$this->assertTrue($language->delete());
     	}
     
    +	/**
    +	 * @covers ::delete
    +	 */
    +	public function testDeleteNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name' => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => true,
    +							'delete' => false
    +						]
    +					]
    +				],
    +			],
    +			'users' => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to delete the language');
    +
    +		$app->impersonate('test@getkirby.com');
    +		$language = Language::create(['code' => 'en']);
    +		$language->delete();
    +	}
    +
    +	/**
    +	 * @covers ::delete
    +	 */
    +	public function testDeleteWithoutLoggedUser()
    +	{
    +		$this->app->impersonate('kirby');
    +		$language = Language::create(['code' => 'en']);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to delete the language');
    +
    +		// unimpersonate and test the method
    +		$this->app->impersonate();
    +		$language->delete();
    +	}
    +
     	/**
     	 * @covers ::delete
     	 */
    @@ -162,7 +255,7 @@ public function testDeleteHooks()
     		$calls = 0;
     		$phpunit = $this;
     
    -		new App([
    +		$app = new App([
     			'roots' => [
     				'index' => static::TMP,
     			],
    @@ -182,6 +275,8 @@ public function testDeleteHooks()
     			]
     		]);
     
    +		$app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en',
     			'name' => 'English'
    @@ -649,6 +744,8 @@ public function testUpdate()
     	{
     		Dir::make(static::TMP . '/content');
     
    +		$this->app->impersonate('kirby');
    +
     		$language = Language::create([
     			'code' => 'en'
     		]);
    @@ -669,6 +766,8 @@ public function testUpdateDefault()
     			]
     		]);
     
    +		$app->impersonate('kirby');
    +
     		$this->assertFalse($app->multilang());
     		$this->assertNull($app->defaultLanguage());
     		$this->assertSame(0, $app->languages()->count());
    @@ -717,7 +816,7 @@ public function testUpdateHooks()
     
     		Dir::make(static::TMP . '/content');
     
    -		new App([
    +		$app = new App([
     			'roots' => [
     				'index' => static::TMP,
     			],
    @@ -745,12 +844,61 @@ public function testUpdateHooks()
     			]
     		]);
     
    +		$app->impersonate('kirby');
    +
     		$language = Language::create(['code' => 'en']);
     		$language->update(['name' => 'English']);
     
     		$this->assertSame(2, $calls);
     	}
     
    +	/**
    +	 * @covers ::update
    +	 */
    +	public function testUpdateNoPermissions()
    +	{
    +		$app = $this->app->clone([
    +			'blueprints' => [
    +				'users/editor' => [
    +					'name'        => 'editor',
    +					'permissions' => [
    +						'languages' => [
    +							'create' => true,
    +							'update' => false
    +						]
    +					]
    +				],
    +			],
    +			'users'      => [
    +				['email' => 'test@getkirby.com', 'role' => 'editor']
    +			]
    +		]);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to update the language');
    +
    +		$app->impersonate('test@getkirby.com');
    +
    +		$language = Language::create(['code' => 'en']);
    +		$language->update(['name' => 'English']);
    +	}
    +
    +	/**
    +	 * @covers ::update
    +	 */
    +	public function testUpdateWithoutLoggedUser()
    +	{
    +		$this->app->impersonate('kirby');
    +		$language = Language::create(['code' => 'en']);
    +
    +		$this->expectException(PermissionException::class);
    +		$this->expectExceptionMessage('You are not allowed to update the language');
    +
    +		// unimpersonate and test the method
    +		$this->app->impersonate();
    +		$language->update(['name' => 'English']);
    +	}
    +
     	/**
     	 * @covers ::variable
     	 */
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

15

News mentions

0

No linked articles in our index yet.