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

CORE-232: user supportSummary API #1617

Draft
wants to merge 19 commits into
base: develop
Choose a base branch
from

Conversation

davidangb
Copy link
Contributor

@davidangb davidangb commented Jan 3, 2025

Ticket: https://broadworkbench.atlassian.net/browse/CORE-232

What:

New admin API for information about a user, including direct, indirect, and unsynchronized group membership counts.

This is implemented by adding the group membership counts to the existing logic for the getSamUserCombinedState API, and then creating a new admin API which reuses that logic but allows an admin to execute that logic for any user.

Review Questions:

  1. What permissions should this API have, and how should we implement them? The new API has the same permissions as GET /api/admin/v1/user/email/{email} - specifically, it requires super-admin. This probably won't work to expose as a support API. I'd like to follow the pattern set by the group supportSummary, which uses a resource type admin action … but Sam has no resource type for "user". Should we make one, even if we don't create user resources?

Example

This new API returns a payload that looks like:

{
  "additionalDetails": {
    "enterpriseFeatures": {
      "format": "flat",
      "resources": []
    }
  },
  "allowances": {
    "allowed": true,
    "details": {
      "enabled": true,
      "termsOfService": true
    }
  },
  "attributes": {
    "marketingConsent": true,
    "userId": "2736276474111094565b5"
  },
  "favoriteResources": [],
  "groupMembershipCounts": {
    "directSynchronized": 2,
    "totalSynchronized": 4,
    "unsynchronized": 1
  },
  "samUser": {
    "azureB2CId": "9ec01dd3-ddec-4273-8e34-39fdd2a480ab",
    "createdAt": "2025-01-07T19:01:14.166491Z",
    "email": "davidan.dev@gmail.com",
    "enabled": true,
    "googleSubjectId": "111137720113699110757",
    "id": "2736276474111094565b5",
    "registeredAt": "2025-01-07T19:01:14.164172Z",
    "updatedAt": "2025-01-07T19:01:18.917368Z"
  },
  "termsOfServiceDetails": {
    "acceptedOn": "2025-01-07T19:01:18.889191Z",
    "isCurrentVersion": true,
    "latestAcceptedVersion": "1",
    "permitsSystemUsage": true
  }
}

PR checklist

  • I've followed the instructions if I've made any changes to the API, especially if they're breaking changes
  • I've filled out the Security Risk Assessment (requires Broad Internal network access) and attached the result to the JIRA ticket

Map("enterpriseFeatures" -> enterpriseFeatures.toJson),
directGroupMembershipCount = directGroupMemberships,
indirectGroupMembershipCount = indirectGroupMemberships
)
Copy link
Contributor Author

@davidangb davidangb Jan 3, 2025

Choose a reason for hiding this comment

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

This follows the pattern set by getSamUserCombinedState in UserRoutesV2, in which business logic that requires multiple services is implemented in the route class. Is there a better place to put this logic?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't love having the business logic in the route class, but alternatives are probably not worth the effort and the pattern already exists in Sam so seems fine to me.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would put this in userService (which already knows about tosService).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately UserService doesn't know about ResourceService, which we need to get the enterprise features. Whaddya think - is it worth giving UserService that knowledge, or do we not actually care about the enterprise features at this point?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved to UserService. This is kinda hacky in that the method requires the caller to pass in a ResourceService. I am hesitant to change the UserService constructor just for this one API.

.apply()
.map(UserTable.unmarshalUserRecord)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

modeled after loadUserByAzureB2CId


query.map(rs => rs.int(1)).single().apply().getOrElse(0)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I based these SQL queries off previous Slack discussion. I believe they are correct … but would love confirmation/oversight!

Copy link
Contributor

Choose a reason for hiding this comment

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

They look correct to me but I haven't done any manual verification

Copy link
Collaborator

Choose a reason for hiding this comment

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

They also look correct to me. One thing to note is that this does not include public resources but we probably want to deal with that separately.

directoryDAO.countDirectGroupMemberships(samUser, samRequestContext)

def countIndirectGroupMemberships(samUser: SamUser, samRequestContext: SamRequestContext): IO[Int] =
directoryDAO.countIndirectGroupMemberships(samUser, samRequestContext)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did not create unit tests for the three new methods here in UserService, since they don't add any logic above/beyond the PostgresDirectoryDAO methods.

@davidangb davidangb marked this pull request as ready for review January 3, 2025 20:24
@davidangb davidangb requested a review from a team as a code owner January 3, 2025 20:24
Copy link
Contributor

@marctalbott marctalbott left a comment

Choose a reason for hiding this comment

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

My understanding is that we can't expand access to the Sam super admin group to everyone who will need to call this API, so creating a user resource_type_admin resource seems reasonable to me even though it's definitely a departure from other resource_type_admin resources


query.map(rs => rs.int(1)).single().apply().getOrElse(0)
}

Copy link
Contributor

Choose a reason for hiding this comment

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

They look correct to me but I haven't done any manual verification

@@ -146,6 +146,32 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/UserStatus'
/api/admin/v1/user/email/{email}/supportSummary:
Copy link
Contributor

Choose a reason for hiding this comment

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

I generally favor the usage of Sam user ids over emails when identifying users, but I expect emails will usually be more convenient for callers of this API. It might be helpful to expose this information via both email and user id in this case.

Map("enterpriseFeatures" -> enterpriseFeatures.toJson),
directGroupMembershipCount = directGroupMemberships,
indirectGroupMembershipCount = indirectGroupMemberships
)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't love having the business logic in the route class, but alternatives are probably not worth the effort and the pattern already exists in Sam so seems fine to me.

termsOfServiceDetails: TermsOfServiceDetails,
additionalDetails: Map[String, JsValue],
directGroupMembershipCount: Int,
indirectGroupMembershipCount: Int // TODO: for clarity, should this be "directAndIndirectGroup..."?
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I might think to add the two counts together to get total otherwise. Maybe totalGroupMembershipCount?

Copy link
Contributor

Choose a reason for hiding this comment

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

I also wonder if they should indicate that they are counts of synchronized groups and not total group membership in Sam

Copy link
Contributor Author

@davidangb davidangb Jan 7, 2025

Choose a reason for hiding this comment

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

I refactored this; now we have counts for directSynchronized, totalSynchronized, and unsynchronized.

Copy link
Collaborator

@dvoet dvoet left a comment

Choose a reason for hiding this comment

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

Sam currently has 2 admin mechanisms, the newer backed by resource type admin and the older backed by the fc-admins google group. It looks like the older is only used to gate access to user specific apis (should verify this). It would be great to consolidate to the newer. I think that would mean creating a user resource type admin resource even though user is not a resource type. I could see that being a follow on ticket. All of our support staff need to be suitable though, so they may already be members of fc-admins.

Map("enterpriseFeatures" -> enterpriseFeatures.toJson),
directGroupMembershipCount = directGroupMemberships,
indirectGroupMembershipCount = indirectGroupMemberships
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would put this in userService (which already knows about tosService).


query.map(rs => rs.int(1)).single().apply().getOrElse(0)
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

They also look correct to me. One thing to note is that this does not include public resources but we probably want to deal with that separately.

@dvoet
Copy link
Collaborator

dvoet commented Jan 6, 2025

This new API is very similar to the getSamUserCombinedState API. Should they be consolidated?

I think it would be nice if they were the same api under hood. Exposing the counts to the user may be useful

@davidangb davidangb marked this pull request as draft January 7, 2025 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants