User space handling
All checks were successful
Test API before pr merge / test-lint (pull_request) Successful in 14s
Test before pr merge / test-lint (pull_request) Successful in 33s
Test before pr merge / test-translation-lint (pull_request) Successful in 33s
Test before pr merge / test-before-merge (pull_request) Successful in 1m40s

This commit is contained in:
Sven Heidemann 2025-04-28 10:34:01 +02:00
parent e378393813
commit af67b679ba
15 changed files with 173 additions and 68 deletions

View File

@ -213,7 +213,6 @@ class QueryABC(ObjectType):
f"{field.name}: {field.input_type.__name__} {kwargs[field.input_key]}" f"{field.name}: {field.input_type.__name__} {kwargs[field.input_key]}"
) )
input_obj = field.input_type(kwargs[field.input_key]) input_obj = field.input_type(kwargs[field.input_key])
del kwargs[field.input_key] del kwargs[field.input_key]
return await resolver_wrapper(input_obj, mutation, info, **kwargs) return await resolver_wrapper(input_obj, mutation, info, **kwargs)

View File

@ -58,7 +58,7 @@ class MutationFieldBuilder(FieldBuilderABC):
self._resolver = resolver_wrapper self._resolver = resolver_wrapper
return self return self
def with_input(self, input_type: Type[InputABC], input_key: str = None) -> Self: def with_input(self, input_type: Type[InputABC], input_key: str = "input") -> Self:
self._input_type = input_type self._input_type = input_type
self._input_key = input_key self._input_key = input_key
return self return self

View File

@ -46,11 +46,14 @@ class Mutation(MutationABC):
self.add_mutation_type( self.add_mutation_type(
"group", "group",
"Group", "Group",
require_any_permission=[ require_any=(
Permissions.groups_create, [
Permissions.groups_update, Permissions.groups_create,
Permissions.groups_delete, Permissions.groups_update,
], Permissions.groups_delete,
],
[by_user_setup_mutation],
),
) )
self.add_mutation_type( self.add_mutation_type(
"shortUrl", "shortUrl",

View File

@ -2,8 +2,10 @@ from typing import Optional
from api.route import Route from api.route import Route
from api_graphql.abc.mutation_abc import MutationABC from api_graphql.abc.mutation_abc import MutationABC
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
from api_graphql.input.group_create_input import GroupCreateInput from api_graphql.input.group_create_input import GroupCreateInput
from api_graphql.input.group_update_input import GroupUpdateInput from api_graphql.input.group_update_input import GroupUpdateInput
from api_graphql.require_any_resolvers import by_user_setup_mutation
from core.configuration.feature_flags import FeatureFlags from core.configuration.feature_flags import FeatureFlags
from core.configuration.feature_flags_enum import FeatureFlagsEnum from core.configuration.feature_flags_enum import FeatureFlagsEnum
from core.logger import APILogger from core.logger import APILogger
@ -20,27 +22,30 @@ class GroupMutation(MutationABC):
def __init__(self): def __init__(self):
MutationABC.__init__(self, "Group") MutationABC.__init__(self, "Group")
self.mutation( self.field(
"create", MutationFieldBuilder("create")
self.resolve_create, .with_resolver(self.resolve_create)
GroupCreateInput, .with_input(GroupCreateInput)
require_any_permission=[Permissions.groups_create], .with_require_any([Permissions.groups_create], [by_user_setup_mutation])
) )
self.mutation(
"update", self.field(
self.resolve_update, MutationFieldBuilder("update")
GroupUpdateInput, .with_resolver(self.resolve_update)
require_any_permission=[Permissions.groups_update], .with_input(GroupUpdateInput)
.with_require_any([Permissions.groups_update], [by_user_setup_mutation])
) )
self.mutation(
"delete", self.field(
self.resolve_delete, MutationFieldBuilder("delete")
require_any_permission=[Permissions.groups_delete], .with_resolver(self.resolve_delete)
.with_require_any([Permissions.groups_delete], [by_user_setup_mutation])
) )
self.mutation(
"restore", self.field(
self.resolve_restore, MutationFieldBuilder("restore")
require_any_permission=[Permissions.groups_delete], .with_resolver(self.resolve_restore)
.with_require_any([Permissions.groups_delete], [by_user_setup_mutation])
) )
@staticmethod @staticmethod
@ -75,6 +80,10 @@ class GroupMutation(MutationABC):
async def resolve_create(cls, obj: GroupCreateInput, *_): async def resolve_create(cls, obj: GroupCreateInput, *_):
logger.debug(f"create group: {obj.__dict__}") logger.debug(f"create group: {obj.__dict__}")
already_exists = await groupDao.find_by({Group.name: obj.name})
if len(already_exists) > 0:
raise ValueError(f"Group {obj.name} already exists")
group = Group( group = Group(
0, 0,
obj.name, obj.name,
@ -98,6 +107,12 @@ class GroupMutation(MutationABC):
raise ValueError(f"Group with id {obj.id} not found") raise ValueError(f"Group with id {obj.id} not found")
if obj.name is not None: if obj.name is not None:
already_exists = await groupDao.find_by(
{Group.name: obj.name, Group.id: {"ne": obj.id}}
)
if len(already_exists) > 0:
raise ValueError(f"Group {obj.name} already exists")
group = await groupDao.get_by_id(obj.id) group = await groupDao.get_by_id(obj.id)
group.name = obj.name group.name = obj.name
await groupDao.update(group) await groupDao.update(group)

View File

@ -58,6 +58,10 @@ class ShortUrlMutation(MutationABC):
async def resolve_create(obj: ShortUrlCreateInput, *_): async def resolve_create(obj: ShortUrlCreateInput, *_):
logger.debug(f"create short_url: {obj.__dict__}") logger.debug(f"create short_url: {obj.__dict__}")
already_exists = await shortUrlDao.find_by({ShortUrl.short_url: obj.short_url})
if len(already_exists) > 0:
raise ValueError(f"Short URL {obj.short_url} already exists")
short_url = ShortUrl( short_url = ShortUrl(
0, 0,
obj.short_url, obj.short_url,
@ -82,6 +86,11 @@ class ShortUrlMutation(MutationABC):
short_url = await shortUrlDao.get_by_id(obj.id) short_url = await shortUrlDao.get_by_id(obj.id)
if obj.short_url is not None: if obj.short_url is not None:
already_exists = await shortUrlDao.find_by(
{ShortUrl.short_url: obj.short_url}
)
if len(already_exists) > 0:
raise ValueError(f"Short URL {obj.short_url} already exists")
short_url.short_url = obj.short_url short_url.short_url = obj.short_url
if obj.target_url is not None: if obj.target_url is not None:

View File

@ -36,11 +36,8 @@ async def by_user_setup_resolver(ctx: QueryContext) -> bool:
if not FeatureFlags.has_feature(FeatureFlagsEnum.per_user_setup): if not FeatureFlags.has_feature(FeatureFlagsEnum.per_user_setup):
return False return False
return all(x.user_setup_id == ctx.user.id for x in ctx.data.nodes) return all(x.user_id == ctx.user.id for x in ctx.data.nodes)
async def by_user_setup_mutation(ctx: QueryContext) -> bool: async def by_user_setup_mutation(ctx: QueryContext) -> bool:
if not FeatureFlags.has_feature(FeatureFlagsEnum.per_user_setup): return await FeatureFlags.has_feature(FeatureFlagsEnum.per_user_setup)
return False
return all(x.user_setup_id == ctx.user.id for x in ctx.data.nodes)

View File

@ -15,6 +15,7 @@ class QueryContext:
data: Any, data: Any,
user: Optional[User], user: Optional[User],
user_permissions: Optional[list[Permissions]], user_permissions: Optional[list[Permissions]],
is_mutation: bool = False,
*args, *args,
**kwargs **kwargs
): ):
@ -31,11 +32,17 @@ class QueryContext:
self._resolve_info = arg self._resolve_info = arg
continue continue
self._filter = kwargs.get("filter", {}) self._filter = kwargs.get("filters", {})
self._sort = kwargs.get("sort", {}) self._sort = kwargs.get("sort", {})
self._skip = get_value(kwargs, "skip", int) self._skip = get_value(kwargs, "skip", int)
self._take = get_value(kwargs, "take", int) self._take = get_value(kwargs, "take", int)
self._input = kwargs.get("input", None)
self._args = args
self._kwargs = kwargs
self._is_mutation = is_mutation
@property @property
def data(self): def data(self):
return self._data return self._data
@ -64,5 +71,21 @@ class QueryContext:
def take(self) -> Optional[int]: def take(self) -> Optional[int]:
return self._take return self._take
@property
def input(self) -> Optional[Any]:
return self._input
@property
def args(self) -> tuple:
return self._args
@property
def kwargs(self) -> dict:
return self._kwargs
@property
def is_mutation(self) -> bool:
return self._is_mutation
def has_permission(self, permission: Permissions) -> bool: def has_permission(self, permission: Permissions) -> bool:
return permission.value in self._user_permissions return permission.value in self._user_permissions

View File

@ -228,7 +228,7 @@ export class DomainsDataService
return this.apollo return this.apollo
.mutate<{ domain: { delete: boolean } }>({ .mutate<{ domain: { delete: boolean } }>({
mutation: gql` mutation: gql`
mutation deleteDomain($id: ID!) { mutation deleteDomain($id: Int!) {
domain { domain {
delete(id: $id) delete(id: $id)
} }
@ -251,7 +251,7 @@ export class DomainsDataService
return this.apollo return this.apollo
.mutate<{ domain: { restore: boolean } }>({ .mutate<{ domain: { restore: boolean } }>({
mutation: gql` mutation: gql`
mutation restoreDomain($id: ID!) { mutation restoreDomain($id: Int!) {
domain { domain {
restore(id: $id) restore(id: $id)
} }

View File

@ -27,8 +27,9 @@
type="text" type="text"
formControlName="name"/> formControlName="name"/>
</div> </div>
<div class="divider"></div> <div *ngIf="!isPerUserSetup" class="divider"></div>
<p-multiSelect <p-multiSelect
*ngIf="!isPerUserSetup"
[options]="roles" [options]="roles"
formControlName="roles" formControlName="roles"
optionLabel="name" optionLabel="name"

View File

@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from 'src/app/service/toast.service';
import { FormPageBase } from 'src/app/core/base/form-page-base'; import { FormPageBase } from 'src/app/core/base/form-page-base';
@ -10,28 +10,40 @@ import {
import { GroupsDataService } from 'src/app/modules/admin/groups/groups.data.service'; import { GroupsDataService } from 'src/app/modules/admin/groups/groups.data.service';
import { Role } from 'src/app/model/entities/role'; import { Role } from 'src/app/model/entities/role';
import { CommonDataService } from 'src/app/modules/shared/service/common-data.service'; import { CommonDataService } from 'src/app/modules/shared/service/common-data.service';
import { FeatureFlagService } from 'src/app/service/feature-flag.service';
@Component({ @Component({
selector: 'app-group-form-page', selector: 'app-group-form-page',
templateUrl: './group-form-page.component.html', templateUrl: './group-form-page.component.html',
styleUrl: './group-form-page.component.scss', styleUrl: './group-form-page.component.scss',
}) })
export class GroupFormPageComponent extends FormPageBase< export class GroupFormPageComponent
Group, extends FormPageBase<
GroupCreateInput, Group,
GroupUpdateInput, GroupCreateInput,
GroupsDataService GroupUpdateInput,
> { GroupsDataService
>
implements OnInit
{
roles: Role[] = []; roles: Role[] = [];
isPerUserSetup = true;
constructor( constructor(
private features: FeatureFlagService,
private toast: ToastService, private toast: ToastService,
private cds: CommonDataService private cds: CommonDataService
) { ) {
super(); super();
this.cds.getAllRoles().subscribe(roles => { }
this.roles = roles;
}); async ngOnInit() {
this.isPerUserSetup = await this.features.get('PerUserSetup');
if (!this.isPerUserSetup) {
this.cds.getAllRoles().subscribe(roles => {
this.roles = roles;
});
}
if (!this.nodeId) { if (!this.nodeId) {
this.node = this.new(); this.node = this.new();

View File

@ -238,7 +238,7 @@ export class GroupsDataService
return this.apollo return this.apollo
.mutate<{ group: { delete: boolean } }>({ .mutate<{ group: { delete: boolean } }>({
mutation: gql` mutation: gql`
mutation deleteGroup($id: ID!) { mutation deleteGroup($id: Int!) {
group { group {
delete(id: $id) delete(id: $id)
} }
@ -261,7 +261,7 @@ export class GroupsDataService
return this.apollo return this.apollo
.mutate<{ group: { restore: boolean } }>({ .mutate<{ group: { restore: boolean } }>({
mutation: gql` mutation: gql`
mutation restoreGroup($id: ID!) { mutation restoreGroup($id: Int!) {
group { group {
restore(id: $id) restore(id: $id)
} }

View File

@ -21,7 +21,8 @@ const routes: Routes = [
component: GroupFormPageComponent, component: GroupFormPageComponent,
canActivate: [PermissionGuard], canActivate: [PermissionGuard],
data: { data: {
permissions: [PermissionsEnum.apiKeysCreate], permissions: [PermissionsEnum.groupsCreate],
checkByPerUserSetup: true,
}, },
}, },
{ {
@ -29,7 +30,8 @@ const routes: Routes = [
component: GroupFormPageComponent, component: GroupFormPageComponent,
canActivate: [PermissionGuard], canActivate: [PermissionGuard],
data: { data: {
permissions: [PermissionsEnum.apiKeysUpdate], permissions: [PermissionsEnum.groupsUpdate],
checkByPerUserSetup: true,
}, },
}, },
{ {
@ -37,7 +39,8 @@ const routes: Routes = [
component: HistoryComponent, component: HistoryComponent,
canActivate: [PermissionGuard], canActivate: [PermissionGuard],
data: { data: {
permissions: [PermissionsEnum.domains], permissions: [PermissionsEnum.groups],
checkByPerUserSetup: true,
}, },
}, },
], ],

View File

@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { PageBase } from 'src/app/core/base/page-base'; import { PageBase } from 'src/app/core/base/page-base';
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from 'src/app/service/toast.service';
import { ConfirmationDialogService } from 'src/app/service/confirmation-dialog.service'; import { ConfirmationDialogService } from 'src/app/service/confirmation-dialog.service';
@ -6,28 +6,63 @@ import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
import { Group } from 'src/app/model/entities/group'; import { Group } from 'src/app/model/entities/group';
import { GroupsDataService } from 'src/app/modules/admin/groups/groups.data.service'; import { GroupsDataService } from 'src/app/modules/admin/groups/groups.data.service';
import { GroupsColumns } from 'src/app/modules/admin/groups/groups.columns'; import { GroupsColumns } from 'src/app/modules/admin/groups/groups.columns';
import { AuthService } from 'src/app/service/auth.service';
import { ConfigService } from 'src/app/service/config.service';
import { FeatureFlagService } from 'src/app/service/feature-flag.service';
@Component({ @Component({
selector: 'app-groups', selector: 'app-groups',
templateUrl: './groups.page.html', templateUrl: './groups.page.html',
styleUrl: './groups.page.scss', styleUrl: './groups.page.scss',
}) })
export class GroupsPage extends PageBase< export class GroupsPage
Group, extends PageBase<Group, GroupsDataService, GroupsColumns>
GroupsDataService, implements OnInit
GroupsColumns {
> {
constructor( constructor(
private toast: ToastService, private toast: ToastService,
private confirmation: ConfirmationDialogService private confirmation: ConfirmationDialogService,
private auth: AuthService,
private config: ConfigService,
private features: FeatureFlagService
) { ) {
super(true, { super(true);
read: [PermissionsEnum.groups], }
create: [PermissionsEnum.groupsCreate],
update: [PermissionsEnum.groupsUpdate], async ngOnInit() {
delete: [PermissionsEnum.groupsDelete], this.requiredPermissions = {
restore: [PermissionsEnum.groupsDelete], read: (await this.features.get('PerUserSetup'))
}); ? []
: [PermissionsEnum.groups],
create: (await this.features.get('PerUserSetup'))
? []
: (await this.auth.hasAnyPermissionLazy(
this.requiredPermissions.create ?? []
))
? (this.requiredPermissions.create ?? [])
: [],
update: (await this.features.get('PerUserSetup'))
? []
: (await this.auth.hasAnyPermissionLazy(
this.requiredPermissions.update ?? []
))
? (this.requiredPermissions.update ?? [])
: [],
delete: (await this.features.get('PerUserSetup'))
? []
: (await this.auth.hasAnyPermissionLazy(
this.requiredPermissions.delete ?? []
))
? (this.requiredPermissions.delete ?? [])
: [],
restore: (await this.features.get('PerUserSetup'))
? []
: (await this.auth.hasAnyPermissionLazy(
this.requiredPermissions.restore ?? []
))
? (this.requiredPermissions.restore ?? [])
: [],
};
} }
load(silent?: boolean): void { load(silent?: boolean): void {

View File

@ -67,9 +67,13 @@ export class ShortUrlsDataService
id id
name name
} }
...DB_MODEL
} }
} }
} }
${DB_MODEL_FRAGMENT}
`, `,
variables: { variables: {
filter: [{ group: { deleted: { equal: false } } }, ...(filter ?? [])], filter: [{ group: { deleted: { equal: false } } }, ...(filter ?? [])],
@ -98,9 +102,13 @@ export class ShortUrlsDataService
id id
name name
} }
...DB_MODEL
} }
} }
} }
${DB_MODEL_FRAGMENT}
`, `,
variables: { variables: {
filter: [{ group: { isNull: true } }, ...(filter ?? [])], filter: [{ group: { isNull: true } }, ...(filter ?? [])],
@ -315,7 +323,7 @@ export class ShortUrlsDataService
return this.apollo return this.apollo
.mutate<{ shortUrl: { delete: boolean } }>({ .mutate<{ shortUrl: { delete: boolean } }>({
mutation: gql` mutation: gql`
mutation deleteShortUrl($id: ID!) { mutation deleteShortUrl($id: Int!) {
shortUrl { shortUrl {
delete(id: $id) delete(id: $id)
} }
@ -338,7 +346,7 @@ export class ShortUrlsDataService
return this.apollo return this.apollo
.mutate<{ shortUrl: { restore: boolean } }>({ .mutate<{ shortUrl: { restore: boolean } }>({
mutation: gql` mutation: gql`
mutation restoreShortUrl($id: ID!) { mutation restoreShortUrl($id: Int!) {
shortUrl { shortUrl {
restore(id: $id) restore(id: $id)
} }

View File

@ -41,7 +41,7 @@
icon="pi pi-pencil" icon="pi pi-pencil"
tooltipPosition="left" tooltipPosition="left"
pTooltip="{{ 'table.update' | translate }}" pTooltip="{{ 'table.update' | translate }}"
[disabled]="url?.deleted" [disabled]="url.deleted"
routerLink="edit/{{ url.id }}"></p-button> routerLink="edit/{{ url.id }}"></p-button>
<p-button <p-button
*ngIf="hasPermissions.delete" *ngIf="hasPermissions.delete"
@ -49,10 +49,10 @@
icon="pi pi-trash" icon="pi pi-trash"
tooltipPosition="left" tooltipPosition="left"
pTooltip="{{ 'table.delete' | translate }}" pTooltip="{{ 'table.delete' | translate }}"
[disabled]="url?.deleted" [disabled]="url.deleted"
(click)="delete(url)"></p-button> (click)="delete(url)"></p-button>
<p-button <p-button
*ngIf="url?.deleted && hasPermissions.restore" *ngIf="url.deleted && hasPermissions.restore"
class="icon-btn btn" class="icon-btn btn"
icon="pi pi-undo" icon="pi pi-undo"
tooltipPosition="left" tooltipPosition="left"