Skip to content

Commit 3b61430

Browse files
authored
Merge pull request #1151 from clowder-framework/release/v2.0-beta-3
v2.0.0-beta3
2 parents 7d054e0 + 70f0fb8 commit 3b61430

File tree

128 files changed

+2319
-496
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

128 files changed

+2319
-496
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ scripts/keycloak/data/*
5959
# ignore clowder chart deps
6060
deployments/kubernetes/charts/clowder2/charts
6161
deployments/kubernetes/charts/clowder2/*clowder2-software-dev.yaml
62+
*secret*.yaml
6263

6364
# Environments
6465
.env

.run/uvicorn.run.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<component name="ProjectRunConfigurationManager">
22
<configuration default="false" name="uvicorn" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
33
<module name="clowder2" />
4-
<option name="ENV_FILES" value="" />
54
<option name="INTERPRETER_OPTIONS" value="" />
65
<option name="PARENT_ENVS" value="true" />
76
<envs>
@@ -14,7 +13,7 @@
1413
<option name="ADD_SOURCE_ROOTS" value="true" />
1514
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
1615
<option name="SCRIPT_NAME" value="uvicorn" />
17-
<option name="PARAMETERS" value="app.main:app --reload --host 0.0.0.0" />
16+
<option name="PARAMETERS" value="app.main:app --host 0.0.0.0 --workers 17" />
1817
<option name="SHOW_COMMAND_LINE" value="false" />
1918
<option name="EMULATE_TERMINAL" value="false" />
2019
<option name="MODULE_MODE" value="true" />

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## [v2.0.0-beta.3] - 2024-07-29
9+
10+
### Added
11+
12+
- License management
13+
- Release dataset with versions
14+
- Enable and disable user account through Keycloak
15+
- Jupyterhub integration
16+
- Interface for creating and editing matching criteria for triggering extractors
17+
- Interface for editing metadata definitions
18+
- My dataset tab listing all the datasets created by the user
19+
- Drag and drop upload multiple files
20+
- Footer with links to documentation, source code, and contact information
21+
- Documentation through MKDocs
22+
23+
### Changed
24+
25+
- Allow public datasets and files to be searchable
26+
- List all the extractors with the ability to enable/disable the extractors
27+
- Filter listeners based on their support for file or dataset
28+
- Helm chart updated to support custom existing secret
29+
30+
### Fixed
31+
32+
- Clowder registration link on the top bar
33+
- Case-insensitive search
34+
- Download count immediately increments after download
35+
836
## [v2.0.0-beta.2] - 2024-02-16
937

1038
### Added

backend/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ ENV PATH="/code/.venv/bin:$PATH"
2222
COPY ./app /code/app
2323

2424
# launch app using uvicorn
25-
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
25+
# Number of recommended workers is 2 x number_of_cores +1
26+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--workers", "17"]

backend/app/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class Settings(BaseSettings):
99
API_V2_STR: str = "/api/v2"
1010
admin_email: str = "[email protected]"
1111
frontend_url: str = "http://localhost:3000"
12-
version: str = "2.0.0-beta.2"
12+
version: str = "2.0.0-beta.3"
1313

1414
# Unique secret for hashing API keys. Generate with `openssl rand -hex 32`
1515
local_auth_secret = "clowder_secret_key"

backend/app/keycloak_auth.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import logging
44
from datetime import datetime
5+
from typing import Optional
56

67
from fastapi import Depends, HTTPException, Security
78
from fastapi.security import APIKeyCookie, APIKeyHeader, OAuth2AuthorizationCodeBearer
@@ -435,6 +436,43 @@ async def create_user(email: str, password: str, firstName: str, lastName: str):
435436
return user
436437

437438

439+
async def update_user(
440+
email: str,
441+
new_email: Optional[str],
442+
new_password: Optional[str],
443+
new_firstName: Optional[str],
444+
new_lastName: Optional[str],
445+
):
446+
"""Update existing user in Keycloak."""
447+
keycloak_admin = KeycloakAdmin(
448+
server_url=settings.auth_server_url,
449+
username=settings.keycloak_username,
450+
password=settings.keycloak_password,
451+
realm_name=settings.keycloak_realm_name,
452+
user_realm_name=settings.keycloak_user_realm_name,
453+
# client_secret_key=settings.auth_client_secret,
454+
# client_id=settings.keycloak_client_id,
455+
verify=True,
456+
)
457+
existing_user_id = keycloak_admin.get_user_id(email)
458+
existing_user = keycloak_admin.get_user(existing_user_id)
459+
# Update user and set password
460+
keycloak_admin.update_user(
461+
existing_user_id,
462+
{
463+
"email": new_email or existing_user["email"],
464+
"username": new_email or existing_user["email"],
465+
"firstName": new_firstName or existing_user["firstName"],
466+
"lastName": new_lastName or existing_user["lastName"],
467+
},
468+
)
469+
if new_password:
470+
keycloak_admin.set_user_password(existing_user_id, new_password, False)
471+
472+
updated_user = keycloak_admin.get_user(existing_user_id)
473+
return updated_user
474+
475+
438476
def delete_user(email: str):
439477
"""Create a user in Keycloak."""
440478
keycloak_admin = KeycloakAdmin(

backend/app/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
description="A cloud native data management framework to support any research domain. Clowder was "
8787
"developed to help researchers and scientists in data intensive domains manage raw data, complex "
8888
"metadata, and automatic data pipelines. ",
89-
version="2.0.0-beta.2",
89+
version="2.0.0-beta.3",
9090
contact={"name": "Clowder", "url": "https://clowderframework.org/"},
9191
license_info={
9292
"name": "Apache 2.0",
@@ -316,7 +316,9 @@ async def startup_beanie():
316316
ThumbnailDBViewList,
317317
LicenseDB,
318318
],
319-
recreate_views=True,
319+
# If view exists, will not recreate
320+
# When view query changes, make sure to manually drop view and recreate
321+
recreate_views=False,
320322
)
321323

322324

backend/app/models/metadata.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,11 @@ async def validate_context(
307307
detail="Context is required",
308308
)
309309
if context is not None:
310-
pass
310+
# TODO validate context
311+
return content
311312
if context_url is not None:
312-
pass
313+
# TODO validate context
314+
return content
313315
if definition is not None:
314316
if (
315317
md_def := await MetadataDefinitionDB.find_one(
@@ -322,7 +324,7 @@ async def validate_context(
322324
status_code=400,
323325
detail=f"{definition} is not valid metadata definition",
324326
)
325-
return content
327+
return content
326328

327329

328330
def deep_update(orig: dict, new: dict):

backend/app/models/users.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ class UserIn(UserBase):
1818
password: str
1919

2020

21+
class UserUpdate(BaseModel):
22+
first_name: Optional[str]
23+
last_name: Optional[str]
24+
password: Optional[str]
25+
26+
2127
class UserLogin(BaseModel):
2228
email: EmailStr
2329
password: str

backend/app/routers/authentication.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@
55
enable_disable_user,
66
get_current_user,
77
keycloak_openid,
8+
update_user,
89
)
910
from app.models.datasets import DatasetDBViewList
10-
from app.models.users import UserDB, UserIn, UserLogin, UserOut
11+
from app.models.users import UserDB, UserIn, UserLogin, UserOut, UserUpdate
12+
from app.routers.utils import save_refresh_token
1113
from beanie import PydanticObjectId
1214
from fastapi import APIRouter, Depends, HTTPException
1315
from keycloak.exceptions import (
1416
KeycloakAuthenticationError,
1517
KeycloakGetError,
1618
KeycloakPostError,
19+
KeycloakPutError,
1720
)
1821
from passlib.hash import bcrypt
1922

@@ -69,6 +72,7 @@ async def save_user(userIn: UserIn):
6972
async def login(userIn: UserLogin):
7073
try:
7174
token = keycloak_openid.token(userIn.email, userIn.password)
75+
await save_refresh_token(token["refresh_token"], userIn.email)
7276
return {"token": token["access_token"]}
7377
# bad credentials
7478
except KeycloakAuthenticationError as e:
@@ -95,6 +99,45 @@ async def authenticate_user(email: str, password: str):
9599
return user
96100

97101

102+
@router.patch("/users/me", response_model=UserOut)
103+
async def update_current_user(
104+
userUpdate: UserUpdate, current_user=Depends(get_current_user)
105+
):
106+
try:
107+
await update_user(
108+
current_user.email,
109+
None,
110+
userUpdate.password,
111+
userUpdate.first_name,
112+
userUpdate.last_name,
113+
)
114+
except KeycloakGetError as e:
115+
raise HTTPException(
116+
status_code=e.response_code,
117+
detail=json.loads(e.error_message),
118+
headers={"WWW-Authenticate": "Bearer"},
119+
)
120+
except KeycloakPutError as e:
121+
raise HTTPException(
122+
status_code=e.response_code,
123+
detail=json.loads(e.error_message),
124+
headers={"WWW-Authenticate": "Bearer"},
125+
)
126+
127+
# Update local user
128+
user = await UserDB.find_one(UserDB.email == current_user.email)
129+
130+
if userUpdate.first_name:
131+
user.first_name = userUpdate.first_name
132+
if userUpdate.last_name:
133+
user.last_name = userUpdate.last_name
134+
if userUpdate.password:
135+
user.hashed_password = bcrypt.hash(userUpdate.password)
136+
137+
await user.save()
138+
return user.dict()
139+
140+
98141
@router.get("/users/me/is_admin", response_model=bool)
99142
async def get_admin(
100143
dataset_id: str = None, current_username=Depends(get_current_user)

0 commit comments

Comments
 (0)