Skip to content

Commit

Permalink
feat: add login_methods field in ProfileNode
Browse files Browse the repository at this point in the history
Refs: HP-2490
  • Loading branch information
danipran committed Jul 23, 2024
1 parent 1506f23 commit 333dcba
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
phones(offset: Int, before: String, after: String, first: Int, last: Int): PhoneNodeConnection
addresses(offset: Int, before: String, after: String, first: Int, last: Int): AddressNodeConnection
contactMethod: ContactMethod
loginMethods: [String]
sensitivedata: SensitiveDataNode
serviceConnections(offset: Int, before: String, after: String, first: Int, last: Int): ServiceConnectionTypeConnection
verifiedPersonalInformation: VerifiedPersonalInformationNode
Expand Down
1 change: 1 addition & 0 deletions profiles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class Meta:
"verified_personal_information",
"language",
"contact_method",
"login_methods",
]

def resolve_profile(self):
Expand Down
30 changes: 29 additions & 1 deletion profiles/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
download_connected_service_data,
)
from .enums import AddressType, EmailType, PhoneType
from .keycloak_integration import delete_profile_from_keycloak
from .keycloak_integration import delete_profile_from_keycloak, get_user_login_methods
from .models import (
Address,
ClaimToken,
Expand Down Expand Up @@ -514,6 +514,10 @@ def validate_ssn(value, info, **input):


class RestrictedProfileNode(DjangoObjectType):
"""
Profile node with a restricted set of data. This does not contain any sensitive data.
"""

class Meta:
model = Profile
fields = ("first_name", "last_name", "nickname", "language")
Expand Down Expand Up @@ -575,6 +579,11 @@ class Meta:
connection_class = ProfilesConnection
filterset_class = ProfileFilter

login_methods = graphene.List(
graphene.String,
description="List of login methods that the profile has used to authenticate. "
"Only visible to the user themselves.",
)
sensitivedata = graphene.Field(
SensitiveDataNode,
description="Data that is consider to be sensitive e.g. social security number",
Expand All @@ -589,6 +598,25 @@ class Meta:
"privileges to access this information.",
)

def resolve_login_methods(self: Profile, info, **kwargs):
if info.context.user != self.user:
raise PermissionDenied(
"No permission to read login methods of another user."
)

amr = {info.context.user_auth.data.get("amr")}

# For future software archeologists:
# This field was added to the API to support the front-end's need to know
# which login methods the user has used. It's only needed for profiles
# with helsinki-tunnus or Suomi.fi, so for other cases, save a couple
# API calls and return an empty list. There's no other reasoning for the
# logic here.
if amr.intersection({"helsinki_tunnus", "heltunnistussuomifi", "suomi_fi"}):
return get_user_login_methods(self.user.uuid)

return []

def resolve_service_connections(self: Profile, info, **kwargs):
return self.effective_service_connections_qs()

Expand Down
58 changes: 58 additions & 0 deletions profiles/tests/test_gql_my_profile_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,3 +576,61 @@ def test_my_profile_checks_allowed_data_fields_for_multiple_queries(
assert executed["data"]["myProfile"]["lastName"] == profile.last_name
assert executed["data"]["myProfile"]["sensitivedata"] is None
assert executed["data"]["services"] is None


@pytest.mark.parametrize(
"amr_claim_value", ["suomi_fi", "helsinki_tunnus", "heltunnistussuomifi"]
)
def test_user_can_see_own_login_methods_with_correct_amr_claim(
user_gql_client, profile, group, service, monkeypatch, amr_claim_value
):
def mock_return(*_, **__):
return {"foo", "bar"}

monkeypatch.setattr(
"profiles.keycloak_integration.get_user_identity_providers", mock_return
)

profile = ProfileFactory(user=user_gql_client.user)
ServiceConnectionFactory(profile=profile, service=service)

query = """
{
myProfile {
loginMethods
}
}
"""
executed = user_gql_client.execute(
query, auth_token_payload={"amr": amr_claim_value}, service=service
)
assert "errors" not in executed
assert set(executed["data"]["myProfile"]["loginMethods"]) == {"foo", "bar"}


@pytest.mark.parametrize("amr_claim_value", [None, "helsinkiad"])
def test_user_cannot_see_own_login_methods_with_other_amr_claims(
user_gql_client, profile, group, service, monkeypatch, amr_claim_value
):
def mock_return(*_, **__):
return {"this should not show up"}

monkeypatch.setattr(
"profiles.keycloak_integration.get_user_identity_providers", mock_return
)

profile = ProfileFactory(user=user_gql_client.user)
ServiceConnectionFactory(profile=profile, service=service)

query = """
{
myProfile {
loginMethods
}
}
"""
executed = user_gql_client.execute(
query, auth_token_payload={"amr": amr_claim_value}, service=service
)
assert "errors" not in executed
assert executed["data"]["myProfile"]["loginMethods"] == []
26 changes: 26 additions & 0 deletions profiles/tests/test_gql_profiles_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,3 +798,29 @@ def test_not_specifying_requesters_service_results_in_permission_denied_error(
"""
executed = user_gql_client.execute(query)
assert_match_error_code(executed, "PERMISSION_DENIED_ERROR")


def test_staff_user_can_not_query_login_methods_of_other_users(
user_gql_client, group, service
):
profile = ProfileFactory()
ServiceConnectionFactory(profile=profile, service=service)
user = user_gql_client.user
user.groups.add(group)
assign_perm("can_view_profiles", group, service)

query = """
{
profiles {
edges {
node {
loginMethods
}
}
}
}
"""

executed = user_gql_client.execute(query, service=service)
assert "errors" in executed
assert_match_error_code(executed, "PERMISSION_DENIED_ERROR")

0 comments on commit 333dcba

Please sign in to comment.