Skip to content

Commit 9d5bec3

Browse files
steveoniluccasmmg
authored andcommitted
Update Members permission to team form + ODP-370 (#642)
* members can not * update role: Editor can create sub-team and admin moving subteam from public to private should not throw error * disabled Public option if parent is private * Trigger CI --------- Co-authored-by: Luccas Mateus <[email protected]>
1 parent c18a6ae commit 9d5bec3

File tree

7 files changed

+142
-35
lines changed

7 files changed

+142
-35
lines changed

ckan-backend-dev/src/ckanext-wri/ckanext/wri/logic/action/create.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
from ckan.logic.action.create import (
3939
organization_create as old_organization_create)
4040

41+
from ckan.logic.action.get import (
42+
organization_show as old_organization_show)
43+
4144
import ckan.authz as authz
4245

4346
NotificationGetUserViewedActivity: TypeAlias = None
@@ -873,27 +876,33 @@ def download_event_create(context: Context, data_dict: DataDict):
873876
return download_event_list_dictize(events, context)
874877

875878

879+
import copy
880+
876881
@logic.side_effect_free
877882
def organization_create(context, data_dict):
878883
visibility = data_dict.get('visibility', "public")
884+
885+
886+
temp_context = {"model": context["model"], "session": context["session"], "user": context["user"]}
887+
888+
889+
parent_org = data_dict.get("parent")
890+
parent_org = parent_org.get("value") if parent_org else None
891+
if parent_org:
892+
parent_org = old_organization_show(temp_context, {"id": parent_org})
893+
users = parent_org.get("users", [])
894+
username = context.get("user")
895+
if users and not authz.is_sysadmin(context.get("user")):
896+
user_capacity = [user.get("capacity") for user in users if user.get("name") == username]
897+
if not any(role in user_capacity for role in ["admin", "editor"]):
898+
raise ValidationError({"message": _("User does not have admin access to create a sub team")})
899+
if parent_org.get("visibility", "public") == "private" and visibility == "public":
900+
raise ValidationError({"message": _("Parent Organization has private visibility and cannot create public teams")})
901+
902+
else:
903+
if not authz.is_sysadmin(context.get("user")):
904+
raise ValidationError({"message": _("Only sysadmins can create public teams without a parent")})
879905

880-
if visibility == "public":
881-
parent_org = data_dict.get("parent")
882-
parent_org = parent_org.get("value") if parent_org else None
883-
if parent_org:
884-
parent_org = logic.get_action("organization_show")(context, {"id": parent_org})
885-
users = parent_org.get("users", [])
886-
username = context.get("user")
887-
if users:
888-
user_capacity = [user.get("capacity") for user in users if user.get("name") == username]
889-
if "admin" not in user_capacity:
890-
raise ValidationError({"message": _("User does not have admin access to create a sub team")})
891-
if parent_org.get("visibility", "public") == "private":
892-
raise ValidationError({"message": _("Parent Organization has private visibility and cannot create public teams")})
893-
894-
else:
895-
if not authz.is_sysadmin(context.get("user")):
896-
raise ValidationError({"message": _("Only sysadmins can create public teams without a parent")})
897906

898907
result = old_organization_create(context, data_dict)
899908
return result

ckan-backend-dev/src/ckanext-wri/ckanext/wri/logic/action/get.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1785,8 +1785,8 @@ def organization_patch(context, data_dict):
17851785
isSysadmin = authz.is_sysadmin(user)
17861786

17871787
if not isSysadmin:
1788-
1789-
old_org = get_action("organization_show")(context, data_dict)
1788+
temp_context = {"model": context["model"], "session": context["session"], "user": context["user"]}
1789+
old_org = get_action("organization_show")(temp_context, data_dict)
17901790
old_parent = old_org.get("groups", [])
17911791
if len(old_parent) == 0 or data_dict.get("parent") is None:
17921792
raise ValidationError({"message": _("You can't edit or create a Parent Team")})
@@ -1798,7 +1798,8 @@ def organization_patch(context, data_dict):
17981798
if visibility == "private":
17991799
users = old_org.get("users", [])
18001800
if users:
1801-
user_capacity = [user.get("capacity") for user in users if user.get("name") == user]
1801+
c_user = str(user)
1802+
user_capacity = [user.get("capacity") for user in users if user.get("name") == c_user]
18021803
if "admin" not in user_capacity:
18031804
raise ValidationError({"message": _("You can't update visibility of a team")})
18041805

@@ -1807,7 +1808,8 @@ def organization_patch(context, data_dict):
18071808
parent_org = data_dict.get("parent")
18081809
parent_org = parent_org.get("value") if parent_org else None
18091810
if parent_org:
1810-
parent_org = get_action("organization_show")(context, {"id": parent_org})
1811+
temp_context = {"model": context["model"], "session": context["session"], "user": context["user"]}
1812+
parent_org = get_action("organization_show")(temp_context, {"id": parent_org})
18111813
if parent_org.get("visibility", "public") == "private":
18121814
raise ValidationError({"message": _("Parent Team has private visibility and cannot create public teams")})
18131815

@@ -1817,7 +1819,8 @@ def organization_patch(context, data_dict):
18171819
"fq": f"(organization:({data_dict.get('name')}) AND visibility_type:(public OR internal))",
18181820
"include_private": False # Include private datasets in the search
18191821
}
1820-
public_package = get_action("package_search")(context, rdata_dict)
1822+
temp_context = {"model": context["model"], "session": context["session"], "user": context["user"]}
1823+
public_package = get_action("package_search")(temp_context, rdata_dict)
18211824
if public_package.get("count") > 0:
18221825
raise ValidationError({"message": _(f"Team has {public_package.get('count')} public dataset(s) and cannot be made private")})
18231826
return old_organization_patch(context, data_dict)

ckan-backend-dev/src/ckanext-wri/ckanext/wri/plugin.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
resource_create,
3434
old_package_create,
3535
download_event_create,
36+
organization_create
3637
)
3738
from ckanext.wri.logic.action.update import (
3839
notification_update,
@@ -71,6 +72,7 @@
7172
organization_show,
7273
package_show,
7374
get_download_events,
75+
7476
)
7577

7678
from ckanext.wri.logic.action.delete import pending_dataset_delete
@@ -272,6 +274,8 @@ def get_actions(self):
272274
"package_update": package_update,
273275
"download_event_create": download_event_create,
274276
"download_event_list": get_download_events,
277+
"organization_create": organization_create,
278+
275279
"prefect_send_error_callback": send_error_callback,
276280
}
277281

deployment/frontend/src/components/_shared/SimpleSelect.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface Option<V> {
1515
label: string
1616
value: V
1717
default?: boolean
18+
disbaled?: boolean
1819
}
1920

2021
interface SimpleSelectProps<T extends FieldValues, V extends Object> {
@@ -109,12 +110,13 @@ export default function SimpleSelect<T extends FieldValues, V extends Object>({
109110
{options.map((option) => (
110111
<Listbox.Option
111112
key={option.value}
113+
disabled={option.disabled}
112114
className={({ active }) =>
113115
classNames(
114116
active
115117
? 'bg-blue-800 text-white'
116118
: 'text-gray-900',
117-
`relative cursor-default select-none py-2 pl-3 pr-9`
119+
`relative cursor-default select-none py-2 pl-3 pr-9 ${option.disabled ? 'opacity-50' : ''}`
118120
)
119121
}
120122
value={option}
@@ -133,7 +135,7 @@ export default function SimpleSelect<T extends FieldValues, V extends Object>({
133135
{option?.visibility &&
134136
option.visibility ===
135137
'private' ? (
136-
<>{' '}&#128274;</>
138+
<> &#128274;</>
137139
) : (
138140
''
139141
)}

deployment/frontend/src/components/dashboard/teams/forms/CreateTeamForm.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { api } from '@/utils/api'
1111
import notify from '@/utils/notify'
1212
import { ErrorAlert } from '@/components/_shared/Alerts'
1313
import { useRouter } from 'next/router'
14+
import { z } from 'zod'
15+
import { useSession } from 'next-auth/react'
1416

1517
const links = [
1618
{ label: 'Teams', url: '/dashboard/teams', current: false },
@@ -20,8 +22,45 @@ const links = [
2022
export default function CreateTeamForm() {
2123
const [errorMessage, setErrorMessage] = useState<string | null>(null)
2224
const router = useRouter()
25+
const possibleParents = api.teams.getAllTeams.useQuery()
26+
const { data: session } = useSession()
27+
const sysadmin = session?.user?.sysadmin ?? false
28+
29+
const TeamSchemaRefine = TeamSchema.superRefine((val, ctx) => {
30+
if (val.visibility.value === 'public' && val.parent) {
31+
const parent = possibleParents.data?.find(
32+
(team) => team.name === val.parent?.value
33+
)
34+
const visibility = parent?.visibility
35+
const isPrivate = parent && visibility === 'private'
36+
if (isPrivate) {
37+
ctx.addIssue({
38+
code: z.ZodIssueCode.custom,
39+
path: ['parent'],
40+
message:
41+
'Parent Organization has private visibility and cannot create public teams',
42+
})
43+
}
44+
}
45+
46+
if (!sysadmin && val.parent) {
47+
const parent = possibleParents.data?.find(
48+
(team) => team.name === val.parent?.value
49+
)
50+
const capacity = parent?.capacity
51+
const isAdmin = parent && !['admin', 'editor'].includes(capacity)
52+
if (isAdmin) {
53+
ctx.addIssue({
54+
code: z.ZodIssueCode.custom,
55+
path: ['parent'],
56+
message:
57+
'User does not have admin access to create a sub team',
58+
})
59+
}
60+
}
61+
})
2362
const formObj = useForm<TeamFormType>({
24-
resolver: zodResolver(TeamSchema),
63+
resolver: zodResolver(TeamSchemaRefine),
2564
})
2665

2766
const createTeam = api.teams.createTeam.useMutation({

deployment/frontend/src/components/dashboard/teams/forms/EditTeamForm.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@ import { Tab } from '@headlessui/react'
2222
import { Fragment } from 'react'
2323
import classNames from '@/utils/classnames'
2424
import { Members } from '../metadata/Members'
25+
import { z } from 'zod'
26+
import { useSession } from 'next-auth/react'
2527

2628
type TeamOutput = RouterOutput['teams']['getTeam']
2729

2830
export default function EditTeamForm({ team }: { team: TeamOutput }) {
2931
const [errorMessage, setErrorMessage] = useState<string | null>(null)
3032
const [deleteOpen, setDeleteOpen] = useState(false)
3133
const router = useRouter()
34+
const possibleParents = api.teams.getAllTeams.useQuery()
35+
const { data: session } = useSession()
36+
const sysadmin = session?.user?.sysadmin ?? false
3237
const links = [
3338
{ label: 'Teams', url: '/dashboard/teams', current: false },
3439
{
@@ -38,6 +43,40 @@ export default function EditTeamForm({ team }: { team: TeamOutput }) {
3843
},
3944
]
4045

46+
const TeamSchemaRefine = TeamSchema.superRefine((val, ctx) => {
47+
if (val.visibility.value === 'public' && val.parent) {
48+
const parent = possibleParents.data?.find(
49+
(team) => team.name === val.parent?.value
50+
)
51+
const visibility = parent?.visibility
52+
const isPrivate = parent && visibility === 'private'
53+
if (isPrivate) {
54+
ctx.addIssue({
55+
code: z.ZodIssueCode.custom,
56+
path: ['parent'],
57+
message:
58+
'Parent Organization has private visibility and cannot create public teams',
59+
})
60+
}
61+
}
62+
63+
if (!sysadmin && val.parent) {
64+
const org = possibleParents.data?.find((team) => team.id === val.id)
65+
66+
const capacity = org?.capacity
67+
const isAdmin = org && capacity !== 'admin'
68+
69+
if (isAdmin) {
70+
ctx.addIssue({
71+
code: z.ZodIssueCode.custom,
72+
path: ['parent'],
73+
message:
74+
'User does not have admin access to edit a sub team',
75+
})
76+
}
77+
}
78+
})
79+
4180
const formObj = useForm<TeamFormType>({
4281
defaultValues: {
4382
...team,
@@ -61,7 +100,7 @@ export default function EditTeamForm({ team }: { team: TeamOutput }) {
61100
},
62101
})),
63102
},
64-
resolver: zodResolver(TeamSchema),
103+
resolver: zodResolver(TeamSchemaRefine),
65104
})
66105

67106
const utils = api.useContext()

deployment/frontend/src/components/dashboard/teams/forms/TeamForm.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { UploadResult } from '@uppy/core'
1313
import { api } from '@/utils/api'
1414
import { P, match } from 'ts-pattern'
1515
import Spinner from '@/components/_shared/Spinner'
16+
import { useSession } from 'next-auth/react'
17+
import { useEffect } from 'react'
1618

1719
function ToolTipOnEdit() {
1820
return (
@@ -67,7 +69,11 @@ export default function TeamForm({
6769
watch,
6870
formState: { errors, isSubmitting },
6971
} = formObj
70-
const possibleParents = api.teams.getAllTeams.useQuery()
72+
73+
const possibleParents = api.teams.getAllTeams.useQuery(undefined, {
74+
refetchOnMount: false,
75+
})
76+
7177
return (
7278
<div className="grid grid-cols-1 items-start gap-x-12 gap-y-4 py-5 lg:grid-cols-2 xxl:gap-x-24">
7379
<div className="flex flex-col justify-start gap-y-4">
@@ -182,13 +188,13 @@ export default function TeamForm({
182188
editing
183189
? ToolTipOnEdit()
184190
: watch('parent')?.value !== '' &&
185-
possibleParents.data?.find(
186-
(a) =>
187-
a.name === watch('parent')?.value &&
188-
a.visibility === 'private'
189-
)
190-
? ToolTipForSubTeam()
191-
: TooltipForParent()
191+
possibleParents.data?.find(
192+
(a) =>
193+
a.name === watch('parent')?.value &&
194+
a.visibility === 'private'
195+
)
196+
? ToolTipForSubTeam()
197+
: TooltipForParent()
192198
}
193199
required
194200
>
@@ -210,6 +216,11 @@ export default function TeamForm({
210216
value: 'private',
211217
default: true,
212218
},
219+
{
220+
label: 'Public',
221+
value: 'public',
222+
disabled: true,
223+
},
213224
]
214225
: [
215226
{ label: 'Public', value: 'public' },
@@ -218,7 +229,7 @@ export default function TeamForm({
218229
}
219230
placeholder="Select visibility"
220231
/>
221-
<ErrorDisplay name="parent" errors={errors} />
232+
<ErrorDisplay name="visibility" errors={errors} />
222233
</InputGroupCustom>
223234
</div>
224235
</div>

0 commit comments

Comments
 (0)