Skip to content

Commit

Permalink
Merge branch 'release_24.1' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
mvdbeek committed Sep 13, 2024
2 parents bd54d46 + 00f6482 commit ac71c06
Show file tree
Hide file tree
Showing 18 changed files with 214 additions and 54 deletions.
10 changes: 10 additions & 0 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9212,6 +9212,11 @@ export interface components {
* @description TODO
*/
api_type?: "file" | null;
/**
* Copied From History Dataset Association Id
* @description ID of HDA this HDA was copied from.
*/
copied_from_history_dataset_association_id?: string | null;
/** Copied From Ldda Id */
copied_from_ldda_id?: string | null;
/**
Expand Down Expand Up @@ -9457,6 +9462,11 @@ export interface components {
* @enum {string}
*/
api_type: "file";
/**
* Copied From History Dataset Association Id
* @description ID of HDA this HDA was copied from.
*/
copied_from_history_dataset_association_id?: string | null;
/** Copied From Ldda Id */
copied_from_ldda_id?: string | null;
/**
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Login/RegisterForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ async function submit() {
Toast.info(response.data.message);
}
window.location.href = props.redirect || withPrefix("/");
window.location.href = props.redirect ? withPrefix(props.redirect) : withPrefix("/");
} catch (error: any) {
disableCreate.value = false;
messageText.value = errorMessageAsString(error, "Registration failed for an unknown reason.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@ function getIdpPreference() {
<img :src="iDPInfo['icon']" height="45" :alt="idp" />
</BButton>
</span>
<span v-else-if="iDPInfo['custom_button_text']">
<BButton class="d-block mt-3" @click="submitOIDCLogin(idp)">
<i :class="oIDCIdps[idp]" />
Sign in with {{ iDPInfo["custom_button_text"] }}
</BButton>
</span>
<span v-else>
<BButton class="d-block mt-3" @click="submitOIDCLogin(idp)">
<i :class="oIDCIdps[idp]" />
Expand Down
12 changes: 11 additions & 1 deletion lib/galaxy/authnz/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ def _parse_oidc_config(self, config_file):
def _get_idp_icon(self, idp):
return self.oidc_backends_config[idp].get("icon") or DEFAULT_OIDC_IDP_ICONS.get(idp)

def _get_idp_button_text(self, idp):
return self.oidc_backends_config[idp].get("custom_button_text")

def _parse_oidc_backends_config(self, config_file):
self.oidc_backends_config = {}
self.oidc_backends_implementation = {}
Expand All @@ -122,7 +125,10 @@ def _parse_oidc_backends_config(self, config_file):
if idp in BACKENDS_NAME:
self.oidc_backends_config[idp] = self._parse_idp_config(child)
self.oidc_backends_implementation[idp] = "psa"
self.app.config.oidc[idp] = {"icon": self._get_idp_icon(idp)}
self.app.config.oidc[idp] = {
"icon": self._get_idp_icon(idp),
"custom_button_text": self._get_idp_button_text(idp),
}
elif idp in KEYCLOAK_BACKENDS:
self.oidc_backends_config[idp] = self._parse_custos_config(child)
self.oidc_backends_implementation[idp] = "custos"
Expand Down Expand Up @@ -159,6 +165,10 @@ def _parse_idp_config(self, config_xml):
rtv["extra_scopes"] = listify(config_xml.find("extra_scopes").text)
if config_xml.find("tenant_id") is not None:
rtv["tenant_id"] = config_xml.find("tenant_id").text
if config_xml.find("oidc_endpoint") is not None:
rtv["oidc_endpoint"] = config_xml.find("oidc_endpoint").text
if config_xml.find("custom_button_text") is not None:
rtv["custom_button_text"] = config_xml.find("custom_button_text").text
if config_xml.find("pkce_support") is not None:
rtv["pkce_support"] = asbool(config_xml.find("pkce_support").text)
if config_xml.find("accepted_audiences") is not None:
Expand Down
3 changes: 3 additions & 0 deletions lib/galaxy/authnz/psa_authnz.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"okta": "social_core.backends.okta_openidconnect.OktaOpenIdConnect",
"azure": "social_core.backends.azuread_tenant.AzureADV2TenantOAuth2",
"egi_checkin": "social_core.backends.egi_checkin.EGICheckinOpenIdConnect",
"oidc": "social_core.backends.open_id_connect.OpenIdConnectAuth",
}

BACKENDS_NAME = {
Expand All @@ -54,6 +55,7 @@
"okta": "okta-openidconnect",
"azure": "azuread-v2-tenant-oauth2",
"egi_checkin": "egi-checkin",
"oidc": "oidc",
}

AUTH_PIPELINE = (
Expand Down Expand Up @@ -133,6 +135,7 @@ def _setup_idp(self, oidc_backend_config):
self.config["KEY"] = oidc_backend_config.get("client_id")
self.config["SECRET"] = oidc_backend_config.get("client_secret")
self.config["TENANT_ID"] = oidc_backend_config.get("tenant_id")
self.config["OIDC_ENDPOINT"] = oidc_backend_config.get("oidc_endpoint")
self.config["redirect_uri"] = oidc_backend_config.get("redirect_uri")
self.config["EXTRA_SCOPES"] = oidc_backend_config.get("extra_scopes")
if oidc_backend_config.get("prompt") is not None:
Expand Down
14 changes: 14 additions & 0 deletions lib/galaxy/authnz/xsd/oidc_backends_config.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,20 @@
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="oidc_endpoint" minOccurs="0" type="xs:anyURI">
<xs:annotation>
<xs:documentation>
OIDC endpoint for the IdP
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="custom_button_text" minOccurs="0" type="xs:string">
<xs:annotation>
<xs:documentation>
Can be used to set a custom text value for the "Sign in with " button for your chosen IdP.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="pkce_support" minOccurs="0" type="xs:boolean">
<xs:annotation>
<xs:documentation>
Expand Down
20 changes: 20 additions & 0 deletions lib/galaxy/config/sample/oidc_backends_config.xml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,26 @@ Please mind `http` and `https`.
<tenant_id> ... </tenant_id>
</provider>

<provider name="oidc">
<!-- This example section uses the generic OIDC backend from python-social-auth. This should work with any provider that
supports the OIDC standard.
-->
<client_id> ... </client_id>
<client_secret> ... </client_secret>
<redirect_uri>http://localhost:8080/authnz/oidc/callback</redirect_uri>
<oidc_endpoint> ... </oidc_endpoint>
<!-- enter the url of a OIDC endpoint that supports autoconfiguration here. Enter the URL minus the ".well-known/openid-configuration" part.
For example: If the complete url pointing to your endpoint's autoconfig file is "https:example.com/my-realm/.well-known/openid-configuration",
then simply enter "https://example.com/my-realm".
-->
<custom_button_text> ... </custom_button_text>
<!-- This is an optional field where you can provide a custom text for the sign in button on the Galaxy front page
that you can click to authenticate with your IdP. If you don't provide a custom button text, the sign in button
will just read the default value of your IdP name. For example: "Sign in with oidc" or "Sign in with Azure".
However, if for example you enter the value "your university account" here, the button will display "Sign in with your university account".
-->
</provider>

<!-- Documentation: https://docs.egi.eu/providers/check-in/sp -->
<provider name="egi_checkin">
<!-- Client id and secret can be obtained by registering your client at EGI Check-in
Expand Down
2 changes: 1 addition & 1 deletion lib/galaxy/managers/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ def add_serializers(self):
"file_size": lambda item, key, **context: self.serializers["size"](item, key, **context),
"nice_size": lambda item, key, **context: item.get_size(nice_size=True, calculate_size=False),
# common to lddas and hdas - from mapping.py
"copied_from_history_dataset_association_id": self.serialize_id,
"copied_from_history_dataset_association_id": lambda item, key, **context: item.id,
"copied_from_library_dataset_dataset_association_id": self.serialize_id,
"info": lambda item, key, **context: item.info.strip() if isinstance(item.info, str) else item.info,
"blurb": lambda item, key, **context: item.blurb,
Expand Down
6 changes: 4 additions & 2 deletions lib/galaxy/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5277,6 +5277,7 @@ def copy_from(self, other_hda, new_dataset=None, include_tags=True, include_meta
if include_tags and self.history:
self.copy_tags_from(self.user, other_hda)
self.dataset = new_dataset or other_hda.dataset
self.copied_from_history_dataset_association_id = other_hda.id
if old_dataset:
old_dataset.full_delete()

Expand Down Expand Up @@ -8223,6 +8224,7 @@ def copy_to(self, copied_step, step_mapping, user=None):
if stored_subworkflow and stored_subworkflow.user == user:
# This should be fine and reduces the number of stored subworkflows
copied_step.subworkflow = subworkflow
copied_subworkflow = subworkflow
else:
# Can this even happen, building a workflow with a subworkflow you don't own ?
copied_subworkflow = subworkflow.copy()
Expand All @@ -8231,8 +8233,8 @@ def copy_to(self, copied_step, step_mapping, user=None):
)
copied_subworkflow.stored_workflow = stored_workflow
copied_step.subworkflow = copied_subworkflow
for subworkflow_step, copied_subworkflow_step in zip(subworkflow.steps, copied_subworkflow.steps):
subworkflow_step_mapping[subworkflow_step.id] = copied_subworkflow_step
for subworkflow_step, copied_subworkflow_step in zip(subworkflow.steps, copied_subworkflow.steps):
subworkflow_step_mapping[subworkflow_step.id] = copied_subworkflow_step

for old_conn, new_conn in zip(self.input_connections, copied_step.input_connections):
new_conn.input_step_input = copied_step.get_or_add_input(old_conn.input_name)
Expand Down
3 changes: 3 additions & 0 deletions lib/galaxy/schema/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,9 @@ class HDADetailed(HDASummary, WithModelClass):
description="The list of sources associated with this dataset.",
),
]
copied_from_history_dataset_association_id: Annotated[
Optional[EncodedDatabaseIdField], Field(None, description="ID of HDA this HDA was copied from.")
]


class HDAExtended(HDADetailed):
Expand Down
12 changes: 9 additions & 3 deletions lib/galaxy/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2999,7 +2999,9 @@ def exec_after_process(self, app, inp_data, out_data, param_dict, job=None, **kw
# if change_datatype PJA is associated with expression tool output the new output already has
# the desired datatype, so we use it. If the extension is "data" there's no change_dataset PJA and
# we want to use the existing extension.
new_ext = output.extension if output.extension != "data" else copy_object.extension
new_ext = (
output.extension if output.extension not in ("data", "expression.json") else copy_object.extension
)
require_metadata_regeneration = copy_object.extension != new_ext
output.copy_from(copy_object, include_metadata=not require_metadata_regeneration)
output.extension = new_ext
Expand All @@ -3014,8 +3016,12 @@ def exec_after_process(self, app, inp_data, out_data, param_dict, job=None, **kw
else:
# TODO: move exec_after_process into metadata script so this doesn't run on the headnode ?
output.init_meta()
output.set_meta()
output.set_metadata_success_state()
try:
output.set_meta()
output.set_metadata_success_state()
except Exception:
output.state = model.HistoryDatasetAssociation.states.FAILED_METADATA
log.exception("Exception occured while setting metdata")

def parse_environment_variables(self, tool_source):
"""Setup environment variable for inputs file."""
Expand Down
70 changes: 31 additions & 39 deletions lib/galaxy/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@
timezone,
)
from decimal import Decimal
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.message import EmailMessage
from hashlib import md5
from os.path import relpath
from pathlib import Path
Expand Down Expand Up @@ -1634,10 +1633,8 @@ def send_mail(frm, to, subject, body, config, html=None, reply_to=None):
return

to = listify(to)
if html:
msg = MIMEMultipart("alternative")
else:
msg = MIMEText(body, "plain", "utf-8")
msg = EmailMessage()
msg.set_content(body)

msg["To"] = ", ".join(to)
msg["From"] = frm
Expand All @@ -1652,45 +1649,40 @@ def send_mail(frm, to, subject, body, config, html=None, reply_to=None):
return

if html:
mp_text = MIMEText(body, "plain", "utf-8")
mp_html = MIMEText(html, "html", "utf-8")
msg.attach(mp_text)
msg.attach(mp_html)
msg.add_alternative(html, subtype="html")

smtp_ssl = asbool(getattr(config, "smtp_ssl", False))
if smtp_ssl:
s = smtplib.SMTP_SSL(config.smtp_server)
else:
s = smtplib.SMTP(config.smtp_server)
if not smtp_ssl:
try:
s.starttls()
log.debug("Initiated SSL/TLS connection to SMTP server: %s", config.smtp_server)
except RuntimeError as e:
log.warning("SSL/TLS support is not available to your Python interpreter: %s", unicodify(e))
except smtplib.SMTPHeloError as e:
log.error("The server didn't reply properly to the HELO greeting: %s", unicodify(e))
s.close()
raise
except smtplib.SMTPException as e:
log.warning("The server does not support the STARTTLS extension: %s", unicodify(e))
if config.smtp_username and config.smtp_password:
try:
s.login(config.smtp_username, config.smtp_password)
except smtplib.SMTPHeloError as e:
log.error("The server didn't reply properly to the HELO greeting: %s", unicodify(e))
s.close()
raise
except smtplib.SMTPAuthenticationError as e:
log.error("The server didn't accept the username/password combination: %s", unicodify(e))
s.close()
raise
except smtplib.SMTPException as e:
log.error("No suitable authentication method was found: %s", unicodify(e))
s.close()
raise
s.sendmail(frm, to, msg.as_string())
s.quit()
try:
if not smtp_ssl:
try:
s.starttls()
log.debug("Initiated SSL/TLS connection to SMTP server: %s", config.smtp_server)
except RuntimeError as e:
log.warning("SSL/TLS support is not available to your Python interpreter: %s", e)
except smtplib.SMTPHeloError as e:
log.error("The server didn't reply properly to the HELO greeting: %s", e)
raise
except smtplib.SMTPException as e:
log.warning("The server does not support the STARTTLS extension: %s", e)
if config.smtp_username and config.smtp_password:
try:
s.login(config.smtp_username, config.smtp_password)
except smtplib.SMTPHeloError as e:
log.error("The server didn't reply properly to the HELO greeting: %s", e)
raise
except smtplib.SMTPAuthenticationError as e:
log.error("The server didn't accept the username/password combination: %s", e)
raise
except smtplib.SMTPException as e:
log.error("No suitable authentication method was found: %s", e)
raise
s.send_message(msg)
finally:
s.quit()


def force_symlink(source, link_name):
Expand Down
6 changes: 5 additions & 1 deletion lib/galaxy/webapps/galaxy/services/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,11 @@ def show(
# Default: return dataset as dict.
if hda_ldda == DatasetSourceType.hda:
return self.hda_serializer.serialize_to_view(
dataset, view=serialization_params.view or "detailed", user=trans.user, trans=trans
dataset,
view=serialization_params.view or "detailed",
keys=serialization_params.keys,
user=trans.user,
trans=trans,
)
else:
dataset_dict = dataset.to_dict()
Expand Down
37 changes: 37 additions & 0 deletions lib/galaxy_test/api/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,43 @@ def test_update_name(self):
workflow_dict = self.workflow_populator.download_workflow(workflow_id)
assert workflow_dict["license"] == "AAL"

def test_update_name_for_workflow_with_subworkflows(self):
workflow_id = self.workflow_populator.upload_yaml_workflow(
"""
class: GalaxyWorkflow
label: old name
inputs:
dataset: data
steps:
subworkflow:
in:
dataset: dataset
outputs:
output:
outputSource: cat1/out_file1
run:
class: GalaxyWorkflow
inputs:
dataset:
type: data
steps:
cat1:
tool_id: cat1
in:
input1: dataset
cat1:
tool_id: cat1
in:
input1: subworkflow/output
"""
)
self.workflow_populator.download_workflow(workflow_id)
new_name = "my cool new name"
data = {"name": new_name}
self._update_workflow(workflow_id, data).raise_for_status()
post_update_workflow = self.workflow_populator.download_workflow(workflow_id)
assert post_update_workflow["name"] == new_name

def test_update_name_empty(self):
# Update doesn't allow empty names.

Expand Down
11 changes: 7 additions & 4 deletions lib/galaxy_test/base/populators.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,14 +1009,17 @@ def get_history_dataset_source_transform_actions(self, history_id: str, **kwd) -
assert isinstance(transform, list)
return {t["action"] for t in transform}

def get_history_dataset_details(self, history_id: str, **kwds) -> Dict[str, Any]:
def get_history_dataset_details(self, history_id: str, keys: Optional[str] = None, **kwds) -> Dict[str, Any]:
dataset_id = self.__history_content_id(history_id, **kwds)
details_response = self.get_history_dataset_details_raw(history_id, dataset_id)
details_response = self.get_history_dataset_details_raw(history_id, dataset_id, keys=keys)
details_response.raise_for_status()
return details_response.json()

def get_history_dataset_details_raw(self, history_id: str, dataset_id: str) -> Response:
details_response = self._get_contents_request(history_id, f"/datasets/{dataset_id}")
def get_history_dataset_details_raw(self, history_id: str, dataset_id: str, keys: Optional[str] = None) -> Response:
data = None
if keys:
data = {"keys": keys}
details_response = self._get_contents_request(history_id, f"/datasets/{dataset_id}", data=data)
return details_response

def get_history_dataset_extra_files(self, history_id: str, **kwds) -> list:
Expand Down
Loading

0 comments on commit ac71c06

Please sign in to comment.