Skip to content

Commit 2e93969

Browse files
authored
Merge pull request #456 from ghga-de/develop
Prepare Release 1.0.4
2 parents f4e6b7c + 954d5f7 commit 2e93969

File tree

8 files changed

+362
-11
lines changed

8 files changed

+362
-11
lines changed

datameta/api/download.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ def get_file_url(request: Request) -> HTTPTemporaryRedirect:
5454
response = {
5555
'fileId' : get_identifier(db_file),
5656
'fileUrl' : f"{request.host_url}{url}",
57-
'expires' : expires_at.isoformat() + "+00:00"
57+
'expires' : expires_at.isoformat() + "+00:00",
58+
'checksum' : db_file.checksum
5859
}
5960

6061
if redirect:

datameta/api/groups.py

+37-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@
2525
from sqlalchemy.orm import joinedload
2626
from sqlalchemy.exc import IntegrityError
2727

28-
from pyramid.httpexceptions import HTTPNoContent, HTTPForbidden
28+
from pyramid.httpexceptions import HTTPNoContent, HTTPForbidden, HTTPNotFound
29+
30+
31+
@dataclass
32+
class GroupResponseElement(DataHolderBase):
33+
"""Class for Group Information Request communication to OpenApi"""
34+
id: dict
35+
name: str
2936

3037

3138
class GroupSubmissions:
@@ -110,6 +117,35 @@ class ChangeGroupName(DataHolderBase):
110117
new_group_name: str
111118

112119

120+
@view_config(
121+
route_name="groups_id",
122+
renderer="json",
123+
request_method="GET",
124+
openapi=True
125+
)
126+
def get(request: Request):
127+
128+
# Authenticate the user
129+
auth_user = security.revalidate_user(request)
130+
131+
group_id = request.matchdict["id"]
132+
db = request.dbsession
133+
134+
# Get the targeted user
135+
target_group = resource_by_id(db, Group, group_id)
136+
137+
if target_group is None:
138+
raise HTTPNotFound()
139+
140+
if not authz.view_group(auth_user, target_group):
141+
raise HTTPForbidden()
142+
143+
return GroupResponseElement(
144+
id = get_identifier(target_group),
145+
name = target_group.name,
146+
)
147+
148+
113149
@view_config(
114150
route_name="groups_id",
115151
renderer='json',

datameta/api/openapi.yaml

+121-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
openapi: 3.0.0
1616
info:
1717
description: DataMeta
18-
version: 1.0.1
18+
version: 1.3.0
1919
title: DataMeta
2020

2121
servers:
@@ -111,7 +111,7 @@ paths:
111111
content:
112112
application/json:
113113
schema:
114-
$ref: "#/components/schemas/UserResponse"
114+
$ref: "#/components/schemas/WhoamiResponse"
115115
'401':
116116
description: Unauthorized
117117
'400':
@@ -341,6 +341,41 @@ paths:
341341
description: Internal Server Error
342342

343343
/users/{id}:
344+
get:
345+
summary: Get user information
346+
description: >-
347+
Get information about a user.
348+
tags:
349+
- Authentication and Users
350+
operationId: UserInformationRequest
351+
parameters:
352+
- name: id
353+
in: path
354+
description: ID of the user
355+
required: true
356+
schema:
357+
type: string
358+
responses:
359+
'200':
360+
description: OK
361+
content:
362+
application/json:
363+
schema:
364+
$ref: '#/components/schemas/UserResponse'
365+
'400':
366+
description: Validation Error
367+
content:
368+
application/json:
369+
schema:
370+
$ref: "#/components/schemas/ErrorModel"
371+
'401':
372+
description: Unauthorized
373+
'403':
374+
description: Forbidden
375+
'404':
376+
description: Not found
377+
'500':
378+
description: Internal Server Error
344379
put:
345380
summary: Update a user's credentials and status
346381
description: Update a user's name, group, admin status and enabled status.
@@ -957,6 +992,41 @@ paths:
957992
description: Internal Server Error
958993

959994
/groups/{id}:
995+
get:
996+
summary: Get group information
997+
description: >-
998+
Get information about a group.
999+
tags:
1000+
- Groups
1001+
operationId: GroupInformationRequest
1002+
parameters:
1003+
- name: id
1004+
in: path
1005+
description: ID of the group
1006+
required: true
1007+
schema:
1008+
type: string
1009+
responses:
1010+
'200':
1011+
description: OK
1012+
content:
1013+
application/json:
1014+
schema:
1015+
$ref: '#/components/schemas/GroupResponse'
1016+
'400':
1017+
description: Validation Error
1018+
content:
1019+
application/json:
1020+
schema:
1021+
$ref: "#/components/schemas/ErrorModel"
1022+
'401':
1023+
description: Unauthorized
1024+
'403':
1025+
description: Forbidden
1026+
'404':
1027+
description: Not found
1028+
'500':
1029+
description: Internal Server Error
9601030
put:
9611031
summary: Change the name of a group.
9621032
description: >-
@@ -1282,10 +1352,13 @@ components:
12821352
expires:
12831353
type: string
12841354
format: date-time
1355+
checksum:
1356+
type: string
12851357
required:
12861358
- fileId
12871359
- fileUrl
12881360
- expires
1361+
- checksum
12891362
additionalProperties: false
12901363

12911364
ApiKeyList:
@@ -1634,8 +1707,20 @@ components:
16341707
required:
16351708
- name
16361709
additionalProperties: false
1637-
1638-
UserResponse:
1710+
1711+
GroupResponse:
1712+
type: object
1713+
properties:
1714+
id:
1715+
$ref: "#/components/schemas/Identifier"
1716+
name:
1717+
type: string
1718+
required:
1719+
- id
1720+
- name
1721+
additionalProperties: false
1722+
1723+
WhoamiResponse:
16391724
type: object
16401725
properties:
16411726
id:
@@ -1667,6 +1752,38 @@ components:
16671752
- group
16681753
additionalProperties: false
16691754

1755+
UserResponse:
1756+
type: object
1757+
properties:
1758+
id:
1759+
$ref: "#/components/schemas/Identifier"
1760+
name:
1761+
type: string
1762+
groupAdmin:
1763+
type: boolean
1764+
nullable: true
1765+
siteAdmin:
1766+
type: boolean
1767+
nullable: true
1768+
siteRead:
1769+
type: boolean
1770+
nullable: true
1771+
email:
1772+
type: string
1773+
nullable: true
1774+
group:
1775+
type: object
1776+
properties:
1777+
id:
1778+
$ref: "#/components/schemas/Identifier"
1779+
name:
1780+
type: string
1781+
required:
1782+
- id
1783+
- name
1784+
- group
1785+
additionalProperties: false
1786+
16701787
UserUpdateRequest:
16711788
type: object
16721789
properties:

datameta/api/ui/admin.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ def v_admin_put_request(request):
6464

6565
# Check if the requesting user is authorized. Both the request group as well as the
6666
# administratively selected group have to be the requesting user's group
67-
if not req_user.site_admin and (reg_req.group_id != req_user.group_id.uuid or newuser_group_id != req_user.group_id.uuid):
67+
68+
if not req_user.site_admin and (reg_req.group_id != req_user.group_id or newuser_group_id != str(req_user.group.uuid)):
6869
return HTTPUnauthorized()
6970

7071
# Check if the specified group id is valid

datameta/api/users.py

+56-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
16+
from typing import Optional
17+
1518
from pyramid.view import view_config
1619
from pyramid.request import Request
1720
from pyramid.httpexceptions import HTTPNoContent, HTTPForbidden, HTTPNotFound
@@ -37,6 +40,32 @@ class UserUpdateRequest(DataHolderBase):
3740

3841
@dataclass
3942
class UserResponseElement(DataHolderBase):
43+
"""Class for User Update Request communication to OpenApi"""
44+
id: dict
45+
name: str # why is this name when it is called fullname in the db?
46+
group: dict
47+
group_admin: Optional[bool] = None
48+
site_admin: Optional[bool] = None
49+
site_read: Optional[bool] = None
50+
email: Optional[str] = None
51+
52+
@classmethod
53+
def from_user(cls, target_user, requesting_user):
54+
restricted_fields = dict()
55+
56+
if authz.view_restricted_user_info(requesting_user, target_user):
57+
restricted_fields.update({
58+
"group_admin": target_user.group_admin,
59+
"site_admin": target_user.site_admin,
60+
"site_read": target_user.site_read,
61+
"email": target_user.email
62+
})
63+
64+
return cls(id=get_identifier(target_user), name=target_user.fullname, group=get_identifier(target_user.group), **restricted_fields)
65+
66+
67+
@dataclass
68+
class WhoamiResponseElement(DataHolderBase):
4069
"""Class for User Update Request communication to OpenApi"""
4170
id: dict
4271
name: str # why is this name when it is called fullname in the db?
@@ -53,11 +82,11 @@ class UserResponseElement(DataHolderBase):
5382
request_method="GET",
5483
openapi=True
5584
)
56-
def get_whoami(request: Request) -> UserResponseElement:
85+
def get_whoami(request: Request) -> WhoamiResponseElement:
5786

5887
auth_user = security.revalidate_user(request)
5988

60-
return UserResponseElement(
89+
return WhoamiResponseElement(
6190
id = get_identifier(auth_user),
6291
name = auth_user.fullname,
6392
group_admin = auth_user.group_admin,
@@ -68,6 +97,31 @@ def get_whoami(request: Request) -> UserResponseElement:
6897
)
6998

7099

100+
@view_config(
101+
route_name="user_id",
102+
renderer="json",
103+
request_method="GET",
104+
openapi=True
105+
)
106+
def get(request: Request):
107+
108+
# Authenticate the user
109+
auth_user = security.revalidate_user(request)
110+
111+
user_id = request.matchdict["id"]
112+
db = request.dbsession
113+
114+
# Get the targeted user
115+
target_user = resource_by_id(db, User, user_id)
116+
if target_user is None:
117+
raise HTTPNotFound()
118+
119+
if not authz.view_user(auth_user, target_user):
120+
raise HTTPForbidden()
121+
122+
return UserResponseElement.from_user(target_user, auth_user)
123+
124+
71125
@view_config(
72126
route_name="user_id",
73127
renderer='json',

datameta/security/authz.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,44 @@
1616
from ..models import MetaDatum, User
1717

1818

19+
def is_own_group(user, group):
20+
return user.group.id == group.id
21+
22+
1923
def user_is_target(user, target_user):
2024
return user.id == target_user.id
2125

2226

2327
def has_group_rights(user, group):
24-
return user.site_admin or (user.group_admin and user.group.id == group.id)
28+
return user.site_admin or (user.group_admin and is_own_group(user, group))
2529

2630

2731
def is_power_grab(user, target_user):
2832
return not user.site_admin and target_user.site_admin
2933

3034

35+
def can_site_read(user):
36+
return user.site_admin or user.site_read
37+
38+
39+
def view_user(user, target_user):
40+
# Regular users shall only be able to request their own user and otherwise receive a 404.
41+
# group_admin users shall be able to request information about all users in their group.
42+
# site_read (!) and site_admin users shall be able to retrieve information about all users.
43+
return any((
44+
user_is_target(user, target_user),
45+
has_group_rights(user, target_user.group),
46+
can_site_read(user)
47+
))
48+
49+
50+
def view_group(user, group):
51+
return any((
52+
is_own_group(user, group), # i assume, a 'normal' user can query their group's details regardless of group_admin status?
53+
can_site_read(user)
54+
))
55+
56+
3157
def has_data_access(user, data_user_id, data_group_id=None, was_submitted=False):
3258
# if dataset was already submitted, the group must match, or the user must have site_read priviledges
3359
# if dataset was not yet submitted, the user must match
@@ -150,6 +176,10 @@ def update_user_name(user, target_user):
150176
))
151177

152178

179+
def view_restricted_user_info(user, target_user):
180+
return has_group_rights(user, target_user.group)
181+
182+
153183
def create_service(user):
154184
return user.site_admin
155185

0 commit comments

Comments
 (0)