diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1df2db1..ba67e3e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,9 +10,6 @@ on: - main - develop pull_request: - branches: - - main - - develop workflow_dispatch: jobs: diff --git a/build.savant b/build.savant index 3a7a890..a4db54a 100644 --- a/build.savant +++ b/build.savant @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2024, FusionAuth, All Rights Reserved + * Copyright (c) 2018-2025, FusionAuth, All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ target(name: "clean", description: "Cleans the build directory") { target(name: "compile", description: "Builds archives of the source and compiled versions of the code.", dependsOn: ["setup-python"]) { def proc = "python3 setup.py sdist bdist_wheel".execute() proc.consumeProcessOutput(System.out, System.err) - proc.waitFor() + assert proc.waitFor() == 0 } target(name: "int", description: "Releases a local integration build of the project", dependsOn: ["compile"]) { @@ -78,11 +78,11 @@ target(name: "test", description: "Runs the project's tests", dependsOn: ["compi target(name: "setup-python", description: "Gets the python dependencies") { def proc1 = "python3 -m pip install --user --upgrade setuptools".execute() proc1.consumeProcessOutput(System.out, System.err) - proc1.waitFor() + assert proc1.waitFor() == 0 def proc2 = "python3 -m pip install --user --upgrade wheel twine requests deprecated".execute() proc2.consumeProcessOutput(System.out, System.err) - proc2.waitFor() + assert proc2.waitFor() == 0 } /** @@ -104,8 +104,7 @@ target(name: "publish", description: "Publishes source and built versions of the def process = pb.start() process.consumeProcessOutput(System.out, System.err) - process.waitFor() - return process.exitValue() == 0 + assert process.waitFor() == 0 } target(name: "release", description: "Releases a full version of the project", dependsOn: ["int"]) { diff --git a/src/main/python/fusionauth/fusionauth_client.py b/src/main/python/fusionauth/fusionauth_client.py index b4e2086..a8ad73f 100644 --- a/src/main/python/fusionauth/fusionauth_client.py +++ b/src/main/python/fusionauth/fusionauth_client.py @@ -260,6 +260,18 @@ def comment_on_user(self, request): .post() \ .go() + def complete_verify_identity(self, request): + """ + Completes verification of an identity using verification codes from the Verify Start API. + + Attributes: + request: The identity verify complete request that contains all the information used to verify the identity. + """ + return self.start().uri('/api/identity/verify/complete') \ + .body_handler(JSONBodyHandler(request)) \ + .post() \ + .go() + def complete_web_authn_assertion(self, request): """ Complete a WebAuthn authentication ceremony by validating the signature against the previously generated challenge without logging the user in @@ -3400,6 +3412,20 @@ def retrieve_user_by_login_id(self, login_id): .get() \ .go() + def retrieve_user_by_login_id_with_login_id_types(self, login_id, login_id_types): + """ + Retrieves the user for the loginId, using specific loginIdTypes. + + Attributes: + login_id: The email or username of the user. + login_id_types: the identity types that FusionAuth will compare the loginId to. + """ + return self.start().uri('/api/user') \ + .url_parameter('loginId', self.convert_true_false(login_id)) \ + .url_parameter('loginIdTypes', self.convert_true_false(login_id_types)) \ + .get() \ + .go() + def retrieve_user_by_username(self, username): """ Retrieves the user for the given username. @@ -3581,6 +3607,27 @@ def retrieve_user_login_report_by_login_id(self, login_id, start, end, applicati .get() \ .go() + def retrieve_user_login_report_by_login_id_and_login_id_types(self, login_id, start, end, login_id_types, application_id=None): + """ + Retrieves the login report between the two instants for a particular user by login Id, using specific loginIdTypes. If you specify an application id, it will only return the + login counts for that application. + + Attributes: + application_id: (Optional) The application id. + login_id: The userId id. + start: The start instant as UTC milliseconds since Epoch. + end: The end instant as UTC milliseconds since Epoch. + login_id_types: the identity types that FusionAuth will compare the loginId to. + """ + return self.start().uri('/api/report/login') \ + .url_parameter('applicationId', self.convert_true_false(application_id)) \ + .url_parameter('loginId', self.convert_true_false(login_id)) \ + .url_parameter('start', self.convert_true_false(start)) \ + .url_parameter('end', self.convert_true_false(end)) \ + .url_parameter('loginIdTypes', self.convert_true_false(login_id_types)) \ + .get() \ + .go() + def retrieve_user_recent_logins(self, user_id, offset, limit): """ Retrieves the last number of login records for a user. @@ -4210,6 +4257,18 @@ def send_two_factor_code_for_login_using_method(self, two_factor_id, request): .post() \ .go() + def send_verify_identity(self, request): + """ + Send a verification code using the appropriate transport for the identity type being verified. + + Attributes: + request: The identity verify send request that contains all the information used send the code. + """ + return self.start().uri('/api/identity/verify/send') \ + .body_handler(JSONBodyHandler(request)) \ + .post() \ + .go() + def start_identity_provider_login(self, request): """ Begins a login request for a 3rd party login that requires user interaction such as HYPR. @@ -4253,6 +4312,19 @@ def start_two_factor_login(self, request): .post() \ .go() + def start_verify_identity(self, request): + """ + Start a verification of an identity by generating a code. This code can be sent to the User using the Verify Send API + Verification Code API or using a mechanism outside of FusionAuth. The verification is completed by using the Verify Complete API with this code. + + Attributes: + request: The identity verify start request that contains all the information used to begin the request. + """ + return self.start().uri('/api/identity/verify/start') \ + .body_handler(JSONBodyHandler(request)) \ + .post() \ + .go() + def start_web_authn_login(self, request): """ Start a WebAuthn authentication ceremony by generating a new challenge for the user @@ -4835,6 +4907,18 @@ def verify_email_address_by_user_id(self, request): .post() \ .go() + def verify_identity(self, request): + """ + Administratively verify a user identity. + + Attributes: + request: The identity verify request that contains information to verify the identity. + """ + return self.start().uri('/api/identity/verify') \ + .body_handler(JSONBodyHandler(request)) \ + .post() \ + .go() + @deprecated("This method has been renamed to verify_user_registration and changed to take a JSON request body, use that method instead.") def verify_registration(self, verification_id): """ diff --git a/src/test/python/fusionauth/fusionauth_client_test.py b/src/test/python/fusionauth/fusionauth_client_test.py index dd72caf..0700681 100644 --- a/src/test/python/fusionauth/fusionauth_client_test.py +++ b/src/test/python/fusionauth/fusionauth_client_test.py @@ -1,4 +1,28 @@ -# Copyright (c) 2024, FusionAuth, All Rights Reserved +# Copyright (c) 2024-2025, FusionAuth, All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific +# language governing permissions and limitations under the License. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -26,9 +50,8 @@ import json import os -import uuid - import unittest +import uuid from fusionauth.fusionauth_client import FusionAuthClient @@ -51,7 +74,8 @@ def runTest(self): def test_retrieve_applications(self): client_response = self.client.retrieve_applications() self.assertEqual(client_response.status, 200) - self.assertEqual(len(client_response.success_response['applications']), 2) + # tnent manager is 1 application, admin is another, Pied piper in the kickstart is the 3rd. + self.assertEqual(len(client_response.success_response['applications']), 3) def test_create_user_retrieve_user(self): # Check if the user already exists. @@ -84,6 +108,25 @@ def test_create_user_retrieve_user(self): self.assertFalse('password' in get_user_response.success_response['user']) self.assertFalse('salt' in get_user_response.success_response['user']) + # Retrieve the user via loginId + get_user_response = self.client.retrieve_user_by_login_id('user@example.com') + self.assertEqual(get_user_response.status, 200) + self.assertIsNotNone(get_user_response.success_response) + self.assertIsNone(get_user_response.error_response) + self.assertEqual(get_user_response.success_response['user']['email'], 'user@example.com') + + # Explicit loginIdType + get_user_response = self.client.retrieve_user_by_login_id_with_login_id_types('user@example.com', ['email']) + self.assertEqual(get_user_response.status, 200) + self.assertIsNotNone(get_user_response.success_response) + self.assertIsNone(get_user_response.error_response) + self.assertEqual(get_user_response.success_response['user']['email'], 'user@example.com') + + # TODO: Once issue 1 is released, this test should pass + # # wrong loginIdType + # get_user_response = self.client.retrieve_user_by_login_id_with_login_id_types('user@example.com', ['phoneNumber']) + # self.assertEqual(get_user_response.status, 404) + def test_retrieve_user_missing(self): user_id = uuid.uuid4() client_response = self.client.retrieve_user(user_id)