Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DLT-1110] Implement Consent Required Action (3/4 & 4/4) #1242

Open
wants to merge 10 commits into
base: devel
Choose a base branch
from

Conversation

AronPerez
Copy link
Collaborator

@AronPerez AronPerez commented Jan 21, 2025

PR Description

This PR:

  • Moves the consent url from utils.js to services/auth/
  • Adds datafed express server handling for getting the consent URL via API GET req /api/globus/consent_url
  • Refactor endpoint browser file
  • Move stylings into css
  • Add testing for api facade pattern handling + update consent handling
  • Add button enable/disable on transfer modal

Addresses

  1. [Web, Globus] - Feature support listing collection contents from mapped collections from web ui #1110
  2. [Web, Globus] - Trigger consent redirect and flow in web UI when listing contents of mapped collection #1070
  3. [Style, Web] - Refactor, can upload to unchecked record #1293

How Was This Tested

Manually

  1. cd compose/all
  2. ./generate_env.sh
  3. ./generate_globus_files.sh
  4. update .env
  5. ./build_images_for_compose.sh
  6. docker compose up
  7. Hit your ip / host
  8. In browser, go to Globus developer settings
  9. Select project that you named in step 4
  10. Select <name> Setup Client
  11. Edit App Details
  12. Add https redirect uri to Redirects
  13. Save
  14. Login to DataFed in browser
  15. Follow:
    USE_CASE - Transfer Data - Download Data Record
    USE_CASE - Transfer Data - New Data Record
    USE_CASE - Transfer Data - Update Data Record

Mocha/Chai

  1. cd web
  2. cp package.json.in package.json
  3. Remove "//": "WARNING package.json.in is where dependency changes should be made package.json is generated by cmake", from package.json
  4. npm install
  5. npm run test
  6. All tests should pass

Artifacts (if appropripate)

Create Data Record from Guest Collection

Screen.Recording.2025-02-07.at.2.16.32.PM.mov

Upload to Data Record with existing data

Screen.Recording.2025-02-07.at.1.56.43.PM.mov

Create Data Record on Mapped Collection Requiring Consent

Screen.Recording.2025-02-07.at.2.21.59.PM.mov

Download (single data record) to Mapped Collection

Screen.Recording.2025-02-07.at.1.57.48.PM.mov

Download (multiple files) to Mapped Collection

Screen.Recording.2025-02-07.at.2.25.01.PM.mov

web/ Testing

Screenshot 2025-02-05 at 2 02 23 PM

Tasks

  • - A description of the PR has been provided, and a diagram included if it is a new feature.
  • - Formatter has been run
  • - CHANGELOG comment has been added
  • - Labels have been assigned to the pr
  • - A reviwer has been added
  • - A user has been assigned to work on the pr
  • - If new feature a unit test has been added

Summary by Sourcery

Implement consent flow for mapped collections.

New Features:

  • List collection contents from mapped collections in the web UI.

Tests:

  • Add tests for API facade pattern handling and update consent handling.

@AronPerez AronPerez changed the base branch from master to devel January 22, 2025 00:35
@AronPerez AronPerez changed the base branch from devel to feat-DLT-1110-mapped-collection-endpoint-browse January 22, 2025 00:35
@AronPerez AronPerez force-pushed the feat-DLT-1110-mapped-collection-endpoint-browse-feat branch from 3adc821 to 72694a2 Compare January 22, 2025 17:21
@AronPerez AronPerez changed the base branch from feat-DLT-1110-mapped-collection-endpoint-browse to devel January 24, 2025 16:07
@AronPerez AronPerez force-pushed the feat-DLT-1110-mapped-collection-endpoint-browse-feat branch 5 times, most recently from 84fdb01 to eb861b6 Compare January 30, 2025 15:38
@AronPerez AronPerez force-pushed the feat-DLT-1110-mapped-collection-endpoint-browse-feat branch 5 times, most recently from 8979a66 to 01d680a Compare February 7, 2025 04:09
@AronPerez AronPerez changed the title [DLT-1110] Implement Consent Required Action (3/4) [DLT-1110] Implement Consent Required Action (3/4 & 4/4) Feb 7, 2025
@AronPerez AronPerez force-pushed the feat-DLT-1110-mapped-collection-endpoint-browse-feat branch from 8bf61bc to 98ed92b Compare February 7, 2025 19:51
Comment on lines +311 to +317
function storeCollectionId(req, res, next) {
if (req.query.collection_id) {
req.session.collection_id = req.query.collection_id;
}
next();
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're storing the collection_id in session since there's no real nice way to save it as a cookie in the FE

@@ -621,6 +634,7 @@ app.get("/ui/authn", (a_req, a_resp) => {
} catch (err) {
redirect_path = "/ui/error";
logger.error("/ui/authn", getCurrentLineNumber(), err);
delete a_req.session.collection_id;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If our auth fails delete the collection_id from session

Comment on lines +1560 to +1574
app.get("/api/globus/consent_url", storeCollectionId, (a_req, a_resp) => {
const { requested_scopes, state, refresh_tokens, query_params } = a_req.query;

const consent_url = generateConsentURL(
g_oauth_credentials.clientId,
g_oauth_credentials.redirectUri,
refresh_tokens,
requested_scopes,
query_params,
state,
);

a_resp.json({ consent_url });
});

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pulled the business logic out, this will just handle the request

@@ -39,7 +39,8 @@
"chai": "^4",
"esm": "^3.2.25",
"mocha": "^10.8.2",
"pug": "^3.0.3"
"pug": "^3.0.3",
"sinon": "17.0.0"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used for sandboxing tests with mocha/chai

state,
) => {
const scopes = requested_scopes
? requested_scopes
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming it's always an array here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return object from Globus has, in my experience, always held an array of scope strings. Here's a doc reference https://docs.globus.org/api/transfer/overview/#data_access_consent

@@ -110,7 +112,7 @@ export default class OAuthTokenHandler {
switch (
resource_server // TODO: exhaustive coverage of types
) {
case "auth.globus.org": {
case AUTH_PREFIX: {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Figured we'd pull out this since it's commonly used

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the feelings are on a constants file

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it, almost went this route myself

Comment on lines +42 to +77
pathNavigator() {
return `
<div class='endpoint-browser col-flex'>
<div class="path-navigator row-flex">
<label class="path-label">Path:</label>
<div class="path-input-container">
<input type="text" id="path" value="${this.state.path}"/>
</div>
<div>
<button id="up" class="btn small">Up</button>
</div>
</div>
</div>
`;
}

fileTree() {
return `
<div class="endpoint-browser file-tree-view ui-widget content">
<table id="file_tree">
<colgroup>
<col/>
<col/>
<col/>
</colgroup>
<tbody>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
`;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Styling is in CSS file

Comment on lines +229 to +234
{
title: CONFIG.PATH.UP,
icon: CONFIG.UI.ICONS.FOLDER,
key: CONFIG.PATH.UP,
is_dir: true,
},
Copy link
Collaborator Author

@AronPerez AronPerez Feb 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gives is the .. in the endpoint view

Screenshot 2025-02-07 at 2 55 55 PM

key: entry.name,
is_dir: false,
size: util.sizeToString(entry.size),
date: new Date(entry.last_modified.replace(" ", "T")).toLocaleString(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handles dates

Screenshot 2025-02-07 at 2 56 48 PM

Comment on lines +260 to +288
handleApiError(data) {
if (data.code === "ConsentRequired") {
api.getGlobusConsentURL(
(ok, data) => {
const source = [
{
title: `<span class='ui-state-error'>Consent Required: Please provide <a href="${data.consent_url}">consent</a>.</span>`,
icon: false,
is_dir: true,
},
];

$.ui.fancytree.getTree("#file_tree").reload(source);
this.state.loading = false;
$("#file_tree").fancytree("enable");
},
this.props.endpoint.id,
data.required_scopes,
);
} else {
return [
{
title: `<span class='ui-state-error'>Error: ${data.message}</span>`,
icon: false,
is_dir: true,
},
];
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be used to handle whatever errors we get on ls command

@@ -90,9 +90,8 @@ export class TransferEndpointManager {

if (ok && !data.code) {
console.info("Direct endpoint match found:", data);
this.#controller.uiManager.updateEndpoint(data);
this.#controller.uiManager.state.endpointOk = true;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

endpointOk is being removed since this was associated with activating an endpoint

this.#controller.uiManager.updateEndpoint(data);
this.#controller.uiManager.state.endpointOk = true;
this.#controller.uiManager.updateButtonStates();
this.#controller.uiManager.enableBrowseButton(true);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have a direct match, enable the ability to press the browse button and ls

@@ -61,7 +59,6 @@ export class TransferUIManager {
this.initializeEndpointInput();
this.initializeTransferOptions();
this.initializeBrowseButton();
this.updateButtonStates();
Copy link
Collaborator Author

@AronPerez AronPerez Feb 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pulling this out since redundant due to initializeButtons

@AronPerez AronPerez linked an issue Feb 7, 2025 that may be closed by this pull request
@AronPerez AronPerez marked this pull request as ready for review February 7, 2025 20:07
@AronPerez AronPerez added Type: Bug Something isn't working Priority: High Highest priority Type: New Feature New or enhanced feature Component: Web API Relates to web service / API Component: Web UI Relates to web appp user interface Type: Update Update a dependency or some other package labels Feb 7, 2025
@@ -1543,6 +1557,21 @@ app.post("/api/cat/search", (a_req, a_resp) => {
});
});

app.get("/api/globus/consent_url", storeCollectionId, (a_req, a_resp) => {
const { requested_scopes, state, refresh_tokens, query_params } = a_req.query;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR - As a note, it would be nice to have some validators for the datafed-web API as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sourcery-ai Write up an issue that is a task to address the need for validation request middleware.
Suggest zod or valibot

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sourcery-ai guide

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sourcery-ai summary

@AronPerez
Copy link
Collaborator Author

@sourcery-ai !review

Copy link

sourcery-ai bot commented Feb 7, 2025

Sure! I'm generating a new review now.

Copy link

sourcery-ai bot commented Feb 7, 2025

Reviewer's Guide by Sourcery

This PR implements the new consent flow for mapped collections and refactors various parts of the application. The changes include moving consent URL generation to a dedicated service, updating server endpoints to support the new flow, refactoring UI logic in the transfer modal and endpoint management, removing obsolete test code, and adding a new endpoint browser component with its own UI and styling.

Sequence diagram for Consent Flow API Interaction

sequenceDiagram
    participant U as User
    participant WC as Web Client
    participant DF as DataFed Server
    participant CH as ConsentHandler Service
    participant GA as Globus Auth (External)

    U->>WC: Initiate consent-required action
    WC->>DF: GET /api/globus/consent_url (with parameters)
    DF->>CH: Call generateConsentURL(clientId, redirectUri, refresh_tokens, scopes, query_params, state)
    CH->>GA: Construct consent URL using AUTH_URL and credentials
    GA-->>CH: Consent URL
    CH-->>DF: Return consent URL
    DF-->>WC: JSON { consent_url }
    WC->>U: Display consent prompt with consent URL link
Loading

File-Level Changes

Change Details Files
Refactored consent URL generation and handling.
  • Moved consent URL logic from utils.js to a new service in services/auth/ConsentHandler.js.
  • Introduced a new constants module to manage AUTH related constants.
  • Updated server endpoint to provide consent URL on GET /api/globus/consent_url using the new service.
  • Added a new API function in web/static/api.js to retrieve the consent URL.
web/static/util.js
web/datafed-ws.js
web/services/auth/ConsentHandler.js
web/services/auth/constants.js
web/static/api.js
Refactored UI components for transfer operations.
  • Updated TransferUIManager to remove outdated state properties and consolidate button state updates.
  • Added new helper functions (updateButtonState, enableBrowseButton, enableStartButton) to better manage UI changes.
  • Replaced deprecated methods and updated event handling in transfer modal.
  • Modified transfer-endpoint-manager.js and transfer-templates.js to adapt to changes in UI logic.
web/static/components/transfer/transfer-ui-manager.js
web/static/components/transfer/transfer-endpoint-manager.js
web/static/components/transfer/transfer-templates.js
Cleaned up obsolete test code.
  • Removed tests for globusGetAuthorizeURL function from the util.test.js file as this functionality has been moved.
web/test/util.test.js
Introduced a new EndpointBrowser component for file/directory browsing.
  • Added a new UI component for endpoint browsing in web/static/components/endpoint-browse/index.js.
  • Included accompanying CSS styling in web/static/components/endpoint-browse/styles.css.
  • Updated the main view template to load the new endpoint browser component and its styles.
web/static/components/endpoint-browse/index.js
web/static/components/endpoint-browse/styles.css
web/views/main.ect
Minor update in token handling.
  • Updated TokenHandler to import AUTH_PREFIX from the new constants module.
  • Minor code cleanup in web/services/auth/TokenHandler.js.
web/services/auth/TokenHandler.js
Updated changelog with new feature entries.
  • Added new entries for consent flow, mapped collections support, and endpoint browsing enhancements.
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!
  • Generate a plan of action for an issue: Comment @sourcery-ai plan on
    an issue to generate a plan of action for it.

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @AronPerez - I've reviewed your changes - here's some feedback:

Overall Comments:

  • Remove commented-out and dead code in TransferUIManager to reduce clutter.
  • Pass query_params as an object (not as a JSON string) in getGlobusConsentURL to match generateConsentURL’s expected input.
Here's what I looked at during the review
  • 🟡 General issues: 1 issue found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +115 to +124
// Initialize FancyTree with configuration
$("#file_tree", this.element).fancytree({
checkbox: false,
extensions: ["themeroller", "table"],
nodata: false,
themeroller: { activeClass: "my-fancytree-active", hoverClass: "" },
table: { nodeColumnIdx: 0 },
source: [{ title: "loading...", icon: false, is_dir: true }],
selectMode: 1,
// Render additional columns (size and date)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Scope jQuery selectors to the component instance.

Currently, selectors such as $('#file_tree') and $('#sel_btn') are used globally. Switching to this.element.find(...) would confine the DOM queries to the dialog instance and avoid potential conflicts if multiple dialogs are rendered concurrently.

Suggested implementation:

        this.element.find("#up").on("click", () => this.navigate(CONFIG.PATH.UP));
        this.element.find("#path").on("input", () => {
            clearTimeout(this.state.timer);
            this.state.timer = setTimeout(() => {
                this.state.path = this.element.find("#path").val();
                this.loadTree();
            }, CONFIG.UI.DELAY);
        });
        // Initialize FancyTree with configuration
        this.element.find("#file_tree").fancytree({

If there are other selectors (e.g., for '#sel_btn') elsewhere in this file or in related components, change them similarly to use this.element.find(...).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense, but using $(..., ...) notation involves looking at the first parameter, the selector, the 2nd being the element/frame. We've scoped out request down to the element, as far as I understand

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that the second argument to the jQuery function $(selector, context) limits the scope of the search. However, the context you're providing is likely the document or some other high-level element. While this is better than a completely global search, it's still broader than necessary and can lead to unexpected behavior if the same IDs are used within other parts of the application, even if those parts aren't currently visible.

My suggestion to use this.element.find(...) is more specific. It ensures that the selector only looks within the current component's DOM element. This creates a stronger isolation and prevents conflicts even if other parts of the application use the same IDs. It's a best practice for encapsulating components and improving maintainability.

Think of it this way: You've built a house (your component) and you're looking for a specific light switch ("#file_tree"). Using just $("#file_tree") is like searching the entire neighborhood for that light switch. Using $(..., document) is like searching your street. But this.element.find("#file_tree") is like searching only inside your house. It's much more efficient and guarantees you'll find the correct switch, even if your neighbor happens to have a switch with the same name.

Could you try replacing the selectors with this.element.find(...) and see if it works as expected? If you encounter any issues, please let me know and we can troubleshoot together.

Comment on lines +27 to +29
const scopes = requested_scopes
? requested_scopes
: ["openid", "profile", "email", "urn:globus:auth:scope:transfer.api.globus.org:all"];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Avoid unneeded ternary statements (simplify-ternary)

Suggested change
const scopes = requested_scopes
? requested_scopes
: ["openid", "profile", "email", "urn:globus:auth:scope:transfer.api.globus.org:all"];
const scopes = requested_scopes || ["openid", "profile", "email", "urn:globus:auth:scope:transfer.api.globus.org:all"];


ExplanationIt is possible to simplify certain ternary statements into either use of an || or !.
This makes the code easier to read, since there is no conditional logic.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this, I’ll implement it later

let updatedPath;
if (newPath === CONFIG.PATH.UP) {
// Handle "up" navigation
if (current.length === 1) return; // Already at root
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (current.length === 1) return; // Already at root
if (current.length === 1) {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).

* Load tree data from API
*/
loadTree() {
if (this.state.loading) return;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (this.state.loading) return;
if (this.state.loading) {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).

*/
handleSelect() {
const node = $.ui.fancytree.getTree("#file_tree").activeNode;
if (!node || !this.props.onSelect) return;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (!node || !this.props.onSelect) return;
if (!node || !this.props.onSelect) {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).

Copy link

sourcery-ai bot commented Feb 7, 2025

Hey @AronPerez, I've posted a new review for you!

@@ -0,0 +1,60 @@
import { AUTH_URL } from "./constants.js";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this usage a lot. We should extract more constants later.

@AronPerez
Copy link
Collaborator Author

@sourcery-ai summary of the usage of middleware validation in web/DataFed-ws.js

@AronPerez
Copy link
Collaborator Author

@sourcery-ai guide

@AronPerez AronPerez force-pushed the feat-DLT-1110-mapped-collection-endpoint-browse-feat branch from 98ed92b to d129616 Compare February 12, 2025 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Component: Web API Relates to web service / API Component: Web UI Relates to web appp user interface Priority: High Highest priority Type: Bug Something isn't working Type: New Feature New or enhanced feature Type: Update Update a dependency or some other package
Projects
None yet
2 participants